原文地址:http://www.cnblogs.com/zhenyulu/articles/657017.html
上篇文章《天書般的ICTCLAS分詞系統代碼(一)》 說了說ICTCLAS分詞系統有些代碼讓人無所適從,需要好一番努力才能弄明白究竟是怎麼回事。儘管有很多人支持應當寫簡單、清晰的代碼,但也有人持不同意見。主要集中在(1)如果效率高,代碼複雜點也行; (2)只要註釋寫得好就行;(3)軟件關鍵在思路(這我同意),就好像買了一臺電腦,不管包裝箱內的電腦本身怎麼,一羣人偏在死扣那個外面透明膠帶帖歪了(這我堅決不同意,因爲只有好思路出不來好電腦,好電腦還要性能穩定,即插即用的好硬件;另外天書般的代碼不僅僅是透明膠帶 貼歪的問題,他甚至可能意味着電腦中的絕緣膠帶失效了...)。
這兩天在抓緊學習ICTCLAS分詞系統的思路的同時,也在消化學習它的代碼實現,然而我看到的代碼已經不僅僅是爲了效率犧牲代碼清晰度的問題了,我看到的是連作者都不知道自己真正想要做什麼了,儘管程序的執行結果是正確的!
爲了說明這種情況的嚴重性,我們需要從CQueue.cpp這個文件着手。我對CQueue這個類頗有些微辭,明明是個Queue,裏面確用的是Push、Pop方法(讓人感覺是個Stack而不是Queue),而且Pop方法純粹是個大雜燴,不過這些都不是原則性問題,畢竟每個人有每個人寫代碼的習慣。CQueue完成的工作是製造一個排序隊列(按照eWeight從小到大排序),如圖一:
(圖一)
在瞭解了這些內容的基礎上,讓我們看看ICTCLAS中NShortPath.cpp中的代碼實現(這裏我們只看ShortPath方法的實現) ,爲了讓問題暴露得更清晰一些,我簡化了代碼中一些不相關的內容。
{
......
for (; nCurNode < m_nVertex; nCurNode++)
{
CQueue queWork;
//此處省略的代碼主要負責將一些結點按照eWeight從
//小到大的順序放入隊列queWork
......
//初始化權重
for (i = 0; i < m_nValueKind; i++)
m_pWeight[nCurNode - 1][i] = INFINITE_VALUE;
i = 0;
while (i < m_nValueKind && queWork.Pop(&nPreNode, &nIndex, &eWeight) != -1)
{
//Set the current node weight and parent
if (m_pWeight[nCurNode - 1][i] == INFINITE_VALUE)
m_pWeight[nCurNode - 1][i] = eWeight;
else if (m_pWeight[nCurNode - 1][i] < eWeight)
//Next queue
{
i++; //Go next queue and record next weight
if (i == m_nValueKind)
//Get the last position
break;
m_pWeight[nCurNode - 1][i] = eWeight;
}
m_pParent[nCurNode - 1][i].Push(nPreNode, nIndex);
}
}
......
}
上面的代碼作者想幹什麼?讓我們來分析一番:
變量queWork中存放的是一個按照eWeight從小到大排列的隊列, 我們不妨假設裏面有4個元素,其eWeight值分別是5、6、7、8。另外我們假設變量m_nValueKind的值爲2,即查找最短的兩條路徑(注意:這種說法不完全正確,後面會解釋爲什麼)。在此假設基礎上,我們看看程序是如何運行的:
1)將所有m_pWeight[nCurNode - 1][i]初始化爲INFINITE_VALUE。
2)在第一輪循環中,我們從queWork中取出第一個元素,其eWeight爲5,注意表達式“if (m_pWeight[nCurNode - 1][i] == INFINITE_VALUE) ”沒有任何作用,因爲我們在第一步將所有m_pWeight[nCurNode - 1][i] 均初始化成了INFINITE_VALUE,所以第一輪循環該條件一定爲true。
3)在第二輪循環中,我們從queWork中取出第二個元素,其eWeight爲6,此時表達式“else if (m_pWeight[nCurNode - 1][i] < eWeight) ”似乎就沒有什麼作用了,因爲queWork是經過排序的,第二個元素的eWeight不會小於第一個eWeight,對於我們這個例子來說, 該表達式一定爲true,於是就讓 i++。
4)緊接着你會發現程序重新進入了步驟2)的循環。
程序執行結果如圖二:
(圖二)
如果真是這樣的話,上面的代碼似乎可以簡化成:
{
......
for (; nCurNode < m_nVertex; nCurNode++)
{
CQueue queWork;
//此處省略的代碼主要負責將一些結點按照eWeight從
//小到大的順序放入隊列queWork
......
//初始化權重
for (i = 0; i < m_nValueKind; i++)
m_pWeight[nCurNode - 1][i] = INFINITE_VALUE;
i = 0;
while (i < m_nValueKind && queWork.Pop(&nPreNode, &nIndex, &eWeight) != -1)
{
m_pWeight[nCurNode - 1][i] = eWeight;
m_pParent[nCurNode - 1][i].Push(nPreNode, nIndex);
i++;
}
}
......
}
對於上面這個案例,簡化後的程序與ICTCLAS中的程序執行結果完全相同。可作者寫出如此複雜的代碼應當是有理由的,難道我們對代碼的分析有什麼問題嗎?
是的!作者將一個最爲重要的內容作爲隱含條件放入了代碼之中,我們只能通過 if 條件以及 else if 條件中的內容推斷出這個隱含條件究竟是什麼,而這個隱含的條件恰恰應當是這段代碼中最關鍵的內容。如果沒能將最關鍵的內容展現在代碼當中,而是需要讀者去推斷的話,我只能說連作者自己都不清楚究竟什麼是最關鍵的東西,僅僅是讓程序執行沒有錯誤而已。
那麼究竟隱藏了什麼關鍵的內容呢?那就是“m_pWeight[nCurNode - 1][i] = eWeight”這個條件。在ShortPath方法代碼中,作者用了 if 條件、 else if 條件,但都沒有提及等於eWeight時程序的執行行爲,他將這個留給了讀者去推敲,看出來這個隱含條件就看出來了,看不出來就只能怪你自己笨了。
我們更換一組數據來看看:假設queWork裏面有4個元素,其eWeight值分別是5、6、6、7,還假設變量m_nValueKind的值爲2,那麼ICTCLAS中ShortPath程序執行結果是什麼呢?讀者可以根據代碼自己推敲一下,然後再看看下面的結果,與你預期的一樣不一樣。如圖三。
(圖三)
這裏m_Parent[nCurNode - 1][2]是一個CQueue,裏面存入了eWeight爲6的兩個結點。這也是爲什麼我前文說,NShortPath中 N 如果取2,並不意味着只有兩條路徑。
如果那位有耐心看到這裏,對ICTCLAS中的NShortPath.cpp代碼有什麼感覺呢?其實要想寫出一個比較清晰的代碼並不複雜,只要你真正瞭解究竟什麼是最重要的東西,對於NShortPath.cpp中的代碼,只要我們稍加修改,就可以讓這天書般的代碼改善不少。經過調整後的代碼如下:
{
......
for (; nCurNode < m_nVertex; nCurNode++)
{
CQueue queWork;
//此處省略的代碼主要負責將一些結點按照eWeight從
//小到大的順序放入隊列queWork
......
//初始化權重
for (i = 0; i < m_nValueKind; i++)
m_pWeight[nCurNode - 1][i] = INFINITE_VALUE;
if(queWork.Pop(&nPreNode, &nIndex, &eWeight) != -1)
{
for(i=0; i < m_nValueKind ; i++)
{
m_pWeight[nCurNode - 1][i] = eWeight;
do
{
m_pParent[nCurNode - 1][i].Push(nPreNode, nIndex);
if(queWork.Pop(&nPreNode, &nIndex, &new_eWeight) == -1)
goto finish;
}while(new_eWeight == eWeight)
eWeight = new_eWeight;
}
}
}
finish:
......
}
經過改造的代碼使用了一個do...while循環,並利用了goto命令簡化代碼結構,我想這樣的代碼讀起來應當清晰多了吧。
- 小結
(1)軟件關鍵在思路,只有真正瞭解思路的人才能寫出清晰的代碼。如果代碼不清晰,說明思路根本不清晰。
(2)註釋寫得好不如代碼結構清晰。
(3)除非經過測試,否則不要爲了一點效率提升而損失代碼的可讀性。