转:trie树--详解

时间:2022-10-25 07:25:33

前几天学习了并查集和trie树,这里总结一下trie。

本文讨论一棵最简单的trie树,基于英文26个字母组成的字符串,讨论插入字符串、判断前缀是否存在、查找字符串等基本操作;至于trie树的删除单个节点实在是少见,故在此不做详解。

  • Trie原理

Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。

  • Trie性质

好多人说trie的根节点不包含任何字符信息,我所习惯的trie根节点却是包含信息的,而且认为这样也方便,下面说一下它的性质 (基于本文所讨论的简单trie树)

1.    字符的种数决定每个节点的出度,即branch数组(空间换时间思想)

2.    branch数组的下标代表字符相对于a的相对位置

3.    采用标记的方法确定是否为字符串。

4.    插入、查找的复杂度均为O(len),len为字符串长度

  • Trie的示意图

如图所示,该trie树存有abc、d、da、dda四个字符串,如果是字符串会在节点的尾部进行标记。没有后续字符的branch分支指向NULL

转:trie树--详解

  • Trie的优点举例

已知n个由小写字母构成的平均长度为10的单词,判断其中是否存在某个串为另一个串的前缀子串。下面对比3种方法:

1.    最容易想到的:即从字符串集中从头往后搜,看每个字符串是否为字符串集中某个字符串的前缀,复杂度为O(n^2)。

2.    使用hash:我们用hash存下所有字符串的所有的前缀子串。建立存有子串hash的复杂度为O(n*len)。查询的复杂度为O(n)* O(1)= O(n)。

3.    使用trie:因为当查询如字符串abc是否为某个字符串的前缀时,显然以b,c,d....等不是以a开头的字符串就不用查找了。所以建立trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以成为查询的过程,hash就不能实现这个功能。所以总的复杂度为O(n*len),实际查询的复杂度只是O(len)。

解释一下hash为什么不能将建立与查询同时执行,例如有串:911,911456输入,如果要同时执行建立与查询,过程就是查询911,没有,然后存入9、91、911,查询911456,没有然后存入9114、91145、911456,而程序没有记忆功能,并不知道911在输入数据中出现过。所以用hash必须先存入所有子串,然后for循环查询。

而trie树便可以,存入911后,已经记录911为出现的字符串,在存入911456的过程中就能发现而输出答案;倒过来亦可以,先存入911456,在存入911时,当指针指向最后一个1时,程序会发现这个1已经存在,说明911必定是某个字符串的前缀,该思想是我在做pku上的3630中发现的,详见本文配套的“入门练习”。

PKU 3630

Description

Given a list of phone numbers, determine if it is consistent in the sense that no number is the prefix of another. Let's say the phone catalogue listed these numbers:

  • Emergency 911
  • Alice 97 625 999
  • Bob 91 12 54 26

In this case, it's not possible to call Bob, because the central would direct your call to the emergency line as soon as you had dialled the first three digits of Bob's phone number. So this list would not be consistent.

Input

The first line of input gives a single integer, 1 ≤ t ≤ 40, the number of test cases. Each test case starts with n, the number of phone numbers, on a separate line, 1 ≤ n ≤ 10000. Then follows n lines with one unique phone number on each line. A phone number is a sequence of at most ten digits.

Output

For each test case, output "YES" if the list is consistent, or "NO" otherwise.

Sample Input
2
3
911
97625999
91125426
5
113
12340
123440
12345
98346

Sample Output
NO
YES

方法一:trie树

有了上面学习的思考与总结,3630用trie树本以为可以水过,可是学习和做题终究是两回事,我很快写出trie树,然后提交,超时了。

后来受discuss提示,我大致计算了一下本题trie树的复杂度,号码个数10000,长度10,树的宽度大概有10000,所以总的节点数大概就有100,000级,即要进行十万次new的操作,确实时间耗费很多,估计这样题目的用时要有1秒到2秒左右的样子。

于是为了纯粹的做题,我将new操作省掉,改为提前申请一个buffer空间,就ac了,时间变为125ms了,不过这样确实挺耗空间的,没办法,为了做题只能用空间换时间。

代码如下:

#include<iostream>using namespace std;
3int cases, count;
4int nodenum;
struct node
{
bool isExist;
node * branch[];
}Node[];
class Trie
{
14private:
node root;
16public:
Trie(){root = Node[];}
bool insert(char num[])
{
node *location = &root;
int i = ;
int len = strlen(num);
while(num[i])
{
if(i==len- && location->branch[num[i]-''] != NULL) //解决没有按照长度排序而存在的问题
{
return false;
}
if(location->branch[num[i]-'']==NULL)//没有建立
{
location->branch[num[i]-''] = &Node[nodenum];
Node[nodenum].isExist = false;
memset(Node[nodenum].branch,NULL,sizeof(Node[nodenum].branch));
nodenum++;
}
if(location->branch[num[i]-'']->isExist == true)
{
return false;
}
location = location->branch[num[i]-''];
i++;
}
location->isExist = true;
return true;
}
};
int main()
{
scanf("%d",&cases);
while(cases--)
{
nodenum = ;
bool flag = true;
scanf("%d",&count);
char tel[];
Trie t;
while(count--)
{
scanf("%s",tel);
if(!t.insert(tel))
{
flag = false;
}
}
if(flag)
{
printf("YES\n");
}
else
{
printf("NO\n");
}
}
return ;
}

我写的如下:运行ok,但是超时。

#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std; int cases, count;
int nodenum; struct Node
{ bool terminal;
Node *branch[]; Node(){ terminal=false;
for(int i=;i<;i++)
branch[i]=NULL; }
};
class Trie
{
public: Node *root;
Trie()
{
root=new Node();
}
bool insert(char *str)
{
Node* p=root;
for(int i=;i<strlen(str);i++)
{
if(p->branch[str[i]-'']==NULL)
{
p->branch[str[i]-'']=new Node();
nodenum++;
}
if(p->branch[str[i]-'']->terminal==true)
return false; p=p->branch[str[i]-''];
}
p->terminal=true;
return true;
}
};
int main()
{
scanf("%d",&cases);
while(cases--)
{
nodenum=;
bool flag=true;
scanf("%d",&count);
char tel[];
Trie *root=new Trie();
while(count--)
{
scanf("%s",tel);
if(root->insert(tel)==false)
flag=false;
}
if(flag)
{
cout<<"Yes"<<endl;
}
else
{
cout<<"No"<<endl;
}
}
}

方法二:

转成数字存储比较,这样的话用long整形就可以,然用除法+取余的方法核对是否是某个数字的前缀,但是这种方法的复杂度显然是O(n^2)呀,所以就不尝试了。

方法三:

受大雄提示,可以使用字符串排序比较来做,因为通过排序,前缀子串肯定是与父串挨着的,嘿嘿,这样做,思路简单、代码量少,易理解啊,所以很快ac,下面分析一下复杂度。

理论上使用trie的平均复杂度应该是n*len;其中,len是号码的平均长度,n是号码的个数。使用数组进行字符比较,理论上的复杂度有n*len+logn,排序为logn,然后查询是否存在前缀子串是n*len。所以后者应该时间稍微多一点,提交后果然,耗时188ms。

另外值得一提的是使用数组比较的方法有个好处,那就是地址都是连续的,cpu在寻址时会非常快,而用链式结构(即指针),包括使用数组型的trie树则是跳来跳去的,故会有一些开销吧。

呵呵,我所崇拜的排序又一次派上用场了。

代码如下:

#include<iostream>using namespace std;
int cases, count;
5char tel[][];
6int i, j;
int cmp(const void *a, const void *b)
{
return strcmp( (char*)a,(char*)b );
}
int main()
{
scanf("%d",&cases);
while(cases--)
{
bool flag = true;
scanf("%d",&count);
for(i = ; i < count; i++)
{
scanf("%s",tel[i]);
}
qsort(tel,count,sizeof(char)*,cmp);
int len1, len2;
for(i = ; i < count; i++)
{
len1 = strlen(tel[i-]);
len2 = strlen(tel[i]);
j = ;
if(len1 <= len2)
{
while(tel[i-1][j] == tel[i][j] && j < len1)
{
j++;
}
if(j == len1)
{
flag = false;
}
}
if(!flag)
{
break;
}
}
if(flag)
{
printf("YES\n");
}
else
{
printf("NO\n");
}
}
return ;
}

参考:http://www.cnblogs.com/cherish_yimi/archive/2009/10/12/1581795.html

http://www.ahathinking.com/archives/14.html

转:trie树--详解的更多相关文章

  1. trie树--详解

    文章作者:yx_th000 文章来源:Cherish_yimi (http://www.cnblogs.com/cherish_yimi/) 转载请注明,谢谢合作.关键词:trie trie树 数据结 ...

  2. &lbrack;转&rsqb; Trie树详解及其应用

    一.知识简介         最近在看字符串算法了,其中字典树.AC自动机和后缀树的应用是最广泛的了,下面将会重点介绍下这几个算法的应用.       字典树(Trie)可以保存一些字符串->值 ...

  3. Trie树详解

    1. 概述 Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树.Trie一词来自retrieve,发音为/tri ...

  4. Trie树详解及其应用

    一.知识简介        最近在看字符串算法了,其中字典树.AC自动机和后缀树的应用是最广泛的了,下面将会重点介绍下这几个算法的应用.      字典树(Trie)可以保存一些字符串->值的对 ...

  5. Trie树详解(转)

    特别声明 本文只是一篇笔记类的文章,所以不存在什么抄袭之类的. 以下为我研究时参考过的链接(有很多,这里我只列出我记得的): Trie(字典树)的应用——查找联系人 trie树 Trie树:应用于统计 ...

  6. B树、Trie树详解

    查找(二) 散列表 散列表是普通数组概念的推广.由于对普通数组可以直接寻址,使得能在O(1)时间内访问数组中的任意位置.在散列表中,不是直接把关键字作为数组的下标,而是根据关键字计算出相应的下标. 使 ...

  7. trie字典树详解及应用

    原文链接    http://www.cnblogs.com/freewater/archive/2012/09/11/2680480.html Trie树详解及其应用   一.知识简介        ...

  8. 数据结构图文解析之:AVL树详解及C&plus;&plus;模板实现

    0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...

  9. Linux DTS&lpar;Device Tree Source&rpar;设备树详解之二&lpar;dts匹配及发挥作用的流程篇&rpar;【转】

    转自:https://blog.csdn.net/radianceblau/article/details/74722395 版权声明:本文为博主原创文章,未经博主允许不得转载.如本文对您有帮助,欢迎 ...

随机推荐

  1. struts2&plus;hibernate 项目实战:图书管理系统

    经典项目,练手必备. 图书管理系统 需求分析(大致,并不专业):1.需要有用户管理: 1.1 用户注册: 1.2 用户登录: 1.3 用户信息修改: 1.4 用户修改密码: 2.需要有书本管理: 2. ...

  2. thinkphp表单自动验证

    ThinkPHP框架表单验证 对注册到test表的表单进行验证 在注册之前要对表单进行验证: 用户名非空验证,两次输入密码必须一致即相等验证,年龄在18~50之间即范围验证,邮箱格式正则验证. 自动验 ...

  3. Mip-Mapping很重要

    MipMap这个东东,记得我除了最早在DX9龙书上了解了其基本概念后,以后便再没接触过,因为从创建到使用都是硬件一手包办,所以这个知识点很容易被遗忘和忽视.这几天空闲时恰好发现了一点MipMap引起的 ...

  4. java动态代理与老式AOP实现

    JAVA的动态代理 代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息.过滤消息.把消息转发给委托类,以及事后处理消息等.代理类与委托类之间通常会 ...

  5. CSS——伪元素与伪类

    伪类与伪元素 伪类:在特殊性中占据0,0,1,0 :link 向未访问的链接添加特殊的样式.也就是说,链接所指的 URI 尚未出现在用户代理的历史中.这种状态与 :visited状态是互斥的. :vi ...

  6. 转:C&num; 泛型编程之泛型类、泛型方法、泛型约束

    C# 泛型编程之泛型类.泛型方法.泛型约束 分类: asp.net c#2012-08-07 17:36 5998人阅读 评论(0) 收藏 举报 c#编程classobject编译器struct 泛型 ...

  7. Moq &amp&semi; RhinoMocks

    Moq & RhinoMocks 使用Mock对象进行测试一般都会有以下三个关键步骤: 使用接口来描述需要测试的对象 为实际的产品代码实现这个接口 以测试为目的,在Mock对象中实现这个接口 ...

  8. 阿里云 Centos7&period;3安装mysql5&period;7&period;18 rpm安装

    卸载MariaDB CentOS7默认安装MariaDB而不是MySQL,而且yum服务器上也移除了MySQL相关的软件包.因为MariaDB和MySQL可能会冲突,故先卸载MariaDB. 1.安装 ...

  9. 关于iOS GangSDK的使用,为App快速集成社群公会模块

    手上有一个自己开发的小游戏,想加一个家族系统活跃下游戏的氛围,想到这块儿可能会有大量的工作需要自己做,就偷了个懒去网上搜罗了一波,结果惊奇的发现居然真的有类似的服务,并且还是免费的,所以决定入坑尝试一 ...

  10. shiro整合ehcache

    目标:让Shiro整合ehcache,提供缓存realm数据的功能. 1.引入encache配置文件,配置缓存 <!-- <ehcache xmlns:xsi="http://w ...