Trie—单词查找树
Trie,又称单词查找树、前缀树,是一种哈希树的变种。应用于字符串的统计与排序,经常被搜索引擎系统用于文本词频统计。
性质:
1.根节点不包含字符,除根节点外的每一个节点都只包含一个字符。
2.从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3.每个节点的所有子节点包含的字符都不相同。
优点:
1.查询快。对于长度为m的键值,最坏情况下只需花费O(m)的时间;而BST需要O(m log n)的时间。
2.当存储大量字符串时,Trie耗费的空间较少。因为键值并非显式存储的,而是与其他键值共享子串。
2 结构示意图
操作:
1.初始化或清空:遍历Trie,删除所有节点,只保留根节点。
2.插入字符串
1).设置当前节点为根节点,设置当前字符为插入字符串中的首个字符;
2).在当前节点的子节点上搜索当前字符,若存在,则将当前节点设为值为当前字符的子节点;否则新建一个值为当前字符的子节点,并将当前结点设置为新创建的节点。
3).将当前字符设置为串中的下个字符,若当前字符为0,则结束;否则转2.
3.查找字符串
搜索过程与插入操作类似,当字符找不到匹配时返回假;若全部字符都存在匹配,判断最终停留的节点是否为树叶,若是,则返回真,否则返回假。
4.删除字符串
首先查找该字符串,边查询边将经过的节点压栈,若找不到,则返回假;否则依次判断栈顶节点是否为树叶,若是则删除该节点,否则返回真。
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
#include <iostream>
#include<stdlib.h> #include<stdio.h> #include<string.h> using namespace std; /* trie的节点类型 */ template < int Size> //Size为字符表的大小 struct trie_node { bool terminable; //当前节点是否可以作为字符串的结尾 int node; //子节点的个数 trie_node *child[Size]; //指向子节点指针 /* 构造函数 */ trie_node() : terminable( false), node( 0) { memset(child, 0, sizeof(child)); } }; /* trie */ template < int Size, typename Index> //Size为字符表的大小,Index为字符表的哈希函数 class trie { public: /* 定义类型别名 */ typedef trie_node<Size> node_type; typedef trie_node<Size> *link_type; /* 构造函数 */ trie(Index i = Index()) : index(i) { } /* 析构函数 */ ~trie() { clear(); } /* 清空 */ void clear() { clear_node(root); for ( int i = 0; i < Size; ++i) root.child[i] = 0; } /* 插入字符串 */ template < typename Iterator> void insert(Iterator begin, Iterator end) { link_type cur = &root; //当前节点设置为根节点 for (; begin != end; ++begin) { if (!cur->child[index[*begin]]) //若当前字符找不到匹配,则新建节点 { cur->child[index[*begin]] = new node_type; ++cur->node; //当前节点的子节点数加一 } cur = cur->child[index[*begin]]; //将当前节点设置为当前字符对应的子节点 } cur->terminable = true; //设置存放最后一个字符的节点的可终止标志为真 } /* 插入字符串,针对C风格字符串的重载版本 */ void insert( const char *str) { insert(str, str + strlen(str)); } /* 查找字符串,算法和插入类似 */ template < typename Iterator> bool find(Iterator begin, Iterator end) { link_type cur = &root; for (; begin != end; ++begin) { if (!cur->child[index[*begin]]) return false; cur = cur->child[index[*begin]]; } return cur->terminable; } /* 查找字符串,针对C风格字符串的重载版本 */ bool find( const char *str) { return find(str, str + strlen(str)); } /* 删除字符串 */ template < typename Iterator> bool erase(Iterator begin, Iterator end) { bool result; //用于存放搜索结果 erase_node(begin, end, root, result); return result; } /* 删除字符串,针对C风格字符串的重载版本 */ bool erase( char *str) { return erase(str, str + strlen(str)); } /* 按字典序遍历单词树 */ template < typename Functor> void traverse(Functor &execute = Functor()) { visit_node(root, execute); } private: /* 访问某结点及其子结点 */ template < typename Functor> void visit_node(node_type cur, Functor &execute) { execute(cur); for ( int i = 0; i < Size; ++i) { if (cur.child[i] == 0) continue; visit_node(*cur.child[i], execute); } } /* 清除某个节点的所有子节点 */ void clear_node(node_type cur) { for ( int i = 0; i < Size; ++i) { if (cur.child[i] == 0) continue; clear_node(*cur.child[i]); delete cur.child[i]; cur.child[i] = 0; if (--cur.node == 0) break; } } /* 边搜索边删除冗余节点,返回值用于向其父节点声明是否该删除该节点 */ template < typename Iterator> bool erase_node(Iterator begin, Iterator end, node_type &cur, bool &result) { if (begin == end) //当到达字符串结尾:递归的终止条件 { result = cur.terminable; //如果当前节点可以作为终止字符,那么结果为真 cur.terminable = false; //设置该节点为不可作为终止字符,即删除该字符串 return cur.node == 0; //若该节点为树叶,那么通知其父节点删除它 } //当无法匹配当前字符时,将结果设为假并返回假,即通知其父节点不要删除它 if (cur.child[index[*begin]] == 0) return result = false; //判断是否应该删除该子节点 else if (erase_node((++begin)--, end, *(cur.child[index[*begin]]), result)) { delete cur.child[index[*begin]]; //删除该子节点 cur.child[index[*begin]] = 0; //子节点数减一 //若当前节点为树叶,那么通知其父节点删除它 if (--cur.node == 0 && cur.terminable == false) return true; } return false; //其他情况都返回假 } /* 根节点 */ node_type root; /* 将字符转换为索引的转换表或函数对象 */ Index index; }; //index function object class IndexClass { public: int operator[]( const char key) { return key % 26; } }; int main() { trie< 26, IndexClass> t; t.insert( "tree"); t.insert( "tea"); t.insert( "A"); t.insert( "ABC"); if(t.find( "tree")) cout << "find tree" << endl; else cout << "not find tree" << endl; if(t.find( "tre")) cout << "find tre" << endl; else cout << "not find tre" << endl; if(t.erase( "tree")) cout << "delete tree" << endl; else cout << "not find tree" << endl; if(t.find( "tree")) cout << "find tree" << endl; else cout << "not find tree" << endl; return 0; } |
3. trie树的应用:
1. 字符串检索,词频统计,搜索引擎的热门查询
事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。
举例:
1)有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
2)给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
3)给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。
4)1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串
5)寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
2. 字符串最长公共前缀
Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。举例:
1) 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少. 解决方案:
首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线 (Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。
而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:
1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;
2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;
3. 排序
Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。
举例: 给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。
4 作为其他数据结构和算法的辅助结构
如后缀树,AC自动机等。