【概述】
字典树,又称为单词查找树,Tire 树,是一种树形结构,它是哈希树的变种。
字典树与字典很相似,当要查一个单词是不是在字典树中,首先看单词的第一个字母是不是在字典的第一层,如果不在,说明字典树里没有该单词,如果在就在该字母的孩子节点里找是不是有单词的第二个字母,没有说明没有该单词,有的话用同样的方法继续查找,以此类推。
基本性质:
- 字典树用边表示字符
- 有相同前缀的单词共用前缀结点
- 根节点不包含字符
- 每个单词结束的时候都用一个特殊字符表示,图中用的红色 $ 符
- 从根节点到一个红点所经过的所有边的字母就是一个字符串
应用:其常用于统计、排序和保存大量的字符串(不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
优点:其利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。
【构建过程】
1.插入
1)思路
对于字典树,从左到右扫描一个单词,若字母在相应根节点下没有出现,就插入这个字母,若出现过,则沿着树走下去,看单词的下一个字母。
那么此时产生一个问题,对于从左到右扫描的这个单词,若字母在相应根节点下没有出现过,如何去选择位置插入?计算机不会自动选择位置,因此需要给他指定一个位置,这样就需要对每个字母进行编号。
设数组 tire[i][j]=k,表示编号为 i 的结点的第 j 个孩子是编号为 k 的结点,此时有两种编号:
- i、k:表示结点位置的编号,相对整棵树而言,此处相同字母编号不同
- j:表示几点 i 的第 j 个孩子,相对结点 i 而言,此处编码应按照 ASCLL 码来编,用到哪个编哪个,相同字母编号相同
以单词 cat,cash,app,apple,aply,ok 为例,按照输入顺序对其进行编号
第一种编号结果:
第二种编号结果:
经过这样的编号后,这样数组 trie[i][j]=k,表示编号为 i 的节点的第 j 个孩子是编号为 k 的节点,那么第一种编号即为 i、k,第二种编号即为 j,从而可以实现插入
2)实现
int tot=0;//编号
int trie[N][26];//字典树
int val[N];//字符串结尾标记,val[i]=x表示第i个节点的权值为x
void insert(char *s){//插入单词s
int len=strlen(s);//单词s的长度
int root=0;//字典树上当前匹配到的结点
for(int i=0;i<len;i++){
int id=s[i]-'a';//子节点编号
if(trie[root][id]==0)//如果之前没有从root到id的前缀
trie[root][id]=++tot;//插入
root=trie[root][id];//顺着字典树往下走
}
val[root]=n;
}
2.查找
1)思路
查找有很多种,可以查找某一个前缀,也可以查找整个单词。一般来说,对于一个单词,从左到右依次扫描每个字母,顺着字典树往下找,能找到这个字母,往下走,否则结束查找,即没有这个单词;单词扫完了,表示有这个单词。
如果是查询某个单词是否在字典树中的话,可用布尔变量 vis[i] 表示节点 i 是否是单词结束的标志,那么返回的是 vis[root],所以在插入操作中插入完每个单词时,要对单词最后一个字母的 vis[i] 置为 true
如果是查询前缀出现的次数的话,那就在开一个数组 sum[i],表示位置 i 被访问过的次数,那么最后返回的是 sum[root],所以插入操作中每访问一个节点,都要让他的 sum++。在这里,前缀的次数是标记在前缀的最后一个字母所在位置的后一个位置上。
例如:前缀 abc 出现的次数标记在 c 所在位置的后一个位置上
2)实现
bool find(char *s){//查询单词是否在树中
int len=strlen(s);//单词长度
int root=0;//从根结点开始找
for(int i=0;s[i];i++){
int x=s[i]-'a';
if(trie[root][x]==0)//以root为头结点的x字母不存在
return false;
root=trie[root][x];//为查询下个字母做准备,往下走
}
return val[root];//找到
}
3.删除
1)思路
对于一个单词,如果要在树中将其删除,有三种情况:
- 没找到这个单词
- 找到叶节点的时,叶节点的 cnt 标志清零,代表不是叶节点
- 当前节点没有其他孩子节点的时,可直接删除这个节点
2)实现
void del(char *str,int word){//word为要删除的单词的标号,一般为seach("单词");
int len=strlen(str);
int root=0;
if(word==0)//没找到单词
return;
for(int i=0;i<len;i++){
int x=str[i]-'a';
if(tire[root][x]==0)//没有子节点
return;
sum[root]-=cnt;//减去前缀
root=tire[root][x];
}
sum[root]=0;//前缀清零
for(int i=0;i<26;i++)//删除节点
tire[root][i]=0;
}
【模版】
1.查询单词/前缀是否出现
int tot;
int trie[N][26];//trie[rt][x]=tot,root是上个节点编号,x是字母,tot是下个节点编号
//bool vis[N];//查询整个单词用
void insert(char *s,int root){
for(int i=0;s[i];i++){
int x=s[i]-'a';
if(trie[root][x]==0)//现在插入的字母在之前同一节点处未出现过
trie[root][x]=++tot;//字母插入一个新的位置,否则不做处理
root=trie[root][x];//为下个字母的插入做准备
}
//vis[root]=true;//标志该单词末位字母的尾结点,在查询整个单词时用
}
bool find(char *s,int root){
for(int i=0;s[i];i++){
int x=s[i]-'a';
if(trie[root][x]==0)
return false;//以root为头结点的x字母不存在,返回0
root=trie[root][x];//为查询下个字母做准备
}
return true;
//return vis[root];//查询整个单词时
}
int main(){
int n,m;
char s[22];
tot=1;
cin>>n;//插入单词个数
for(int i=1;i<=n;i++){
cin>>s;
insert(s,1);
}
cin>>m;//查询单词个数
for(int i=1;i<=n;i++){
cin>>s;
if(find(s,1))
printf("YES\n");
else
printf("NO\n");
}
return 0;
}
2.查询前缀出现次数
int trie[400001][26],tot;
int sum[400001];
void insert(char *s){
int root=0;
int len=strlen(s);
for(int i=0;i<len;i++){
int id=s[i]-'a';
if(!trie[root][id])
trie[root][id]=++tot;
sum[trie[root][id]]++;//前缀保存
root=trie[root][id];
}
}
int search(char *s){
int root=0;
int len=strlen(s);
for(int i=0;i<len;i++){//root经过循环后变成前缀最后一个字母所在位置
int id=s[i]-'a';
if(!trie[root][id])
return 0;
root=trie[root][id];
}
return sum[root];
}
int main(){
int n,m;
char s[11];
tot=1;
cin>>n;//插入单词个数
for(int i=1;i<=n;i++){
cin>>s;
insert(s);
}
cin>>m;//查询次数
for(int i=1;i<=m;i++){
cin>>s;
printf("%d\n",search(s));
}
}
3.结构体实现的增删查字典树
struct Node{
int sum;//前缀
int next[26];//子节点
void init(){
sum=0;
memset(next,-1,sizeof next);
}
}tire[N];
int tot;
void insert(char *str){
int len=strlen(str);
int root=0;
for(int i=0;i<len;i++){
int x=str[i]-'a';
if(tire[root].next[x]==-1)
tire[root].next[x]=tot++;
root=tire[root].next[x];
tire[root].sum++;
}
}
int search(char *str){
int len=strlen(str);
int root=0;
for(int i=0;i<len;i++){
int x=str[i]-'a';
if(tire[root].next[x]==-1)
return 0;
root=tire[root].next[x];
}
return tire[root].sum;
}
void del(char *str,int word){
int len=strlen(str);
int root=0;
if(word<0)
return;
for(int i=0;i<len;i++){
int x=str[i]-'a';
if(tire[root].next[x]==-1)
return;
tire[root].sum-=word;
root=tire[root].next[x];
}
tire[root].sum=0;
for(int i=0;i<26;i++)
tire[root].next[i]=-1;
}
int main(){
tot=1;
for(int i=0;i<N;i++)
tire[i].init();
int t;
scanf("%d",&t);
while(t--){
char str[10],word[35];
scanf("%s%s",str,word);
if(str[0]=='i')//插入
insert(word);
else if(str[0]=='d')//删除
del(word,search(word));
else{//查询
if(search(word)>0)
printf("Yes\n");
else
printf("No\n");
}
}
return 0;
}
4.输出唯一前缀
int tot;
int trie[N][26];//trie[rt][x]=tot,root是上个节点编号,x是字母,tot是下个节点编号
int val[N];
void insert(char *s,int root){
int len=strlen(s);
for(int i=0;i<len;i++){
int x=s[i]-'a';
if(trie[root][x]==0){//现在插入的字母在之前同一节点处未出现过
trie[root][x]=tot;//字母插入一个新的位置,否则不做处理
val[tot]=0;//记录以当前结点为根的子树下单词个数
tot++;
}
root=trie[root][x];//为下个字母的插入做准备
val[root]++;
}
}
void find(char *s,int root){
int len=strlen(s);
for(int i=0;i<len;i++){
int x=s[i]-'a';
root=trie[root][x];//为查询下个字母做准备
printf("%c",s[i]);//输出当前字母
if(val[root]==1)//为1时直接返回
return;
}
}
char word[N][50];
int main(){
int cnt=0;
tot=1;
while(scanf("%s",word[cnt])!=EOF)
insert(word[cnt++],0);
for(int i=0;i<cnt;i++){//枚举所有单词
printf("%s ",word[i]);
find(word[i],0);
printf("\n");
}
return 0;
}