前言
字典是干啥的?查找字的。
字典树自然也是起查找作用的。
查找的是啥?单词。
看以下几个题:
1、给出n个单词和m个询问,每次询问一个单词,回答这个单词是否在单词表中出现过。
答:简单!map,短小精悍。
好。下一个
2、给出n个单词和m个询问,每次询问一个前缀,回答询问是多少个单词的前缀。
答:map,把每个单词拆开。
judge:n<=200000,TLE!
这就需要一种高级数据结构——Trie树(字典树)
什么是字典树?
叫前缀树更容易理解
字典树的样子
黑色的点就是插入一个单词时候的结束标记点。
由此可以看出:
1、字典树用边表示字母
2、有相同前缀的单词公用前缀节点,那我们可以的得出每个节点最多有26个子节点(在单词只包含小写字母的情况下)若是包含大写和小写字母,则每个节点应该有’z’-‘A’+1个子节点。
3、整棵树的根节点是空的。为什么呢?便于插入和查找,这将会在后面解释。
4、每个单词结束的时候用一个黑点表示,那么从根节点到任意一个黑色的点所经过的边的所有字母表示一个单词。也就是之前说的结束标记点
基本操作
A、insert,插入一个单词
1.思路
从图中可以直观看出,从左到右扫这个单词,如果字母在相应根节点下没有出现过,就插入这个字母;否则沿着字典树往下走,看单词的下一个字母。
这就产生一个问题:往哪儿插?计算机不会自己选择位置插,我们需要给它指定一个位置,那就需要给每个字母编号。
以我们的字符串只包括小写字母为例,那么我们每个节点就会有26个子节点,这些子节点的顺序编号是什么呢?0-25。
比如我们要插入bc,那么我们会从根节点开始遍历,因为没有根节点的子节点中b这个点的编号还是为0,所以我们可以把
trie【root】【‘b’-‘a’】 = 1,这里是1是因为bc是我们插入的第一个单词,也就是整棵树的编号。
之后将根节点=trie【root】【‘b’ - ‘a’】,继续上面的操作,如果子节点中c的编号还是0,就将trie【root】【‘c’-‘a’】 = 2,因为c已经是插入的这个单词中的最后一个字母了,所以需要加一个结束标记数组,将js【root】标记为1。
代码:
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点
// cnt[]存储以每个节点结尾的单词数量
// 插入一个字符串
void insert(char *str)
{
int p = 0; //根节点编号为0
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';//第二种编号
if (!son[p][u])//如果之前没有从p到u的前缀
son[p][u] = ++ idx;//插入,tot即为第一种编号
p = son[p][u];//顺着字典树往下走
}
cnt[p] ++ ;
}
B、search,查找
查找有很多种,可以查找某一个前缀,也可以查找整个单词。
再次我们以查找一个前缀是否出现过为例讲解
1、思路
从左往右以此扫描每个字母,顺着字典树往下找,能找到这个字母(即这个字母子节点的编号不为0),往下走,否则结束查找,即没有这个前缀;前缀扫完了,表示有这个前缀。
如果是查找整个单词的话需要注意判断最后一个字母的子节点的位置是否有结束标记。
代码:
// 查询字符串出现的次数
int query(char *str)
{
int p = 0;//从根结点开始找
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) return 0;//以p为头结点的u字母不存在,返回0
p = son[p][u];//为查询下个字母做准备,往下走
}
return cnt[p];//找到了,并返回出现次数
}
2、如果是查询单词出现的次数的话,那就在开一个sum[],sum【root】表示这个单词出现的次数。
trie树的应用:
- 字符串检索,词频统计,搜索引擎的热门查询
事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。
举例:
1)有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
2)给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
3)给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。
4)1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串
5)寻找热门查询:搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来,每个查询串的长度为1-255字节。假设目前有一千万个记录,这些查询串的重复读比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个。一个查询串的重复度越高,说明查询它的用户越多,也就越热门。请你统计最热门的10个查询串,要求使用的内存不能超过1G。
-
字符串最长公共前缀
Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。举例:- 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少. 解决方案:
首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线 (Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。 而最近公共祖先问题同样是一个经典问题,可以用下面几种方法: 1 利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法; 2. 求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;
-
排序
Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。举例: 给你N 个互不相同的仅由一个单词构成的英文名, 让你将它们按字典序从小到大排序输出。
4 .作为其他数据结构和算法的辅助结构
如后缀树,AC自动机等。