Trie树(字典树,单词查找树)

本文深入解析Trie树(字典树)的原理及应用,通过实例展示如何使用Trie树解决单词查询问题,包括插入、查询操作,以及如何优化时间复杂度。

转载:https://blog.youkuaiyun.com/forever_dreams/article/details/81009580

Trie树(字典树,单词查找树)

【例题引入】

题目传送门于是他错误的点名开始了

题目大意:给出n个单词,有m个询问,每次给出一个单词,如果这个单词出现过且是第一次出现,输出“OK”,如果这个单词没有出现过,输出“WRONG”,如果这个单词出现过但不是第一次出现,输出“REPEAT”,其中n≤10000,m≤100000,每个单词长度l≤50(洛谷 P2580)

对于这道题,暴力是很好做的,每次询问都枚举一下n个单词,再判断一下是否符合题意

但是这样做的时间复杂度是O(n*m*l),很明显会超时

这个时候我们就要用到Trie树了,Trie树每次插入和查询的复杂度都为O(l),总复杂度为O((n+m)*l)

 

【介绍】

Trie树是一种树形结构,是一种哈希树的变种。典型应用是用于统计排序保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

(这是从百度上找来的,本蒟蒻连哈希树是什么都不知道)

 

【基本思想】

那么首先,Trie树长什么样子呢?

上图就是由单词at,bee,ben,bt,q组成的Trie树

很容易可以看出,每个字母的父亲节点就是它的前一个字母

Trie树的三个性质:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
  3. 每个节点的所有子节点包含的字符都不相同

这样看来,对于一个长为l的单词,无论是插入还是查询都是O(l)的时间复杂度

我习惯于用结构体来存储Trie树:


 
  1. struct Trie
  2. {
  3. int son[ 26]; //son[i]记录的当前节点的子节点
  4. int num; //num是当前这个单词在查询中出现的次数(题目要求)
  5. }a[ 1000005]; //其实也不用开到这么大,但我一般为了保险都喜欢开大一点

那么,接下来就是介绍如何插入查询

 

插入:

插入操作就是将单词的每个字母都逐一插入Trie树,插入前看这个字母对应的节点是否存在,若不存在就新建一个节点,否则就共享那一个节点,还是以下图为例:

假如说我们要在原Trie树中新插入一个单词and,那我们的操作为:

  1. 插入第一个字母a,发现根节点存在子节点a,则共享节点a
  2. 插入第二个字母n,发现节点a不存在子节点n,则新建子节点n
  3. 插入第三个字母d,发现节点n不存在子节点d,则新建子节点d

代码如下:


 
  1. char x[ 15]; //x是当前的单词
  2. int t= 0; //t是节点的编号
  3. void build_trie()
  4. {
  5. int i,l,p= 0; //p是当前字母的编号
  6. l= strlen(x);
  7. for(i= 0;i<l;++i)
  8. {
  9. if(a[p].son[x[i]- 'a']== 0) //如果这个子节点不存在
  10. a[p].son[x[i]- 'a']=++t; //新建一个子节点
  11. p=a[p].son[x[i]- 'a']; //插入下一个字母
  12. }
  13. }

 

查询:

查询操作和插入操作其实差不多,就是在Trie树中找这个单词的每个字母,若找到了就继续找下去,若没有找到就可以直接退出了,因为若没找到就说明没有这个单词,还还还是以下图为例:

假如说我们要在原Trie树上查询单词and是否存在,那我们的操作为:

  1. 查询第一个字母a,发现根节点存在子节点a,则继续查询n
  2. 查询第二个字母n,发现节点a不存在子节点n,则直接退出并返回0

代码如下:


 
  1. char x[ 15]; //x是当前的单词
  2. int get_answer()
  3. {
  4. int i,l,p= 0; //p是当前字母的编号
  5. l= strlen(x);
  6. for(i= 0;i<l;++i)
  7. {
  8. if(a[p].son[x[i]- 'a']== 0) //如果这个子节点不存在
  9. return 0; //直接退出并返回0
  10. p=a[p].son[x[i]- 'a']; //查询下一个字母
  11. }
  12. a[p].num++; //这个单词的查询次数加一(题目要求)
  13. return a[p].num; //返回它的查询次数
  14. }

 

【复杂度分析】

Trie树其实是一种用空间换时间的算法,前面也提到过,它占用的空间一般很大,但时间是非常高效的,插入和查询的时间复杂度都是O(l)的,总体来说还是很优秀的

 

【代码】

下面是例题的完整代码(洛谷 P2580):


 
  1. #include<cstdio>
  2. #include<cstring>
  3. #include<algorithm>
  4. using namespace std;
  5. struct Trie
  6. {
  7. int son[ 26];
  8. int num;
  9. }a[ 1000005];
  10. int t= 0;
  11. char x[ 15];
  12. void build_trie()
  13. {
  14. int i,l,p= 0;
  15. l= strlen(x);
  16. for(i= 0;i<l;++i)
  17. {
  18. if(a[p].son[x[i]- 'a']== 0)
  19. a[p].son[x[i]- 'a']=++t;
  20. p=a[p].son[x[i]- 'a'];
  21. }
  22. }
  23. int get_answer()
  24. {
  25. int i,l,p= 0;
  26. l= strlen(x);
  27. for(i= 0;i<l;++i)
  28. {
  29. if(a[p].son[x[i]- 'a']== 0)
  30. return 0;
  31. p=a[p].son[x[i]- 'a'];
  32. }
  33. a[p].num++;
  34. return a[p].num;
  35. }
  36. int main()
  37. {
  38. int n,m,i,ans;
  39. scanf( "%d",&n);
  40. for(i= 1;i<=n;++i)
  41. {
  42. scanf( "%s",x);
  43. build_trie();
  44. }
  45. scanf( "%d",&m);
  46. for(i= 1;i<=m;++i)
  47. {
  48. scanf( "%s",x);
  49. ans=get_answer();
  50. if(ans== 1) printf( "OK\n");
  51. if(ans== 0) printf( "WRONG\n");
  52. if(ans>= 2) printf( "REPEAT\n");
  53. }
  54. return 0;
  55. }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值