上图表示的字符串集合集合为{abc,abcd,abd,b,bcd,efg,hii},每个单词的结束位置对应一个“单词结点”。反过来,从根节点到每个单词结点的路径上所有字母连接而成的字符串就是该单词结点对应的字符串。在程序上,将根结点编号为0,然后把其余结点编号为从1开始的正整数,然后用一个数组来保存每个结点的所有子节点,用下标直接存取。
具体来说,可以用child[i][j]保存结点i的那个编号为j的子结点的结点编号。编号i和编号j的性质不一样,编号i是指结点编号为i。而j则是指子结点所代表的字符的编号,比如,若是处理全部由小写字母组成的字符串,把所有小写字母按照字典序编号为0,1,2,……,则ch[i][0]表示结点i的子结点'a',如果这个子结点不存在,则ch[i][0] = 0,这不会引起误会,因为任何结点的子结点都不可能是根结点。用SIGMA_SIZE表示字符集的大小,比如,当字符集为全体小写字母时,SIGMA_SIZE = 26。
使用Trie的时候,往往需要在单词结点上附加信息,其中val[i]表示结点i对应的附加信息。例如,如果每个字符串有一个权值,就可以把这个权值保存在val[i]中。简单起见,下面的代码中假定权值大于0,因此,val[i] > 0当且仅当结点i是单词结点。
Trie的定义,查询和插入代码如下:
class Trie {
private:
int child[NODE_SIZE][SIGMA_SIZE];//用之前搞定两个宏
int val[NODE_SIZE];//记录附加信息,无附加信息时可考虑使用bool型
int cnt; //结点总数
public:
//初始时只有一个根结点
Trie () {
cnt = 1;
memset (child[0], 0, sizeof (child[0]));
}
//将trie重置为只有一个根结点
void clear () {
cnt = 1;
memset (child[0], 0, sizeof (child[0]));
}
//返回字符ch的编号
//根据字符集的不同进行调整
int getIndex (char ch) {
return ch -'a';
}
/*
* 查询字符串target是否在trie中,是则返回其附加的信息,否则返回0
* target是string类型
* 对象做实参要加引用,不然每次一调用函数就要复制一个对象,很影响效率
* 无附加信息时若将val换成bool型数组,则要注意函数返回值类型
*/
int query (string &target) {
int parent = 0, index;
for (string::size_type i = 0; i != target.size(); ++i) {
index = getIndex (target[i]);
if (child[parent][index] == 0)
return 0;
parent = child[parent][index];
}
return val[parent];
}
/*
* 插入字符串src,附加信息为v,v必须非0,0代表“本结点不是单词结点”
* src是string类型,插入前不用查询是否存在
* 对象做实参要加引用,不然每次一调用函数就要复制一个对象,很影响效率
* 无附加信息时可考虑去掉参数v,将val换成bool型数组,单词结点赋值为true
*/
void insert (string &src, int v) {
int parent = 0, index;
for (string::size_type i = 0; i != src.size(); ++i) {
index = getIndex (src[i]);
if (child[parent][index] == 0) { //结点不存在
child[parent][index] = cnt; //新建结点
memset (child[cnt], 0, sizeof(child[cnt]));
val[cnt] = 0;//非单词结点的附加信息为0,若val为bool型,改成false
++cnt;
}
parent = child[parent][index]; //往下走
}
val[parent] = v;//单词结点,其附加信息为v,若val为bool型,改成true
}
};