之所以研究這個算法,是因為最近在研究NLP中文的分詞,所謂分詞就是將一個完整的句子,例如“計算語言學課程有意思”,分解成一些詞組單元“計算語言學,課程,有,意思”。 “最大匹配法” 在中文分詞中有所應用,因此這里介紹一下。
“最大匹配法” 分為正向匹配和逆向匹配,這里先看正向匹配。
算法思想:
正向最大匹配算法:從左到右將待分詞文本中的幾個連續字符與詞表匹配,如果匹配上,則切分出一個詞。但這里有一個問題:要做到最大匹配,並不是第一次匹配到就可以切分的 。我們來舉個例子:
待分詞文本: sentence[]={"計","算","語","言","學","課","程","有","意","思"}
詞表: dict[]={"計算", "計算語言學", "課程", "有", "意思"} (真實的詞表中會有成千上萬個已經平時我們使用的分好的詞語)
(1) 從sentence[1]開始,當掃描到sentence[2]的時候,發現"計算"已經在詞表dict[]中了。但還不能切分出來,因為我們不知道后面的詞語能不能組成更長的詞(最大匹配)。
(2) 繼續掃描content[3],發現"計算語"並不是dict[]中的詞。但是我們還不能確定是否前面找到的"計算語"已經是最大的詞了。因為"計算語"是dict[2]的前綴。
(3) 掃描content[4],發現"計算語言"並不是dict[]中的詞。但是是dict[2]的前綴。繼續掃描:
(3) 掃描content[5],發現"計算語言學"是dict[]中的詞。繼續掃描下去:
(4) 當掃描content[6]的時候,發現"計算語言學課"並不是詞表中的詞,也不是詞的前綴。因此可以切分出前面最大的詞——"計算語言學"。
由此可見,最大匹配出的詞必須保證下一個掃描不是詞表中的詞或詞的前綴才可以結束。
代碼實現:
這里字典就臨時這樣簡單寫死,真實的情況需要構造一個hash,這樣效率較高,我們看下算法的代碼吧:
#include<iostream> #include<string> using namespace std; // 宏,計算數組個數 #define GET_ARRAY_LEN(array,len){len=(sizeof(array)/sizeof(array[0]));} string dict[] = {"計算", "計算語言學", "課程", "有", "意思"}; // 是否為詞表中的詞或者是詞表中詞的前綴 bool inDict(string str) { bool res = false; int i; int len = 0; GET_ARRAY_LEN(dict, len); for (i = 0; i<len; i++) { // 是否和詞表詞相等或者是詞表詞前綴 if( str == dict[i].substr(0, str.length())) { res = true; } } return res; } int main() { string sentence = "計算語言學課程有意思"; string word = "一"; int wordlen = word.length(); int i; string s1 = ""; for (i = 0; i<sentence.length(); i = i+wordlen) { string tmp = s1 + sentence.substr(i, wordlen); if(inDict(tmp)) { s1 = s1 + sentence.substr(i, wordlen); } else { cout<<"分詞結果:"<<s1<<endl; s1 = sentence.substr(i, wordlen); } } cout<<"分詞結果:"<<s1<<endl; }
我在linux下運行的結果是:
可以看到分詞的結果符合我們的預期,如果詞表足夠大,那么所有的句子都可以被分詞。iao
接下來說下逆向的最大匹配算法。
算法思想:
逆向匹配算法大致思路是從右往左開始切分。我們還是用上面的例子:
待分詞句子: sentence[]={"計算語言學課程有意思"}
詞表: dict[]={"計算", "計算語言學", "課程", "有", "意思"}
首先我們定義一個最大分割長度5,從右往左開始分割:
(1) 首先取出來的候選詞W是 “課程有意思”。
(2) 查詞表,W不在詞表中,將W最左邊的第一個字去掉,得到W“程有意思”;
(3) 查詞表,W也不在詞表中,將W最左邊的第一個字去掉,得到W“有意思”;
(4) 查詞表,W也不在詞表中,將W最左邊的第一個字再去掉,得到W“意思”;
(5) 查詞表,W在詞表中,就將W從整個句子中拆分出來,此時原句子為“計算語言學課程有”
(6) 根據分割長度5,截取句子內容,得到候選句W是“語言學課程有”;
(7) 查詞表,W不在詞表中,將W最左邊的第一個字去掉,得到W“言學課程有”;
(8) 查詞表,W也不在詞表中,將W最左邊的第一個字去掉,得到W“學課程有”;
(9) 依次類推,直到W為“有”一個詞的時候,這時候將W從整個句子中拆分出來,此時句子為“計算語言學課程”
(10) 根據分割長度5,截取句子內容,得到候選句W是“算語言學課程”;
(11) 查詞表,W不在詞表中,將W最左邊的第一個字去掉,得到W“語言學課程”;
(12) 依次類推,直到W為“課程”的時候,這時候將W從整個句子中拆分出來,此時句子為“計算語言學”
(13) 根據分割長度5,截取句子內容,得到候選句W是“計算語言學”;
(14) 查詞表,W在詞表,分割結束。
代碼實現:
#include<iostream> #include<string> using namespace std; // 宏,計算數組個數 #define GET_ARRAY_LEN(array,len){len=(sizeof(array)/sizeof(array[0]));} // 定義最大詞長 #define MAX_WORD_LENGTH 5 string dict[] = {"計算", "計算語言學", "課程", "意思"}; // 是否為詞表中的詞 bool inDict(string str) { bool res = false; int i; int len = 0; GET_ARRAY_LEN(dict, len); for (i = 0; i<len; i++) { if( str == dict[i]) { res = true; } } return res; } int main() { string sentence = "計算語言學課程有意思"; string word = "一"; int wordlen = word.length(); int i; for (i = 0; i<sentence.length(); ) { int dealstrbegin = sentence.length()-wordlen*MAX_WORD_LENGTH-i; int dealstrlen = wordlen*MAX_WORD_LENGTH; // 截取的要處理的字符串 string dealstr = sentence.substr(dealstrbegin, dealstrlen); int j; for (j = 0; j<MAX_WORD_LENGTH; j++) { int fb = j*wordlen; int fl = wordlen*(MAX_WORD_LENGTH-j); // 去掉簽名的j個字 string tmp = dealstr.substr(fb, fl); if(inDict(tmp) || j==MAX_WORD_LENGTH-1 ) { cout<<"分詞結果:"<<tmp<<endl; i=i+fl; break; } } } }
代碼運行的結果是: