字典树(trie)

字典树是一种树形结构,节点存储的不是完整的字符串而是前缀字符。

字典树的优点:因为通过前缀字符来选择树上的分支,所以可以节省时间

字典树的缺点:比较浪费空间,与总字符数量有关

字典树的根节点不存储字符

用动态分配空间实现(常见)

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>

typedef struct TreeNode {
    int pass;//记录有多少个字符串以当前字符串做前缀 
    int end;//计入有多少个字符串以当前字符串结束 
    struct TreeNode* next[26];//指向下一个字符,即下一个节点 
} Node;

typedef struct trie {//只需要知道根节点就可以访问整个字典树 
	Node* root;
} Trie;

void deleteNode(Node* root)//从root层序遍历字典树,删除root的整颗树,包括 root节点 
{
	if (root == NULL) {
		return ;
	}
	
    Node* q[100000] = { 0 };//准备一个队列 
    int l = 0;//队列头 
    int r = 0;//队列尾 

    q[r++] = root;//根节点入队 

    while (l != r) {//只要队列中还有节点 
        Node* t = q[l++];//出队 

        for (int i = 0; i < 26; i++) {//访问所有孩子节点 
            if (t->next[i]) {//只要有孩子 
                q[r++] = t->next[i];//把孩子节点入队 
            }
        }
        free(t);//释放刚刚出队的节点,因为已经处理完了它的孩子节点,所以可以释放它了  
    }      
}

void freeTrie(Trie* root) {
	deleteNode(root -> root);//先释放字典树 
	free(root);//再释放root 
}

Node* makeNode(void) {//创建一个节点 
    Node* node = (Node*)malloc(sizeof(Node));//malloc一个节点 

    node->end = 0;//初始化 
    node->pass = 0;//初始化 
    for (int i = 0; i < 26; i++) {
        node->next[i] = NULL;//指向空代表没有路了 
    }

    return node;//返回创建的节点 
}

void insert(Trie* root, char s[]) {//把s放入字典树中 
    Node* r = root->root;//指向字典树的根节点 
    
    r->pass++;//先让根节点的pass加一,因为来到了根节点 

    for (int i = 0; s[i]; i++) {//遍历s中的每一个字符 
        int index = s[i] - 'a';//取出对应的下标,0 -> 'a' ~ 25 -> 'z' 

        if (r->next[index] == NULL) {//如果没有路了,那么就创建一个节点 
                r->next[index] = makeNode();//让对应的下标指向新创建的节点 
        }
        r = r->next[index];//来到下一个节点 
        r->pass++;//因为来到一个新的节点,所以pass++ 
    }

    r->end++;//遍历完了之后,r来到最后一个节点,然后end++,表示以字符串s结尾的字符串的个数加一 
}

int search(Trie* root, char s[]) {//返回在字典树中字符串s出现的次数 
    Node* cur = root->root;//指向字典树的根节点 

    for (int i = 0; s[i]; i++) {//遍历字符串中的每一个字符 
        int index = s[i] - 'a';//计算字符所对应的下标 

        if (cur->next[index] == NULL)//如果没有路了代表肯定没有这个字符串 
        {
                return 0;//返回0,表示出现0次 
        }
        cur = cur->next[index];//去下一个节点 
    }

    return cur -> end;//最后返回最后一个节点的end,因为end表示以当前路径结尾组成的字符串有多少个 
}

void deleteWord(Trie* root, char s[]) {//删除字典树中,字符串s,只删除一次 
    if (search(root, s)) {//只有存在字典树中才删除 
        Node* cur = root->root;//指向字典树的根节点 
        --cur->pass;//来到一个节点就让pass减一 

        for (int i = 0; s[i]; i++) {//遍历s中的每一个字符串 
            int index = s[i] - 'a';//取出对应的下标 
			//注意是下一个节点的 pass,因为要删掉的是下一个节点而不是当前的节点 
            if (--cur -> next[index] -> pass <= 0) {//如果下一个节点的pass减一之后 <= 0,那么释放下一个节点,因为后面的节点都没有用了。 
                    deleteNode(cur->next[index]);//删除下一个节点的整颗树 
                    cur->next[index] = NULL;//让它指向空 
                    return;//没有必要再删了,因为已经删除完了,所以结束函数 
            }
            cur = cur->next[index];//去对应的下一个节点 
        }

        cur->end--;//让最后一个节点的end减一,表示字符串s在字典树中的出现次数减一 
    }
}

int prefixNumber(Trie* root, char s[]) {//返回在字典树中有多少个字符串以字符串s做前缀 
    Node* cur = root->root;//指向字典树的根节点 

    for (int i = 0; s[i]; i++) {//遍历字符串s的每一个字符 
        int index = s[i] - 'a';//取出对应的下标 

        if (cur->next[index] == NULL) {//如果没有路了代表字符串s没有在字典树中,那么就不可能有以s做前缀的字符串,所以返回0 
                return 0;
        }

        cur = cur->next[index];//去对应的下一个节点 
    }

    return cur->pass;//pass含义就是以路径上的字符串做前缀的字符串个数 
}

int main() {
    int a;
    int n;
    char s[21] = "";
    Trie* r = (Trie*)malloc(sizeof(Trie));

    r->root = (Node*)malloc(sizeof(Node));

    r->root->end = 0;
    r->root->pass = 0;
    for (int i = 0; i < 26; i++) {
        r->root->next[i] = NULL;
    }

    scanf("%d", &n);

    while (n--) {
        scanf("%d %s", &a, &s);

        if (a == 1) {
                insert(r, s);
        }
        else if (a == 2) {
                deleteWord(r, s);
        }
        else if (a == 3) {
                if (search(r, s) > 0) {
                        printf("YES\n");
                }
                else {
                        printf("NO\n");
                }
        }
        else {
                int ans = prefixNumber(r, s);

                printf("%d\n", ans);
        }
    }
    
    freeTrie(r);
    
    return 0;
}

如果总字符类型很多的话,可以用哈希表做next数组来节约空间。

静态数组实现

#define  _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <time.h>
#include <stdbool.h>
#define u unsigned
#define ll long long
#define sc scanf
#define pr printf 
#define fr(i, j, n) for (int i = j; i < n; i++)
#define N 150001

int tree[N][26] = {0};//N表示字典树最多同时有n个节点,根节点的下标是1 
int end[N] = {0};//end数组表示以对应字符串的结尾的字符串的个数 
int pass[N] = {0};//pass数组表示以对应字符串做前缀的字符串的个数 
int cnt = 1;//表示使用到了多少下标,很关键的一个变量 

void clear(void) {//从下标1到cnt的空间全部置为0 
	for (int i = 1; i <= cnt; i++) {
		for (int j = 0; j < 26; j++) {
			tree[i][j] = 0;
		}
		pass[i] = 0;
		end[i] = 0;
	}
}

void build(void) {//创建一个字典树就是让cnt等于1 
	cnt = 1;
}

void insert(char s[]) {//插入一个字符串s到字典树中 
	int cur = 1;//指向根节点的下标 
	
	pass[cur]++;//让根节点对应的pass++ 
	
	for (int i = 0; s[i]; i++) {//遍历字符串s中的每一个字符 
		int index = s[i] - 'a';//取出对应的位置 
		
		if (!tree[cur][index]) {//如果tree[cur][index]的值为0,代表没有下一个节点,所以把++cnt的位置给它作为下一个节点 
			tree[cur][index] = ++cnt;
		}
		cur = tree[cur][index];//去下一个节点 
		pass[cur]++;//让对应的pass++ 
	}
	
	end[cur]++;//让对应的end++ 
}

int search(char s[]) {//返回字符串s在字典树中出现了多少次 
	int cur = 1;//来到根节点 
	
	for (int i = 0; s[i]; i++) {
		int index = s[i] - 'a';
		
		if (!tree[cur][index]) {//如果tree[cur][index]的值为0,代表没有路了,也就是在字典树中没有字符串s,所以返回0 
			return 0;
		}
		cur = tree[cur][index];//去下一个节点 
	}
	
	return end[cur];//返回对应end,即字符串s在字典树中出现的次数 
}

void deleteWord(char s[]) {//在字典树中删除字符串s 
	if (search(s) > 0) {//字符串s在字典树中出现过才删除 
		int cur = 1;//来到根节点 

		pass[cur]--;//根节点的pass值减一 
		
		for (int i = 0; s[i]; i++) {
			int index = s[i] - 'a';
			
			if (--pass[tree[cur][index]] <= 0) {//如果下一个节点对应的pass值减一后小于等于0了,那么代表这条分支没有用了,让对应的下标置为0 
				tree[cur][index] = 0;//为什么呢,因为cnt一直在加,没有减过,所以实际上是拿没有用过的空间来用,就像数组实现队列一样,队列尾一直往后面走。 
				return ;
			}
			
			cur = tree[cur][index];//去下一个节点 
		}
		end[cur]--;//对应的end减一 
	}
}

int prefixNumber(char s[]) {//返回以字典树中以字符串s做前缀有多少个字符串 
	int cur = 1;
	
	for (int i = 0; s[i]; i++) {
		int index = s[i] - 'a';
		
		if (!tree[cur][index]) {//如果tree[cur][index]的值为0,代表没有路了,也就是在字典树中没有字符串s 
			return 0;//所以返回0 
		}
		
		cur = tree[cur][index];//去下一个节点 
	}
	
	return pass[cur];//返回对应的pass值 
}

int main(int argc, char* argv[])
{
	int n;
	char s[21] = "";
	int t;
	
	build();
	
	sc("%d", &n);
	
	while(n--) {
		sc("%d %s", &t, s);
		
		if (t == 1) {
			insert(s);
		} 
		else if (t == 2) {
			deleteWord(s);
		}
		else if (t == 3) {
			if (search(s) > 0) {
				pr("YES\n");
			}
			else {
				pr("NO\n");
			}
		}
		else {
			pr("%d\n", prefixNumber(s));
		}
	}
	
	clear();
	
	return 0;
}

静态数组的实现本质是一种模拟链表,这样可以节约空间,因为实现了空间的复用,而且常数时间更快,所以写题目更推荐使用静态数组的实现。

posted @   lwj1239  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示