这几天在研究中文分词,目前已经研究试验了基于词典的常用中文分词算法,包括正向最大匹配、逆向最大匹配、整词二分法、基于tire的中文分词、逐词二分法、双字多字hash的方法,稍后的文章会提及中文分词的方法和程序。此篇文章是基于tire的中文分词中检索树的实现,希望对tire感兴趣或者想研究中文分词的朋友有所帮助,仅做交流。 firstChHash.h头文件内容: /* 描述: 首字hash函数的实现和说明 作者: xiaocui 时间: 2008.2.27 版本: v1.0 */ /* 首字hash方法: 在整词二分法,基于Trie树的分词方法,逐字二分法,多次 hash方法中,第一部都需要首字hash方法。首字hash方法采用2级hash策略,对 gb2312编码中的常用汉字,利用gb2312编码的区位码概念,hash函数为: index = (区号 – 0xA0 – 16) * 94 + (位号 – 0xA0 – 1),index为该汉字的索引。 由于gb2312中汉字有限,对没有出现在gb2312编码中的汉字,利用其gbk编码中的 高字节、低字节概念进行类似的hash,gbk的hash作为二次hash。下面先给出关于 gb2312和gbk编码的一些背景知识。 背景知识:GB2312标准共收录6763个汉字,其中一级汉字3755个,二级汉字3008个. GB2312中对所收汉字进行了“分区”处理,每区含有94个汉字/符号。这种表示方式 也称为区位码。具体分区为: 01-09区为特殊符号。 16-55区为一级汉字,按拼音排序。 56-87区为二级汉字,按部首/笔画排序。 10-15区及88-94区则未有编码。 举例来说,“啊”字是GB2312之中的第一个汉字,它的区位码就是1601。 为了区分汉字字符和ascii码,汉字的区码和位码都加上了OxA0,所以最高位都为1, char显示为负值。通过(char + 256) % 256可以得到其对应的正值。 ////////////////////////////////////////////////////////////////////////// gbk编码介绍: 子集 编码范围 编码空间 编码字数 ===== ============= ======== ========= GBK/1 0xA1A1-0xA9FE 846 717 GBK/2 0xB0A1-0xF7FE 6,768 6,763 //这一部分向后兼容gb2312编码 GBK/3 0x8140-0xA0FE 6,080 6,080 GBK/4 0xAA40-0xFEA0 8,160 8,160 GBK/5 0xA840-0xA9A0 192 166 EUDC/1 0xAAA1-0xAFFE 564 用户定义1 EUDC/2 0xF8A1-0xFEFE 658 用户定义2 EUDC/3 0xA140-0xA7A0 672 用户定义3 从上表可以看出,GBK共提供了23,940字的编码空间, 实际定义了 21,886汉字,可用户定义1,894汉字。双字节编码规则如下: 第一字节 0x81-0xFE 第二字节 0x40-0x7E, 0x80-0xFE;每行定义190汉字 */ /* 首字hash函数1,针对gb2312拥有的99.5%的常用汉字的hash 输入: 汉字(分高低2字节) 输出: 该汉字的索引号 */ #include <iostream> using namespace std; inline int hashGb2312(const char* ch) { //检验是不是gb2312编码 if ( ((ch[0] + 256) % 256 – 0xA0 < 16) || ((ch[0] + 256) % 256 – 0xA0 > 87) )//gb2312汉字编码高位从第16区到第87区 { return -1; } if ( ((ch[1] + 256) % 256 – 0xA0 < 1) || ((ch[1] + 256) % 256 – 0xA0 > 94) )//gb2312汉字编码低位从1到94 { return -1; } int sectionIndex = (ch[0] + 256) % 256 – 0xA0 – 16; //区号(基数为0),减16因为gb2312前15区没用,汉字区号从第16区开始 int locationIndex = (ch[1] + 256) % 256 – 0xA0 – 1; //位号(基数为0),减1因为gb2312位号从1开始,希望从0开始,故减1 int index = sectionIndex * 94 + locationIndex; //gb2312每区94个字符,这个保证hash的结果和区位码是一一对应的 return index; } /*首字hash函数2,针对gb2312编码中没有出现的汉字的hash函数 输入: 汉字(分高低2字节) 输出: 该汉字的索引号 */ inline int hashGbk(const char* ch) { int highIndex = (ch[0] + 256) % 256 – 0x81; int lowIndex; if ( (ch[1]+256)% 256 > 0x7f ) { lowIndex = (ch[1] + 256) % 256 – 0x40 – 1; //第二字节不能是0x7f,所以第二字节比0x7f大的再多减1,这样防止hash空白空间的浪费 } else { lowIndex = (ch[1] + 256) % 256 – 0x40; } int index = highIndex * 190 + lowIndex; return index; } trieTree.h头文件内容: #include <vector> using namespace std; /* 检索树节点定义 */ struct node { vector<char*> mKeyWordVec; //关键字向量(递增顺序,便于以后二分查找) vector<node*> mLinkVec; //指向子检索树的指针向量 }; /* 检索树类的定义 */ class trieTree { public: trieTree(node* root = NULL):mRoot(root){} node* getTrieTreeRoot() const { return mRoot; } trieTree& insert(const char* str); //增加新的字符串 trieTree& del(const char* str); //删除指定字符串 bool find(const char* str); //在检索树中查找指定字符串 trieTree findCh(const char* ch); //在检索树中检索单独汉字 private: node* mRoot; }; /* 根据区位码比较2个汉字序列的大小 输入: 2个汉字序列 输出: 0表示2个汉字序列相等;正数1表示前者大,负数1表示前者小 */ int wordCmp(const char* chineseWord1, const char* chineseWord2); /* 汉字的二分查找 */ int bSearch(const vector<char*>& vec, const char* word); /* 特殊二分查找,如果在有序序列中没有找到指定元素,返回该元素应该被插入的位置 */ int specBSearch(const vector<char*>& vec, char* word); /* 有序序列的插入,返回插入的位置 */ int insertToVec(vector<char*>& vec, char* word); 实现和测试文件: // trieTree.cpp : Defines the entry point for the console application. // /* 描述: 自己实现的检索树 作者: xiaocui 时间: 2008.2.27 版本: v1.0 */ #include “stdafx.h” #include “trieTree.h” #include “firstChHash.h” #include <iostream> #include <vector> #include <string> using namespace std; /* 根据区位码比较2个汉字序列的大小 输入: 2个汉字序列 输出: 0表示2个汉字序列相等;正数1表示前者大,负数1表示前者小 */ int wordCmp(const char* chineseWord1, const char* chineseWord2) { size_t len1 = strlen(chineseWord1); size_t len2 = strlen(chineseWord2); int firstIndex; int secondIndex; size_t i; for (i = 0; i < (len1 < len2 ? len1 : len2); i += 2) { char ch1[3]; ch1[0] = chineseWord1[i]; ch1[1] = chineseWord1[i+1]; ch1[2] = ‘/0’; char ch2[3]; ch2[0] = chineseWord2[i]; ch2[1] = chineseWord2[i+1]; ch2[2] = ‘/0’; firstIndex = hashGb2312(ch1); secondIndex = hashGb2312(ch2); if ( (firstIndex >= 0) && (secondIndex >= 0) && (firstIndex < secondIndex) ) //2个首字都是gb2312的常用汉字 { return -1; } else if ( (firstIndex >= 0) && (secondIndex >= 0) && (firstIndex > secondIndex) ) { return 1; } else if ( (firstIndex < 0) && (secondIndex >= 0) )//第1个汉字不在gb2312编码中(gbk中),第2个汉字是gb2312常用汉字 { return 1; } else if ((firstIndex >= 0) && (secondIndex < 0) )//第1个汉字是gb2312常用汉字,第2个汉字不在gb2312编码中(gbk中) { return -1; } else if ( (firstIndex < 0) && (secondIndex < 0) )//2个汉字都不是gb2312常用汉字 { firstIndex = hashGbk(ch1); secondIndex = hashGbk(ch2); if ( firstIndex < secondIndex ) { return -1; } else if ( firstIndex > secondIndex ) { return 1; } } } if ( i < len1 ) { return 1; } else if ( i < len2 ) { return -1; } return 0; } /* 汉字的二分查找 */ int bSearch(const vector<char*>& vec, const char* word) { int low = 0; int high = int(vec.size() – 1); //如果空向量,直接返回-1,-1表示找不到该汉字 while ( low <= high ) { int mid = (low + high) / 2; if ( wordCmp(vec[mid], word) == 0 ) { return mid; } else if ( wordCmp(vec[mid], word) < 0 ) { low = mid + 1; } else if ( wordCmp(vec[mid], word) > 0 ) { high = mid – 1; } } return -1; } /* 特殊二分查找,如果在有序序列中没有找到指定元素,返回该元素应该被插入的位置 */ int specBSearch(const vector<char*>& vec, char* word) { int low = 0; int high = int(vec.size() – 1); //如果空向量,insertPoint返回0,表示插入在首位置 int insertPoint = 0; //如果已在序列中,插入点-1,无需插入;初始化为0,防止要插入的汉字比序列里的汉字都小 while ( low <= high ) { int mid = (low + high) / 2; if ( wordCmp(vec[mid], word) == 0 ) { return -1; } else if ( wordCmp(vec[mid], word) < 0 ) { low = mid + 1; insertPoint = low; } else { high = mid – 1; } } return insertPoint; } /* 有序序列的插入,返回插入的位置 */ int insertToVec(vector<char*>& vec, char* word) { int insertPoint = specBSearch(vec, word); //得到插入点 if ( insertPoint == -1 ) { return -1; } vec.insert(vec.begin() + insertPoint, word); return insertPoint; } /* 增添新字符串 */ trieTree& trieTree::insert(const char* str) { char* ch = new char[3]; //此处用动态存储,是为了长久保留汉字 ch[0] = str[0]; ch[1] = str[1]; ch[2] = ‘/0’; //取得汉字字符串的第一个汉字 if ( mRoot == NULL ) //检索树为空 { mRoot = new node; mRoot->mKeyWordVec.push_back(ch); mRoot->mLinkVec.push_back(NULL); const char* s = str + 2; //除第一个汉字外的子串 if ( *s != ‘/0’ ) //子串不为空(表明还有子节点) { node* n = new node; trieTree child(n); //子检索树 mRoot->mLinkVec[0] = n; //连接到子节点 child.insert(s); //在子检索树递归插入 } else { node* n = new node; n->mKeyWordVec.push_back(“##”); //存储”##”表示一个词的结束 n->mLinkVec.push_back(NULL); mRoot->mLinkVec[0] = n; } } else if ( mRoot != NULL ) { int index = bSearch(mRoot->mKeyWordVec, ch); //在根节点查找第一个汉字 if ( index != -1 ) //第一个汉字已经存在 { const char* s = str + 2; if ( *s != ‘/0’ ) { node* n = mRoot->mLinkVec[index]; trieTree child(n); child.insert(s); } else { node* n = new node; n->mKeyWordVec.push_back(“##”); //存储”##”表示一个词的结束 n->mLinkVec.push_back(NULL); mRoot->mLinkVec[index] = n; } } else { int insertPoint = insertToVec(mRoot->mKeyWordVec, ch); mRoot->mLinkVec.insert(mRoot->mLinkVec.begin() + insertPoint, NULL); const char* s = str + 2; if ( *s != ‘/0’ ) { node* n = new node; trieTree child(n); //子检索树 mRoot->mLinkVec[insertPoint] = n; //连接到子节点 child.insert(s); } else { node* n = new node; n->mKeyWordVec.push_back(“##”); //存储”##”表示一个词的结束 n->mLinkVec.push_back(NULL); mRoot->mLinkVec[insertPoint] = n; } } } return *this; } /* 删除指定字符串 */ trieTree& trieTree::del(const char* str) { if ( mRoot == NULL ) { return *this; } if ( find(str) == false ) //指定字符串在检索树中不存在 { return *this; } //下面统计每个汉字的分支 size_t len = strlen(str); vector<int> countVec(len/2); //记录分支数,0表示只有一条分支,1表示有多条分支 node* p = mRoot; node* q; char ch[3]; for (size_t i = 0; i < len; i += 2) { ch[0] = str[i]; ch[1] = str[i+1]; ch[2] = ‘/0’; int index = bSearch(p->mKeyWordVec, ch); q = p->mLinkVec[index]; if ( q->mKeyWordVec.size() > 1 ) { countVec[i/2] = 1; } else { countVec[i/2] = 0; } p = q; } //寻找最后一个1的位置(这个1说明前面的字组成的前缀对其他词还有用,不能删,从最后那个1的位置(那个字)可以删除 int pos = -1; for (int i = int(countVec.size() -1); i >= 0; –i) { if ( countVec[i] == 1 ) { pos = i; break; } } //删掉单分支(单分支对其他词无影响) p = mRoot; const char* s = str; for (int i = 0; i <= pos; ++i) { ch[0] = *s; ch[1] = *(s+1); ch[2] = ‘/0’; s = s + 2; //向后移动一个汉字 int index = bSearch(p->mKeyWordVec, ch); p = p->mLinkVec[index]; } if ( *s == ‘/0’ ) //当前有多路分支的字是最后一个字,最后一个字有多路分支,只删除该词的结尾符即可 { int index = bSearch(p->mKeyWordVec, “##”); p->mKeyWordVec.erase(p->mKeyWordVec.begin() + index); p->mLinkVec.erase(p->mLinkVec.begin() + index); return *this; } while ( *s != ‘/0’ ) { ch[0] = *s; ch[1] = *(s+1); ch[2] = ‘/0’; int index = bSearch(p->mKeyWordVec, ch); node* q = p->mLinkVec[index]; p->mKeyWordVec.erase(p->mKeyWordVec.begin() + index); p->mLinkVec.erase(p->mLinkVec.begin() + index); p = q; s = s + 2; } if ( *s == ‘/0’ ) { delete p; //删除结束符节点 return *this; } return *this; } /* 在检索树中查找指定字符串 */ bool trieTree::find(const char* str) { char ch[3]; ch[0] = str[0]; ch[1] = str[1]; ch[2] = ‘/0’; //取得汉字字符串的第一个汉字 int index = bSearch(mRoot->mKeyWordVec, ch); if ( index == -1 ) { return false; //第一个汉字没有查找到,直接返回 } else { const char* s = str + 2; //取得除第一个汉字外的剩余字符串 if ( *s != ‘/0’ ) { if ( mRoot->mLinkVec[index] == NULL ) //字符串没有结束,检索树中前缀已经结束,该词不存在 { return false; } trieTree child(mRoot->mLinkVec[index]); //得到子检索树 return child.find(s); //在子检索树递归查找子字符串 } else { node* n = mRoot->mLinkVec[index]; if ( bSearch(n->mKeyWordVec, “##”) != -1 ) { return true; } else { return false; } } } return true; //all path return } /* 在检索树中检索单独汉字,如果存在返回子检索树 */ trieTree trieTree::findCh(const char* ch) { int index = bSearch(mRoot->mKeyWordVec, ch); if ( index == -1 ) { trieTree rst; //空检索树,mRoot == NULL return rst; } trieTree rst(mRoot->mLinkVec[index]); return rst; } int main() { //test trieTree obj; obj.insert(“啊啊”); obj.insert(“阿波罗”); obj.insert(“篮球”); obj.insert(“篮板”); cout << obj.find(“啊”) << endl; cout << obj.find(“篮球”) << endl; cout << obj.find(“篮筐”) << endl; obj.del(“篮球”); cout << obj.find(“篮球”) << endl; return 0; } |