一、引入
字典是用来干啥的? 查找字的;
字典树自然也是起查找作用。
我们也许会遇到这样子的问题:
给出n个单词和m个询问,每次询问一个单词,回答这个单词是否在单词表中出现过;
答案:很容易想到用map, 快速解题。。。。
那么当我们再次遇到这样的呢?
给出n个单词和m个询问,每次询问一个前缀,回答这个前缀是多少单词的前缀;
这样我们用map显然不行,我们需要一种高级的数据结构——Trie树(字典树);
二、原理
在本文中,假设所有单词都是由小写字母构成;
对cat , cash , app , apple , aply , ok 建立一棵Trie字典树 ,建成之后如下图:
由图可以看出:
1、字典树用边表示字母;
2、有相同前缀的单词 共用前缀结点,每个结点最多有26个子结点(在单词中只包含小写字母的情况下)
3、整棵树的根结点是空值,这样有便于插入和查找;
4、每个单词结束时用‘ $ ’符号标记,那么从跟节点到任意一个‘ $ ’所经过的边表示一个单词;
三、基本操作
A、insert,插入一个单词
1.思路
从图中直观的看出,从左到右扫描这个单词,如果字母在相应节点下没有出现过,就插入这个字母;否则沿着字典树往下走,看单词的下一个字母。
那么我们就遇到一个问题:
怎么确定插入的位置,计算机不会自己选择位置,我们需要给他指定一个位置,那就需要给每个字母编号。
我们设数组trie[i][j] = k,表示编号为i的结点的标号为j的孩子的编号为k的结点;
什么意思呢?
我们把它分解就是两种编号:
第一种:i , k 表示结点在整棵树上的位置编号;
第二种:i , j 表示结点 i 的编号为 j 的孩子(相对于结点 i,j用ASSCLL码表示);
还不理解,看图:
还是单词cat , cash , app , apple , aply , ok
我们按照输入顺序对其编第一种编号,红色表示编号结果。因为先输入cat ,所以分别是 1 , 2 , 3,然后输入cash,因为c, a是公共前缀,所以从s开始编号,s是4,以此类推。
注意这里相同字母编号可能不同;
第二种编号,相对结点的编号,紫色表示编号结果。
因为每个节点最多有26个子节点,我们可以按他们的字典序从0~25编号,也就是s[i] - 'a'。
注意这里相同字母的编号相同
实际上每个节点的子节点都应该从 0 编到 25,但这样会发现许多事根本用不到的。比如上图的根节点应该分出26个叉。节约空间,用到哪个分哪个。
这样编号有什么用呢?
回到数组trie[i][j]=k。 数组trie[i][j]=k,表示编号为i的节点的第j个孩子是编号为k的节点。
那么第二种编号即为j,第一种编号即为i,k
2、代码
const int maxn = 1e6+7;
int trie[maxn][27], tot = 0;
void insert(string s) //插入单词s
{
int len = s.length(), root = 0; //根节点编号为0;
for(int i = 0; i < len; i++) {
int id = s[i] - 'a'; //第一种编号j
if(!trie[root][id]) //如果之前没有root到id的前缀
trie[root][id] = ++tot; //插入,tot即为第一种编号
root = trie[root][id]; //顺着字典树往下走
}
}
B、search,查找
查找有很多种,可以查找某一个前缀,也可以查找整个单词。
再次我们以查找一个前缀是否出现过为例讲解
1、思路
从左往右以此扫描每个字母,顺着字典树往下找,能找到这个字母,往下走,否则结束查找,即没有这个前缀;前缀扫完了,表示有这个前缀。
2、代码
const int maxn = 1e6+7;
int trie[maxn][27], tot = 0;
bool rsearch(string s) {
int len = s.length(), root = 0; //从跟节点开始查找;
for(int i = 0; i < len; i++) {
int id = s[i] - 'a';
if(!trie[root][id]) return false; //从root到id的前缀不存在,返回false
root = trie[root][id]; //为查询下一个字母,顺着字典树往下走
}
return true; //存在返回true
}
3、如果是查询某个单词的话,我们用bool变量 v[i]表示节点i是否是单词结束的标志。
那么最后return的是v[root],所以在插入操作中插入完每个单词是,要对单词最后一个字母的v[i]置为true,其他的都是false
4、如果是查询前缀出现的次数的话,那就在开一个sum[],表示位置i被访问过的次数,
那么最后return的是sum[root],插入操作中每访问一个节点,都要让他的sum++
这里前缀的次数是标记在前缀的最后一个字母所在位置的后一个位置上。
比如:前缀abc出现的次数标记在c所在位置的后一个位置上,
整个模板:
#include<bits/stdc++.h>
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
const int maxn = 1e6+7;
int trie[maxn][27], tot = 0;
bool isw[maxn]; //标记单词结束的结点i
int sum[maxn]; //保存前缀出现的次数
void insert(string s) //插入单词s
{
int len = s.length(), root = 0; //根节点编号为0;
for(int i = 0; i < len; i++) {
int id = s[i] - 'a'; //第一种编号j
if(!trie[root][id]) //如果之前没有root到id的前缀
trie[root][id] = ++tot; //插入,tot即为第一种编号
root = trie[root][id]; //顺着字典树往下走
sum[root]++; //前缀后一个位置保存前缀出现的次数;
}
/*isw[root] = true; 标记单词结束的结点*/
}
bool rsearch(string s) {
int len = s.length(), root = 0; //从跟节点开始查找;
for(int i = 0; i < len; i++) {
int id = s[i] - 'a';
if(!trie[root][id]) return false; //从root到id的前缀不存在,返回false
root = trie[root][id]; //为查询下一个字母,顺着字典树往下走
}
return true; //存在返回true
//return isw[root]; 查询整个单词是返回isw[root];
//return sum[root]; 前缀后一个位置保存前缀出现的次数;返回值需改为int;
}
int main()
{
memset(isw, 0, sizeof(isw));
memset(sum, 0, sizeof(sum));
}