原文链接 http://www.cnblogs.com/freewater/archive/2012/09/11/2680480.html
Trie树详解及其应用
一、知识简介
最近在看字符串算法了,其中字典树、AC自动机和后缀树的应用是最广泛的了,下面将会重点介绍下这几个算法的应用。
字典树(Trie)可以保存一些字符串->值的对应关系。基本上,它跟 Java 的 HashMap 功能相同,都是 key-value 映射,只不过 Trie 的 key 只能是字符串。
Trie 的强大之处就在于它的时间复杂度。它的插入和查询时间复杂度都为 O(k) ,其中 k 为 key 的长度,与 Trie 中保存了多少个元素无关。Hash 表号称是 O(1) 的,但在计算 hash 的时候就肯定会是 O(k) ,而且还有碰撞之类的问题;Trie 的缺点是空间消耗很高。
至于Trie树的实现,可以用数组,也可以用指针动态分配,我做题时为了方便就用了数组,静态分配空间。
Trie树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较,查询效率比哈希表高。
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
Trie树的基本性质可以归纳为:
(1)根节点不包含字符,除根节点意外每个节点只包含一个字符。
(2)从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
(3)每个节点的所有子节点包含的字符串不相同。
Trie树有一些特性:
1)根节点不包含字符,除根节点外每一个节点都只包含一个字符。
2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
3)每个节点的所有子节点包含的字符都不相同。
4)如果字符的种数为n,则每个结点的出度为n,这也是空间换时间的体现,浪费了很多的空间。
5)插入查找的复杂度为O(n),n为字符串长度。
基本思想(以字母树为例):
1、插入过程
对于一个单词,从根开始,沿着单词的各个字母所对应的树中的节点分支向下走,直到单词遍历完,将最后的节点标记为红色,表示该单词已插入Trie树。
2、查询过程
同样的,从根开始按照单词的字母顺序向下遍历trie树,一旦发现某个节点标记不存在或者单词遍历完成而最后的节点未标记为红色,则表示该单词不存在,若最后的节点标记为红色,表示该单词存在。
二、字典树的数据结构:
利用串构建一个字典树,这个字典树保存了串的公共前缀信息,因此可以降低查询操作的复杂度。
下面以英文单词构建的字典树为例,这棵Trie树中每个结点包括26个孩子结点,因为总共有26个英文字母(假设单词都是小写字母组成)。
则可声明包含Trie树的结点信息的结构体:
typedef struct Trie_node
{
int count; // 统计单词前缀出现的次数
struct Trie_node* next[26]; // 指向各个子树的指针
bool exist; // 标记该结点处是否构成单词
}TrieNode , *Trie;
其中next是一个指针数组,存放着指向各个孩子结点的指针。
如给出字符串"abc","ab","bd","dda",根据该字符串序列构建一棵Trie树。则构建的树如下:
Trie树的根结点不包含任何信息,第一个字符串为"abc",第一个字母为'a',因此根结点中数组next下标为'a'-97的值不为NULL,其他同理,构建的Trie树如图所示,红色结点表示在该处可以构成一个单词。很显然,如果要查找单词"abc"是否存在,查找长度则为O(len),len为要查找的字符串的长度。而若采用一般的逐个匹配查找,则查找长度为O(len*n),n为字符串的个数。显然基于Trie树的查找效率要高很多。
如上图中:Trie树中存在的就是abc、ab、bd、dda四个单词。在实际的问题中可以将标记颜色的标志位改为数量count等其他符合题目要求的变量。
已知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)。
三、Trie树的操作
在Trie树中主要有3个操作,插入、查找和删除。一般情况下Trie树中很少存在删除单独某个结点的情况,因此只考虑删除整棵树。
1、插入
假设存在字符串str,Trie树的根结点为root。i=0,p=root。
1)取str[i],判断p->next[str[i]-97]是否为空,若为空,则建立结点temp,并将p->next[str[i]-97]指向temp,然后p指向temp;
若不为空,则p=p->next[str[i]-97];
2)i++,继续取str[i],循环1)中的操作,直到遇到结束符'\0',此时将当前结点p中的 exist置为true。
2、查找
假设要查找的字符串为str,Trie树的根结点为root,i=0,p=root
1)取str[i],判断判断p->next[str[i]-97]是否为空,若为空,则返回false;若不为空,则p=p->next[str[i]-97],继续取字符。
2)重复1)中的操作直到遇到结束符'\0',若当前结点p不为空并且 exist 为true,则返回true,否则返回false。
3、删除
删除可以以递归的形式进行删除。
前缀查询的典型应用:
http://acm.hdu.edu.cn/showproblem.php?pid=1251
#include<iostream>
#include<cstring>
using namespace std; typedef struct Trie_node
{
int count; // 统计单词前缀出现的次数
struct Trie_node* next[26]; // 指向各个子树的指针
bool exist; // 标记该结点处是否构成单词
}TrieNode , *Trie; TrieNode* createTrieNode()
{
TrieNode* node = (TrieNode *)malloc(sizeof(TrieNode));
node->count = 0;
node->exist = false;
memset(node->next , 0 , sizeof(node->next)); // 初始化为空指针
return node;
} void Trie_insert(Trie root, char* word)
{
Trie node = root;
char *p = word;
int id;
while( *p )
{
id = *p - 'a';
if(node->next[id] == NULL)
{
node->next[id] = createTrieNode();
}
node = node->next[id]; // 每插入一步,相当于有一个新串经过,指针向下移动
++p;
node->count += 1; // 这行代码用于统计每个单词前缀出现的次数(也包括统计每个单词出现的次数)
}
node->exist = true; // 单词结束的地方标记此处可以构成一个单词
} int Trie_search(Trie root, char* word)
{
Trie node = root;
char *p = word;
int id;
while( *p )
{
id = *p - 'a';
node = node->next[id];
++p;
if(node == NULL)
return 0;
}
return node->count;
} int main(void)
{
Trie root = createTrieNode(); // 初始化字典树的根节点
char str[12] ;
bool flag = false;
while(gets(str))
{
if(flag)
printf("%d\n",Trie_search(root , str));
else
{
if(strlen(str) != 0)
{
Trie_insert(root , str);
}
else
flag = true;
}
} return 0;
}
字典树的查找
http://acm.hdu.edu.cn/showproblem.php?pid=1075
#include<iostream>
#include<cstring>
using namespace std; typedef struct Trie_node
{
int count; // 统计单词前缀出现的次数
struct Trie_node* next[26]; // 指向各个子树的指针
bool exist; // 标记该结点处是否构成单词
char trans[11]; // 翻译
}TrieNode , *Trie; TrieNode* createTrieNode()
{
TrieNode* node = (TrieNode *)malloc(sizeof(TrieNode));
node->count = 0;
node->exist = false;
memset(node->next , 0 , sizeof(node->next)); // 初始化为空指针
return node;
} void Trie_insert(Trie root, char* word , char* trans)
{
Trie node = root;
char *p = word;
int id;
while( *p )
{
id = *p - 'a';
if(node->next[id] == NULL)
{
node->next[id] = createTrieNode();
}
node = node->next[id]; // 每插入一步,相当于有一个新串经过,指针向下移动
++p;
node->count += 1; // 这行代码用于统计每个单词前缀出现的次数(也包括统计每个单词出现的次数)
}
node->exist = true; // 单词结束的地方标记此处可以构成一个单词
strcpy(node->trans , trans);
} char* Trie_search(Trie root, char* word)
{
Trie node = root;
char *p = word;
int id;
while( *p )
{
id = *p - 'a';
node = node->next[id];
++p;
if(node == NULL)
return 0;
}
if(node->exist) // 查找成功
return node->trans;
else // 查找失败
return NULL;
} int main(void)
{
Trie root = createTrieNode(); // 初始化字典树的根节点
char str1[3003] , str2[3003] , str[3003] , *p;
int i , k; scanf("%s",str1);
while(scanf("%s",str1) && strcmp(str1 , "END") != 0)
{
scanf("%s",str2);
Trie_insert(root , str2 , str1);
} getchar();
gets(str1);
k = 0;
while(gets(str1))
{
if(strcmp(str1 , "END") == 0)
break;
for(i = 0 ; str1[i] != '\0' ; ++i)
{
if(str1[i] >= 'a' && str1[i] <= 'z')
{
str[k++] = str1[i];
}
else
{
str[k] = '\0';
p = Trie_search(root , str);
if(p)
printf("%s", p);
else
printf("%s", str);
k = 0;
printf("%c", str1[i]);
}
}
printf("\n");
} return 0;
}
转自:http://blog.csdn.net/Hackbuteer1/article/details/7964147#comments
华为机试题:
输出字符串对应的数字,即yiersansi转化为1234
#ifndef TRIETREE_H
#include<memory>
class TrieTree{
private:
struct Node{
int count;//前缀次数
Node* next[26];
char digit;
bool terminal; Node(char d=0):count(0),digit(d),terminal(false){
memset(next,0,sizeof(next));
}
}; Node* _root; public:
TrieTree():_root(new Node()){} void insert(char* str,int digit){
Node* cur=_root;
while(*str){
int i=*str-'a';
if(!cur->next[i]){
cur->next[i]=new Node();
}
cur=cur->next[i];
cur->count++; str++;
}
cur->digit=digit;
cur->terminal=true;
} char search(char* str){
Node* cur=_root;
while(*str){
int i=*str-'a';
Node* next=cur->next[i];
if(!next)//error
return '\0';
str++;
}
if(cur->terminal)
return cur->digit;
else
return '\0';
} void search2(char* str){
Node* cur=_root;
while(*str){
int i=*str-'a';
Node* next=cur->next[i];
cur=next;
str++; if(cur->terminal){
cout<<cur->digit<<' ';
cur=_root;
}
}
cout<<endl;
}
}; #define TRIETREE_H
#endif//TRIETREE_H int main()
{
TrieTree trie;
char* pinyin[]={"ling","yi","er","san","si","wu","liu","qi","ba","jiu"};
char digits[]={'0','1','2','3','4','5','6','7','8','9'};
for(int i=0;i<10;i++){
trie.insert(pinyin[i],digits[i]);
} char* searched="yilingyisi*";
trie.search2(searched);
}
四、trie树的应用:
1. 字符串检索,词频统计,搜索引擎的热门查询
事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。
举例:
1)有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
2)给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
3)给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。
4)1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串
5)寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
2. 字符串最长公共前缀
Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。举例:
1) 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少. 解决方案:
首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线 (Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。
而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:
1. 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;
2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;
3. 排序
Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。
举例: 给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。
4 作为其他数据结构和算法的辅助结构
如后缀树,AC自动机等。
-------------希望有一天我有能力自己写自己的博客。
trie字典树详解及应用的更多相关文章
-
Trie字典树详解
今天上午省选字符串......只会KMP.连hash都不会的我被大佬虐惨了......于是我要发奋图强学习字符串,学习字符串当然就要从Trie树这种可爱的数据结构开始啦!!! 一.什么是Trie树?? ...
-
Trie字典树的学习及理解
字典树详解见此 我这里学习时主要是看了李煜东的进阶指南里的讲解,以下是书中介绍的内容. Trie,又称字典树,是一种用于实现字符串快速检索的多叉树结构,Tire的每个节点都拥有若干个字符指针,若在插入 ...
-
Trie树详解(转)
特别声明 本文只是一篇笔记类的文章,所以不存在什么抄袭之类的. 以下为我研究时参考过的链接(有很多,这里我只列出我记得的): Trie(字典树)的应用——查找联系人 trie树 Trie树:应用于统计 ...
-
萌新笔记——C++里创建 Trie字典树(中文词典)(一)(插入、遍历)
萌新做词典第一篇,做得不好,还请指正,谢谢大佬! 写了一个词典,用到了Trie字典树. 写这个词典的目的,一个是为了压缩一些数据,另一个是为了尝试搜索提示,就像在谷歌搜索的时候,打出某个关键字,会提示 ...
-
数据结构图文解析之:AVL树详解及C++模板实现
0. 数据结构图文解析系列 数据结构系列文章 数据结构图文解析之:数组.单链表.双链表介绍及C++模板实现 数据结构图文解析之:栈的简介及C++模板实现 数据结构图文解析之:队列详解与C++模板实现 ...
-
Trie字典树 动态内存
Trie字典树 #include "stdio.h" #include "iostream" #include "malloc.h" #in ...
-
算法导论:Trie字典树
1. 概述 Trie树,又称字典树,单词查找树或者前缀树,是一种用于快速检索的多叉树结构,如英文字母的字典树是一个26叉树,数字的字典树是一个10叉树. Trie一词来自retrieve,发音为/tr ...
-
Python - 字典(dict) 详解 及 代码
字典(dict) 详解 及 代码 本文地址: http://blog.csdn.net/caroline_wendy/article/details/17291329 字典(dict)是表示映射的数据 ...
-
Linux DTS(Device Tree Source)设备树详解之二(dts匹配及发挥作用的流程篇)【转】
转自:https://blog.csdn.net/radianceblau/article/details/74722395 版权声明:本文为博主原创文章,未经博主允许不得转载.如本文对您有帮助,欢迎 ...
随机推荐
-
USACO Milking Cows
思路: 脑抽了,一看题目,这不就是线段树么,离散化区间合并..最终发现我并不会写...于是看了下题目范围10^6...模拟水之..每个区间左端点+1,右端点-1,从左到右扫一下就行了... 代码: / ...
-
SQL Server 高性能写入的一些总结
1.1.1 摘要 在开发过程中,我们不时会遇到系统性能瓶颈问题,而引起这一问题原因可以很多,有可能是代码不够高效.有可能是硬件或网络问题,也有可能是数据库设计的问题. 本篇博文将针对一些常用的数据库性 ...
-
jQuery1.9.1源码分析--Animation模块
var fxNow, // 使用一个ID来执行动画setInterval timerId, rfxtypes = /^(?:toggle|show|hide)$/, // eg: +=30.5px / ...
-
Android 里面的小坑
1.listview加了个blockdescen,竟然导致editTextView不能获取焦点
-
Android 开源优秀的项目
webrtc square/picasso Android 的一个强大的图像下载和缓存库 A powerful image downloading and caching library for A ...
-
BZOJ 4176: Lucas的数论 [杜教筛]
4176: Lucas的数论 题意:求\(\sum_{i=1}^n \sum_{j=1}^n \sigma_0(ij)\) \(n \le 10^9\) 代入\(\sigma_0(nm)=\sum_{ ...
-
mysql存储引擎、事务
MySQL存储引擎介绍 文件系统 操作系统组织和存取数据的一种机制. 文件系统是一种软件. 文件系统类型 ext2 ext3 ext4 xfs 数据 不管使用什么文件系统,数据内容不会变化 不同 ...
-
关于IE浏览器 ajax 请求返回数据不对的问题
在使用ajax向后台发送请求的时候,在使用ie 进行调试的时候发现根据条件进行查询时,返回的数据与没有根据条件进行查询时数据相同,也就是条件没有发生作用. 经过同事的帮助发现ajax初始化设置时没有c ...
-
c#反射(1)
反射可以读取程序集中代码的内容,程序集一般指(dll或exe文件). 反射中Type类,这个类太强大了,可以获取到另一个类的名称,命名空间,程序集,以及这个类中的字段,属性,方法.可以方便我们查看某个 ...
-
在HTML中用循环语句
<%for(){% > <tr> <td></td> <td></td> </tr> <%}%> 注意 ...