概念
如果现在有b,abc,abd,bcd,abcd,efg,hii 这7个单词,我们可以构建一棵如下图所示的树:
如上图所示,对于每一个节点,从根遍历到他的过程就是一个单词,如果这个节点被标记为红色,就表示这个单词存在,否则不存在。
Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树。
与二叉查找树不同,Trie树的键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
基本性质:
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
复杂度
Trie树优点是最大限度地减少无谓的字符串比较,查询效率比较高。核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
- 插入、查找的时间复杂度均为O(N),其中N为字符串长度。
- 空间复杂度是26^n级别的,非常庞大(可采用双数组实现改善)。
Trie树的实现
trie树实际上是一个DFA,通常用转移矩阵表示。行表示状态,列表示输入字符,(行,列)位置表示转移状态。这种方式的查询效率很高,但由于稀疏的现象严重,空间利用效率很低。也可以采用压缩的存储方式即链表来表示状态转移,但由于要线性查询,会造成效率低下。
Trie树的创建要考虑的是父节点如何保存孩子节点,主要有链表和数组两种方式:
- 使用节点数组,因为是英文字符,可以用Node[26]来保存孩子节点(如果是数字我们可以用Node[10]),这种方式最快,但是并不是所有节点都会有很多孩子,所以这种方式浪费的空间太多
- 用一个链表根据需要动态添加节点。这样我们就可以省下不小的空间,但是缺点是搜索的时候需要遍历这个链表,增加了时间复杂度。
应用场景
- 字符串检索
事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。
例如:给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词 - 字符串最长公共前缀
Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。 - 排序
Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。
也可以快速获得最小、最大字符串(举出数据地图分页查看分区、求最大最小分区的例子) - 词频统计
统计每个单词出现的次数,以及找到出现频率最高的n个单词 - 字符串搜索的前缀匹配
trie树常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。 - 作为其他数据结构和算法的辅助结构
如后缀树,AC自动机等
数据结构的比较
二叉搜索树(binary search tree)
二叉搜索树又叫做二叉排序树,它满足:
- 任意节点如果左子树不为空,左子树所有节点的值都小于根节点的值;
- 任意节点如果右子树不为空,右子树所有节点的值都大于根节点的值;
- 左右子树也都是二叉搜索树;
- 所有节点的值都不相同。
其实二叉搜索树的优势已经在与查找、插入的时间复杂度上了,通常只有 O(log n),很多集合都是通过它来实现的。在进行插入的时候,实质上是给树添加新的叶子节点,避免了节点移动,搜索、插入和删除的复杂度等于树的高度,属于 O(log n),最坏情况下整棵树所有的节点都只有一个子节点,完全变成一个线性表,复杂度是 O(n)。
Trie 树在最坏情况下查找要快过二叉搜索树,如果搜索字符串长度用 m 来表示的话,它只有 O(m),通常情况(树的节点个数要远大于搜索字符串的长度)下要远小于 O(n)。
我们给 Trie 树举例子都是拿字符串举例的,其实它本身对 key 的适宜性是有严格要求的,如果 key 是浮点数的话,就可能导致整个 Trie 树巨长无比,节点可读性也非常差,这种情况下是不适宜用 Trie 树来保存数据的;而二叉搜索树就不存在这个问题。
Hash表
Trie 树可以比较方便地按照 key 的字母序来排序(整棵树先序遍历一次就好了),这是绝大多数 Hash 表是不同的(Hash 表一般对于不同的 key 来说是无序的)。
考虑一下 Hash 表键冲突的问题。Hash 表通常我们说它的复杂度是 O(1),其实严格说起来这是接近完美的 Hash 表的复杂度,另外还需要考虑到 hash 函数本身需要遍历搜索字符串,复杂度是 O(m)。在不同键被映射到 “同一个位置”(考虑 closed hashing,这 “同一个位置” 可以由一个普通链表来取代)的时候,需要进行查找的复杂度取决于这 “同一个位置” 下节点的数目,因此,在最坏情况下,Hash 表也是可以成为一张单向链表的。
在较理想的情况下,Hash 表可以以 O(1) 的速度迅速命中目标,如果这张表非常大,需要放到磁盘上的话,Hash 表的查找访问在理想情况下只需要一次即可;但是 Trie 树访问磁盘的数目需要等于节点深度。
很多时候 Trie 树比 Hash 表需要更多的空间,我们考虑这种一个节点存放一个字符的情况的话,在保存一个字符串的时候,没有办法把它保存成一个单独的块。Trie 树的节点压缩可以明显缓解这个问题,后面会讲到。
参考链接
字典树(Trie树)的实现及应用
Trie(前缀树/字典树)及其应用
Trie 树和其它数据结构的比较
Lucene数字类型处理