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;
}

posted on 2019-01-28 12:56  PPXppx  阅读(211)  评论(0编辑  收藏  举报