字典树(Trie树)

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

字典树

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

  • 根节点不包含字符,除根节点外的每一个子节点都包含一个字符。
  • 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。一条路径代表一个字符串,路径不通代表无此字符串。
  • 每个节点的所有子节点包含的字符互不相同。

Trie树的关键字一般都是字符串,而且Trie树把每个关键字保存在一条路径上,而不是一个节点中。另外,有两个公共前缀的关键字,在Trie树种前缀部分的路径相同。所以Trie又称为前缀树

字典树的一种C++实现:

#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;
}

void trie_delete(trie root)
{
	int i;
	for(i=0;i<ALPHABET_SIZE;i++)
	{
		if(root->children[i] != NULL)
			trie_delete(root->children[i]);
	}
	delete root;
}


void trie_insert(trie root, string key)
{
	trie_node* node = root;
	for(size_t i=0;i<key.size();++i)
	{
		if(node->children[key[i]-'a'] == NULL)
		{
			node->children[key[i]-'a'] = create_trie_node();
		}
		node = node->children[key[i]-'a'];
	}
	node->count += 1;
}

int trie_search(trie root, string key)
{
	trie_node* node = root;
	
	for(size_t i=0;i<key.size()&&node!=NULL;++i)
	{
		node = node->children[key[i]-'a'];
	}

	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] = {"在字典树中", "不在字典树中"};

    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", "ther", trie_search(root, "ther")>0?s[0]:s[1]);

    trie_delete(root);

    root = create_trie_node();

    // 创建trie树

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

    string strs[3]={"thu","pku","shanghaijiaotonguniversity"};
    for(int i = 0; i < 3; i++)
        trie_insert(root, strs[i]);

    string str;
    while (cin>>str)
    {
        printf("%s --- %s\n", str.c_str(), trie_search(root,str)>0?s[0]:s[1]);
    }

    cin.get();

    return 0;
}

优点

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

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

  3. Trie树中只有在允许一个关键字关联多个值的情况下才有类似hash碰撞发生。

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

  5. Trie树可以对关键字 按照字典序排序 (先序遍历)。

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

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

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

缺点

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

  2. 空间消耗大。

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)。

猜你喜欢

转载自blog.csdn.net/jirryzhang/article/details/81153651