Trie字典树(学习笔记)
简介
Trie字典树,又称单词查找树,是一种树形结构,是哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。 ---引自<<百度某科>>
不管上面那一串东西(≧▽≦)/,直接开始.
如图,这就是一棵Trie树,树的每条边上恰好对应一个字符,每个节点代表从根到该节点的路径所对应的字符串(也就是按顺序将所有经过的边上的字符连接起来),比如左下角这个叶节点代表的就是abcd.
此外,节点上还能存储额外的信息.这就要看题目的要求灵活变通了.
注意,Trie树的根节点为空,根节点表示空串.
对于任意一个节点,它到它的子结点边上的字符都互不相同(这不是废话么)
操作实现
初始化:一颗空Trie仅包含一个根节点,且根节点的字符指针指向空.
int tot=1;
//tot=1,表示新建一个空节点作为Trie树的根
插入操作:当需要插入一个字符串S时,我们令一个指针u先指向根节点.然后依次扫面字符串S中的每个字符c:
\(ch[u][c]\)表示节点u的c字符指针指向的节点,\(bj[u]=1\)表示从根到该节点u所经过的边上的字符所构成的字符串是题目给出的(我们输入的)一个完整的字符串,\(tot\)是Trie树中节点总数.
(1) 若指针u的c字符指针指向一个已经存在的节点,则令指针u等于该节点(即\(u=ch[u][c]\)).
(2) 若指针u的c字符指针指向空,则新建一个节点(即\(tot++\)),然后令指针u的c字符指针指向该新节点(即\(ch[u][c]=tot\)),再令指针u等于该新节点(即\(u=ch[u][c]\)).
(3) 当字符串S中的字符全部扫描完毕时,在当前节点指针u(即字符串S最后一个字符所指向的位置)打个标记,标记该节点是某一个字符串的末尾(这是为了查询前缀和等操作方便).
void insert(char *s){
int u=1;
int len=strlen(s);
for(int i=0;i<len;i++){
int c=s[i]-'0';
if(!ch[u][c])ch[u][c]=++tot;
u=ch[u][c];
}
bj[u]=1;
return;
}
主函数部分:
char s[10];
for(int i=1;i<=n;i++){
scanf("%s",s);
insert(s);
}
查询操作:当需要查询一个字符串S是否在Trie树中时,我们还是令一个指针u先指向根节点,然后依次扫描字符串S中的每个字符c:
(1) 若指针u的c字符指针指向空,则说明Trie树中不存在字符串S,结束查询.(即\(if(!ch[u][c])return false\))
(2) 若指针u的c字符指针指向一个已经存在的节点,则令u等于该节点.(即\(u=ch[u][c]\))
(3) 当字符串S中所有字符扫描完毕时,若当前节点u被打了标记,即表示当前节点u是在一个字符串的末尾,说明字符串S在Trie树中已经存在,否则说明不存在.
(即
if(bj[u])return true;
else return false;
)
完整查询代码:
bool find(char *s){
int len=strlen(s);
int u=1;
for(int i=0;i<len;i++){
int c=s[i]-'a';
if(!ch[u][c])return false;
u=ch[u][c];
}
if(bj[u])return true;
else return false;
}
来看几道模板题~\(≧▽≦)/~
题意:给定n个数字串,判断其中是否存在一个数字串是另一个数字串的前缀.
分析:Trie树就是为找前缀而生的吧.本题就作为模板题了.考虑把所有数字串构成一棵Trie树,在构建过程中就可以顺便判断答案了:
(1)若当前串插入后没有新建任何节点,则当前串肯定是之前插入的某个串的前缀;
(2)若插入过程中,有某个经过的节点带有串结尾的标记,则之前插入的某个串是当前串的前缀;
int T,tot;char s[15];
int ch[100005][15],bj[100005];
bool insert(char *s){
bool flag=false;
int u=1,len=strlen(s);
for(int i=0;i<len;i++){
int a=s[i]-'0';
if(!ch[u][a])ch[u][a]=++tot;
else if(i==len-1)flag=true;
//情况1:没有插入任何新节点;
u=ch[u][a];
if(bj[u])flag=true;
//情况2:经过某个有标记的节点;
}
bj[u]=1;
return flag;
}
int main(){
T=read();
while(T--){
tot=1;//多组数据,记得把Trie树初始化
memset(ch,0,sizeof(ch));
memset(bj,0,sizeof(bj));
int n=read(),ans=0;
for(int i=1;i<=n;i++){
scanf("%s",s);
if(insert(s))ans=1;
}
if(!ans)puts("YES");
else puts("NO");
}
return 0;
}
题意:有n个人,每个人的名字是长度不超过50的小写字符串.有m次点名,如果该名字正确且是第一次点到,输出“OK”,如果该名字错误,输出“WRONG”,如果该名字正确但不是第一次点到,输出“REPEAT”.
分析:就是两个基本操作(插入和查询),唯一需要注意的是判重,在查询cheek的时候用flag数组做个标记,记录是否已经被点到过.
int n,m,tot=1,ans;
int ch[1000005[27],bj[1000005];
int flag[1000005];
void insert(string s){
int u=1,len=s.size();
for(int i=0;i<len;i++){
int a=s[i]-'a';
if(!ch[u][a])ch[u][a]=++tot;
u=ch[u][a];
}
bj[u]=1;
return;
}
int cheek(string s){
int u=1,len=s.size();
for(int i=0;i<len;i++){
int a=s[i]-'a';
if(!ch[u][a])return 0;
u=ch[u][a];
}
if(!flag[u])flag[u]=1;
//如果是第一次点到,就把该名字的flag数组标记为1
else ans=1;
//否则,就把ans记为1,表示重复点到.
return 1;
//return 1仅表示此次点名的名字是合法的.
}
int main(){
n=read();
for(int i=1;i<=n;i++){
string s;cin>>s;
insert(s);
}
m=read();
while(m--){
ans=0;
string s;cin>>s;
if(cheek(s)){
if(ans==1)puts("REPEAT");
else puts("OK");
}
else puts("WRONG");
}
return 0;
}