1. 什么是trie树
1.Trie树 (特例结构树)
Trie的核心思想是空间换时间。利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。
Trie树也有它的缺点,Trie树的内存消耗非常大。
2. 三个基本特性:
3 .例子
和二叉查找树不同,在trie树中,每个结点上并非存储一个元素。
trie树把要查找的关键词看作一个字符序列。并根据构成关键词字符的先后顺序构造用于检索的树结构。
在trie树上进行检索类似于查阅英语词典。
基本思想(以字母树为例):
1、插入过程
对于一个单词,从根开始,沿着单词的各个字母所对应的树中的节点分支向下走,直到单词遍历完,将最后的节点标记为红色,表示该单词已插入Trie 树。
2、查询过程
同样的,从根开始按照单词的字母顺序向下遍历trie树,一旦发现某个节点标记不存在或者单词遍历完成而最后的节点未标记为红色,则表示该单词不存在,若最后的节点标记为红色,表示该单词存在。
如给出字符串"abcd","ab","bd","dda","ddb",根据该字符串序列构建一棵Trie树。则构建的树如下:
Trie树的根结点不包含任何信息,第一个字符串为"abcd",第一个字母为'a',因此根结点的NextChar(map)中存在字符 'a',其他同理,构建的Trie树如图所示,红色结点表示在该处可以构成一个单词。在一条路径上可能存在多个单词,因此有多个节点被标记为红色。
字符 'ab' 和 'abcd' 都存在,但 'abc' 不存在,虽然 'abc' 可以从树的路径中找到,但是这条路径中的 'c' 节点不是字符串的结尾(不是红色)。
很显然,如果要查找单词"abcd"是否存在,查找长度则为O(len),len为要查找的字符串的长度。而若采用一般的逐个匹配查找,则查找长度为O(len*n),n为字符串的个数。显然基于Trie树的查找效率要高很多。
因为当查询如字符串abc是否为某个字符串的前缀时,显然以b、c、d....等不是以a开头的字符串就不用查找了,这样迅速缩小查找的范围和提高查找的针对性。所以建立Trie的复杂度为O(n*len),而建立+查询在trie中是可以同时执行的,建立的过程也就可以成为查询的过程,hash就不能实现这个功能。所以总的复杂度为O(n*len),实际查询的复杂度只是O(len)。
2. trie树的实现
程序实现(c++):
用char字符 ch 来保存字符串的每个字符,unordered_map (字典)来保存当前字符的所有下一个字符以及节点指针,isEnd 作为一个字符串结束的标识。
insert 函数用来插入一个字符串,isExist 函数用来判断一个字符串是否存在;findNextChar 函数用来返回下一个字符的节点指针(如果存在)。
#include <unordered_map>
#include <string>
#include <vector>
#include <set>
using namespace std;
class TrieTree
{
public:
TrieTree(char _ch) :ch(_ch),isEnd(false){} //初始化
void insert(const string &); //增加一个单词
bool isExist(const string &s); //判断字符串是否存在
~TrieTree(){}
private:
char ch; //字符
unordered_map<char, TrieTree*> NextChar; //当前字符的下一个字符及对应节点的集合
bool isEnd; //是否为结尾
//下一个字符集合种查找指定字符
TrieTree *findNextChar(char ch_find)
{
auto it = NextChar.find(ch_find);
//找到,返回节点指针
if (it != NextChar.end())
return NextChar[ch_find];
return NULL;
}
};
void TrieTree::insert(const string &s)
{
TrieTree *node = this;
for (auto it = s.begin(); it != s.end(); ++it)
{
//匹配,则移到下一个字符
if (node->findNextChar(*it) != NULL)
node = node->NextChar[*it];
//未匹配
else
{
//从未匹配的字符索引 it 处开始构建树
for (auto it_char = it; it_char != s.end(); ++it_char)
{
//每个字符生成一个节点,加入到下一层的 map 中
TrieTree *p = new TrieTree(*it_char);
node->NextChar[*it_char] = p;
node = p;
}
break;
}
}
//将最后一个节点设置为单词末尾
node->isEnd = true;
}
bool TrieTree::isExist(const string &s)
{
TrieTree *node = this;
for (auto it = s.begin(); it != s.end(); ++it)
{
if (node->findNextChar(*it) == NULL)
break;
node = node->NextChar[*it];
}
//如果到达末尾,则匹配存在
return node->isEnd;
}
3. trie树的应用:
1. 字符串检索,词频统计,搜索引擎的热门查询
事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。
举例:
1)有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
2)给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
3)1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串
4)寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
5)敏感词过滤:详细分析可以参考这里。
2. 字符串最长公共前缀
Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。举例:
1) 给出N 个小写英文字母串,以及Q 个查询,即查询某两个串的最长公共前缀的长度是多少。
解决方案:
首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是问题就转化为了离线 (Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。
3. 排序
Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。
举例: 给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。
4 作为其他数据结构和算法的辅助结构
如后缀树,AC自动机等。