原贴:http://hi.baidu.com/cuifenghui/blog/item/d66ff3360198db350b55a964.html
这几天在研究中文分词,目前已经研究试验了基于词典的常用中文分词算法,包括正向最大匹配、逆向最大匹配、整词二分法、基于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;
}
相关推荐
对于给定的字符串α1, α2, …, αn,在Trie树中搜索最多只需要经过n次匹配即可完成一次查找,这使得它成为中文匹配分词算法中词典的一种常见实现方式。然而,Trie树的空间利用率通常较低。 双数组Trie树是对Trie树...
本项目“C#实现的中文分词程序”提供了一种基于键树(Trie树)的解决方案,为开发者提供了在C#环境中进行中文分词的工具。 首先,我们需要理解什么是中文分词。中文分词是将连续的汉字序列切分成具有实际意义的词语...
在Java程序中,通常会通过I/O操作读取这些词典文件,并根据其内容构建数据结构,如哈希表或者Trie树,以优化查询效率。 描述中提到的“导入词典”功能存在问题,这可能意味着程序在加载词典时遇到困难,可能是由于...
KMP算法和Trie搜索树是两种在计算机科学中用于字符串处理的重要算法,它们在文本检索、模式匹配和数据结构优化等领域有着广泛的应用。 首先,我们来深入了解一下KMP算法。KMP(Knuth-Morris-Pratt)算法是一种高效...
一个简单的C语言程序:用Trie树实现词频统计和单词查询
这段代码实现了将字符串插入到Trie树中。对于每个字符,都检查其对应的子节点是否存在,不存在则创建新节点,并递归地向下进行,直到到达字符串末尾。 #### 四、删除操作 删除操作相对复杂,需要考虑两种情况:一是...
通过这些优化,他们实现了基于双数组Trie树的中文分词系统,并与其他分词方法进行了比较。结果显示,优化后的双数组Trie树在插入速度、空间利用率和分词查询效率上都有显著提升。 中文信息处理中的分词是一个关键...
trie.c中定义了trie树的操作函数; trie.h为相应的头文件; test.c用于测试相关的函数。 在trie.c中,关于查找定义了两个函数,一个是find(),一个是search(),二者的区别是,前者仅判断一个字符串是否在树中出现,...
在英文分词中,Trie树的应用尤为显著,它能够帮助我们快速地查找、插入和删除单词,同时避免了在长字符串列表中进行线性搜索的低效率。** ### Trie树的基本概念 1. **节点与链接**:Trie树由一系列节点组成,每个...
用Python实现Trie树的应用,并可以对英汉词典进行导入和检索、添加和删除,最终可以将导入的英汉词典保存到本地磁盘。内附两个.py文件,分别是tree.py和d_gui.py,tree.py是类和方法,d_gui.py是图形界面;一个.txt...
在IT领域,中文分词是自然语言处理(NLP)中的关键步骤,它涉及到将连续的汉字序列分割成有意义的词汇单元,以便计算机能够理解和处理中文文本。本项目是使用C++编程语言实现的一个中文分词系统,采用了正向最大匹配...
Trie树是搜索树的一种,来自英文单词"Retrieval"的简写,可以建立有效的数据检索组织结构,是中文匹配分词算法中词典的一种常见实现。它本质上是一个确定的有限状态自动机(DFA),每个节点代表自动机的一个状态。在...
【作品名称】:基于C++实现中文敏感词过滤,支持特殊符号,半角全角,停顿词,重复词等等,基于trie树算法实现 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、...
Trie树,又称字典树或前缀树,是一种用于快速检索的多叉树结构,广泛应用于字符串处理领域。它能有效地利用字符串的公共前缀来减少存储空间,并在查询、插入和删除等方面具有较高的效率。 #### Trie树基本特性 - *...
Trie是一种树型数据结构,用于存储字符串,可以实现字符串的快速查找。Trie的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。 适用范围:统计和排序大量的字符串
Trie树,也被称为前缀树或字典树,是一种数据结构,主要用于高效地存储和检索字符串。在Trie树中,每个节点代表一个字符串的前缀,而从根节点到某个节点的路径上的边代表了这个节点所代表的字符串。这种结构特别适合...
在IT领域,中文分词是自然语言处理(NLP)中的关键步骤,它涉及到将连续的汉字序列分割成有意义的词汇单元,这对于信息检索、文本分析、机器翻译等多个应用至关重要。"C语言中文分词源代码"是一个专门用于实现这一...
在实现Trie树时,我们可以使用结构体来表示每个节点,结构体中包含该节点的字符、该节点的子节点数组、该节点的终结标志等信息。这样可以方便地插入、删除和查找操作。 在实际应用中,Trie树有很多的应用场景,例如...