之所以研究这个算法,是因为最近在研究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; } } } }
代码运行的结果是: