天書般的ICTCLAS分詞系統代碼(二)

原文地址: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方法的實現) ,爲了讓問題暴露得更清晰一些,我簡化了代碼中一些不相關的內容。

來自NShortPath.cpp中的ShortPath方法
int CNShortPath::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)的循環。

程序執行結果如圖二:

(圖二)

如果真是這樣的話,上面的代碼似乎可以簡化成:

簡化後的程序
int CNShortPath::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)  
    {  
      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中的代碼,只要我們稍加修改,就可以讓這天書般的代碼改善不少。經過調整後的代碼如下:

重新改造後的代碼
int CNShortPath::ShortPath()  
{  
  ......  
  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)除非經過測試,否則不要爲了一點效率提升而損失代碼的可讀性。

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章