字典树Trie

1、字典树

字典树——Trie树,又称为前缀树(Prefix Tree)、单词查找树或键树,是一种多叉树结构。html

字典树

上图是一棵 Trie 树,表示了关键字集合{“a”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”} 。从上图能够概括出Trie树的基本性质:node

  • 根节点不包含字符,除根节点外的每个子节点都包含一个字符。
  • 从根节点到某一个节点,路径上通过的字符链接起来,为该节点对应的字符串。
  • 每一个节点的全部子节点包含的字符互不相同。

一般在实现的时候,会在结点结构中设置一个标志,用来标记该节点处是否构成一个单词(关键字 : count)。ios

struct trie_node{
    int count; // 0 : 不构成一个单词 , > 0 : 构成一个单词
    struct trie_node * children[26]; // 如上图,'to' 中 o 对应节点的 children 数组的值全为NULL
};

能够看出,Trie树的关键字通常都是字符串,并且Trie树把每一个关键字保存在一条路径上,而不是一个节点中。另外,有两个公共前缀的关键字,在Trie树种前缀部分的路径相同。因此Trie又称为前缀树web

2、字典树的优缺点

优势

  1. 插入和查询的效率很高,均是O(m),其中 m 是待插入/查询的 字符串长度算法

    • 关于查询,有人会说hash表时间复杂度是O(1)不是更快?可是哈希搜索的效率取决于哈希函数的好坏,若一个坏的hash函数致使了不少冲突,效率不必定比Trie树高。
  2. Trie树中不一样的关键字不会产生冲突。数组

  3. Trie树中只有在容许一个关键字关联多个值的状况下才有相似hash碰撞发生。数据结构

  4. Trie树不用求hash值,对短字符串有更快的速度。一般,求hash值也是须要遍历字符串的(与hash函数相关)svg

  5. Trie树能够对关键字 按照字典序排序 (先序遍历)。函数

    • 字典排序(lexicographical order)是一种对于随机变量造成序列的排序方法。其方法是,按照字母顺序,或者数字小大顺序,由小到大的造成序列。
  6. 每一颗Trie树均可以被看作一个简单版的肯定有限状态的自动机(DFA,deterministic finite automation),也就是说,对于一个任意给定属于该自动机的状态(①)和一个属于该自动机字母表的字符(②),均可以根据给定的转移函数(③)转到下一个状态。其中:性能

    • ① 对于Trie树的每个节点都肯定一个自动机的状态。
    • ② 给定一个属于该自动机字母表的字符,在图中能够看到根据不一样字符造成的分支;
    • ③ 从当前节点进入下一层次节点的过程进过状态转移函数得出。

    核心思想是:空间换时间,利用字符串的公共前缀来减小无谓的字符串比较以达到提升查询效率的目的。

缺点

  1. 当hash函数很好时,Trie树的查找效率低于哈希搜索。

  2. 空间消耗大。

3、Trie树的应用

  1. 字符串检索

    检索、查询功能是Trie树最原始功能,思路就是从根节点开始一个一个字符进行比较。

    • 若是沿路比较,发现不一样的字符,则表示该字符串在集合中不存在。

    • 若是全部的字符所有比较而且彻底相同,还须要判断最后一个节点标识位(标记该节点是否为一个关键字)。

  2. 词频统计
    Trie树常被搜索引擎用于文本词频统计。

    思路:为了实现词频统计,咱们修改了节点结构,用一个整型变量count来计数。对每个关键字执行插入操做,若已存在,计数加1,若不存在,插入后count置 1。
    (1. 2. 均可以用hash table作)

  3. 字符串排序
    Trie树能够对大量字符串按字典序进行排序,思路也很简单:遍历一次全部关键字,将它们所有插入trie树,树的每一个结点的全部儿子很显然地按照字母表排序,而后先序遍历输出Trie树中全部关键字便可。

  4. 前缀匹配
    例如:找出一个字符串集合中全部以ab开头的字符串。咱们只须要用全部字符串构造一个trie树,而后输出以a->b->开头的路径上的关键字便可。 trie树前缀匹配经常使用于搜索提示。如当输入一个网址,能够自动搜索出可能的选择。当没有彻底匹配的搜索结果,能够返回前缀最类似的可能。

  5. 做为辅助结构
    如后缀树,AC自动机
    有穷自动机 参考资料:http://blog.csdn.net/yukuninfoaxiom/article/details/6057736

  6. 与哈希表相比
    优势:

    • trie数据查找与不完美哈希表(链表实现)在最坏状况下更快;对于trie树,最差为O(m),m为查找字符串的长度;对于不完美哈希表,会有键值冲突(不一样键哈希相同),最坏为O(N),N为所有字符产生的个数。典型状况是O(m)用于哈希计算,O(1)用于数据查找。

    • trie中不一样键没有冲突

    • trie的桶与哈希表用于存储键冲突的桶相似,仅在单个键与多个值关联时须要

    • 当更多的键加入到trie中,无需提供hash方法或改变hash方法

    • trie经过键为条目提供字母顺序
      缺点:

    • trie数据查找在某些状况下(磁盘或随机访问时间远远高于主存)比哈希表慢

    • 当键值为某些类型(如浮点型),前缀链很长且前缀不是特别有意义。

    • 一些trie会比hash表更消耗内存。对于trie,每一个字符串的每一个字符都要分配内存;对于大多数hash,只须要为整个条目分配一块内存。

  7. 与二叉搜索树相比

    二叉搜索树,又称二叉排序树,它知足:

    • 任意节点若是左子树不为空,左子树全部节点的值都小于根节点的值;

    • 任意节点若是右子树不为空,右子树全部节点的值都大于根节点的值;

    • 左右子树也都是二叉搜索树;

    • 全部节点的值都不相同。

    其实二叉搜索树的优点已经在与查找、插入的时间复杂度上了,一般只有O(log n),不少集合都是经过它来实现的。在进行插入的时候,实质上是给树添加新的叶子节点,避免了节点移动,搜索、插入和删除的复杂度等于树的高度,属于O(log n),最坏状况下整棵树全部的节点都只有一个子节点,彻底变成一个线性表,复杂度是O(n)。

    Trie树在最坏状况下查找要快过二叉搜索树,若是搜索字符串长度用m来表示的话,它只有O(m),一般状况(树的节点个数要远大于搜索字符串的长度)下要远小于O(n)。

4、实现

#include <iostream>

#include <string>

using namespace std;

#define ALPHABET_SIZE 26

typedef struct trie_node
{
    int count;   // 记录该节点表明的单词的个数
    trie_node *children[ALPHABET_SIZE]; // 各个子节点 
}*trie;

trie_node* create_trie_node()
{
    trie_node* pNode = new trie_node();
    pNode->count = 0;
    for(int i=0; i<ALPHABET_SIZE; ++i)
        pNode->children[i] = NULL;
    return pNode;
}

void trie_insert(trie root, char* key)
{
    trie_node* node = root;
    char* p = key;
    while(*p)
    {
        if(node->children[*p-'a'] == NULL)
        {
            node->children[*p-'a'] = create_trie_node();
        }
        node = node->children[*p-'a'];
        ++p;
    }
    node->count += 1;
}

/** * 查询:不存在返回0,存在返回出现的次数 */
int trie_search(trie root, char* key)
{
    trie_node* node = root;
    char* p = key;
    while(*p && node!=NULL)
    {
        node = node->children[*p-'a'];
        ++p;
    }

    if(node == NULL)
        return 0;
    else
        return node->count;
}

int main()
{
    // 关键字集合

    char keys[][8] = {"the", "a", "there", "answer", "any", "by", "bye", "their"};

    trie root = create_trie_node();

    // 建立trie树

    for(int i = 0; i < 8; i++)
        trie_insert(root, keys[i]);

    // 检索字符串

    char s[][32] = {"Present in trie", "Not present in trie"};

    printf("%s --- %s\n", "the", trie_search(root, "the")>0?s[0]:s[1]);
    printf("%s --- %s\n", "these", trie_search(root, "these")>0?s[0]:s[1]);
    printf("%s --- %s\n", "their", trie_search(root, "their")>0?s[0]:s[1]);
    printf("%s --- %s\n", "thaw", trie_search(root, "thaw")>0?s[0]:s[1]);

    return 0;
}

对于Trie树,咱们通常只实现插入和搜索操做。这段代码能够用来检索单词和统计词频。

5、Trie树改进

  1. 按位树(Btiwise Trie):原理上和普通Trie树差很少,只不过普通Trie树存储的最小单位是字符,可是Bitwise Trie存放的是位而已。位数据的存取由CPU指令一次直接实现,对于二进制数据,它理论上要比普通Trie树快。

  2. 节点压缩

    • ①分支压缩: 对于稳定的Trie树,基本上都是查找和读取的操做,彻底能够把一些分支进行压缩。例如,下图中最右侧分支inn能够直接压缩成一个节点“inn”,而不须要做为一个常规子树存在。Radix树就是根据这个原理来解决Trie树过深的问题。
      分支压缩

    • ②节点映射表:这种方式也是Trie树节点可能几乎彻底肯定下采用的,针对Trie树节点的每个状态,若是状态总数重复不少的话,经过一个元素为数字的多维数组(好比Triple Array Trie)来表示,这样存储Trie树自己的空间开销会小一些,虽然引入了额外的映射表。

  3. 双数组TRIE树(Double Array Trie)

    它在保证Trie树检索速度的前提下,提升空间利用率而提出的一种数据结构,本质上仍是一个肯定有限自动机。(所谓DFA就是一个可以实现状态专注的自动机,对于一个给定的属于该自动机的状态和一个数据该自动机字符表Σ的字符,它可以根据预先给定的状态转移函数转移到下一个状态。)

    对于DAT来讲,每一个节点表明自动机的一个状态, 根据变量的不一样,进行状态转移,当达到结束状态或者没法转移时,完成查询。

    参考资料:http://blog.csdn.net/zzran/article/details/8462002

6、Trie树的其余形式

Trie树变形

上图主要说明下这些算法数据结构之间的关系。图中黄色部分主要写明了这些算法和数据结构的一些关键点。

图中能够看到这样一些关系:extend-kmp 是kmp的扩展;ac自动机是kmp的多串形式;它是一个有限自动机;而trie图其实是一个肯定性有限自动机;ac自动机,trie图,后缀树实际上都是一种trie;后缀数组和后缀树都是与字符串的后缀集合有关的数据结构;trie图中的后缀指针和后缀树中的后缀连接这两个概念及其一致。

7、Trie树的性能比较

性能比较

参考博客 http://www.hankcs.com/nlp/performance-comparison-of-several-trie-tree.html

参考资料

  1. Trie树 http://www.raychase.net/1783?replytocom=264917

  2. Trie树 http://blog.csdn.net/v_july_v/article/details/6897097

  3. BitWise Trie http://blog.csdn.net/breeze_gao/article/details/8461856

  4. AC自动机 http://www.cppblog.com/menjitianya/archive/2014/07/10/207604.html