字典树原理模板(数组模拟VS指针)+例题

一、字典树概念:

又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高,但是我们时间空间这两个东西可以相互优化的,什么意思呢,也就是说,你时间复杂度降低了,但是你却增加了内存消耗,也就是所说的空间换时间。

二、字典树的优缺点

优点

  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. 空间消耗大。
给你一些单词inn、int、ate、age、adv、ant,现在你来建立一颗字典树。

二、建立字典树步骤:

1.定义指向根节点的指针P(建树过程实际上就是建立根节点)

2.从字符串开始下标到结束(0-len)我们依次将字符做出节点。方法是:将字母s[i]由字典序化为数字id,当指针P指向的下一个next[id]为空时,将其单词计数初始化为0,从0-maxn初始化P指向的所有节点为NULL(一棵字典树很明显有很多棵子树),同时修改指针P指向next[id],如果要统计前缀,将计数节点++即可,如果统计单词,则是这个单词用来建完树之后计数节点再++,大概就是这样了,具体我们见代码。

下面我们以HDU1521(统计前缀)为例
传送门:http://acm.hdu.edu.cn/showproblem.php?pid=1251
指针代码:(C++提交可以通过,G++MLE,听大佬们说是G++在申请指针内存的同时也会申请一个指针对应类型大小的内存,这样消耗内存可能就是原来的两倍了)

#include<stdio.h>
#include<iostream>
using namespace std ;
const
int
maxn = 26 ;
struct
Trie
{

     int
cnt = 0 ;//计数(前缀或者单词)
     struct
Trie * next [ maxn ];//指向各个子树的指针
     Trie
()//写成构造函数在new时候自动调用
     {

          cnt
= 0 ;
          for
(int
i = 0 ; i < maxn ; i ++) next [ i ]= NULL ;
     }
     ~
Trie (){}//析构函数
};

Trie root
;//根节点
void
Insert ( string s )
{

     Trie
* p =& root ;//指向根节点的指针
     for
(int
i = 0 ; i < s . size (); i ++)
     {

          int
id = s [ i ]- 'a' ;//将字母数字化
          if
(
p -> next [ id ]== NULL )//如果之前没有指针指向这个字符,则为该字符所在节点分配内存
          {

               p
-> next [ id ]=new Trie ;
          }

          p
= p -> next [ id ];修改指针p指向当前字符
          p
-> cnt ++;//统计前缀
     }
//p->cnt++;统计单词
}

int
Search ( string s )
{

     Trie
* p =& root ;
     for
(int
i = 0 ; i < s . size (); i ++)
     {

          int
id = s [ i ]- 'a' ;
          if
(
p -> next [ id ]== NULL ) return 0 ;//如果没有该单词/该单词为前缀的单词,则表示没找到
          p
= p -> next [ id ];//修改指正继续往下找
     }

     return
p -> cnt ;//返回统计单词/前缀的数量
}

int
main()
{

     char
s [ 11 ];
     while
(
gets ( s ))
     {

          if
(
s [ 0 ]== NULL ) break;//gets读入空行之后,自动转换为NULL
          Insert
( s );
     }

     while
(
gets ( s ))
     {

          printf
( "%d\n" , Search ( s ));
     }

     return
0 ;
}

数组代码:

 
  
#include<bits/stdc++.h>
using namespace std;
const
int
maxn=1e6+5;
int
trie[maxn][26];//字符位置
int
num[maxn];//计数
int
pos=1;
void
Insert(string s)
{

     int
p=0;//节点位置
     for
(int
i=0;s[i];i++)
     {

          int
id=s[i]-'a';
          if
(
trie[p][id]==0) trie[p][id]=pos++;//如过没有p到id的前缀,则为节点编号
          p
=trie[p][id];//修改位置
          num
[p]++;//前缀数量
     }
//num[p]++;单词数量++
}

int
Search(string s)
{

     int
p=0;
     for
(int
i=0;s[i];i++)
     {

          int
id=s[i]-'a';
          if
(
trie[p][id]==0) return 0;//没有找到p到id的前缀
          p
=trie[p][id];//继续向下找
     }

     return
num[p];
}

int
main()
{

     char
s[12];
     /*
     这里输入主要是判断空行,因为cin、scanf输入时候检测空格自动停止读取
     也可以用cin.getline(s,12)读入,然后看看s长度是否为0
     */

     while
(
gets(s))
     {

          if
(
s[0]==NULL) break;
          Insert
(s);
     }

     while
(
gets(s)) printf("%d\n",Search(s));
     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)。

  1. 后面部分摘自:https://blog.csdn.net/hihozoo/article/details/51248823

猜你喜欢

转载自blog.csdn.net/dl962454/article/details/80091023
今日推荐