Trie树
Trie树是什么
trie树又称字典树,是一种数据结构。
对于数据的储存,并非像其他大多数的数据结构那样将数据存到节点中,而是将数据存到每一条**「边」中,从「根节点」到一个「终点节点」的路径是一条数据,节点存储的信息是「第p号节点的u字符」**指向的下一个节点的编号(这一句话会在下文详细介绍)。
以存储abb,abc,abcc,bd,cebz这五个字符串为例
IMG_0451(20221220-215024)
Trie树的所有节点都由一个空根结点扩展出来,根据当前有无存在等于当前字符的边,来决定是否通过建立新的节点来创造新的边。再存储完一个元素的最后,通过一个单独的数组来表示这个节点是一个终点(即终点节点),从根节点到当前节点的所有边是一个元素。
Trie树具体用处
- 查询一个元素是否在一个集合中
- 将一个元素插入到一个集合中
- 统计一个元素出现的次数
- 求多个元素的公共前缀
- 对于01数组进行各种异或操作(01trie树)
- 配合kmp实现ac自动机
insert
这里以洛谷trie树模板题www.luogu.com.cn/problem/P25…
首先介绍一下各个变量的含义
idx,初始值为1,用于给每个节点标号,通过++ idx来实现增加新的节点(有些像链表)p,在insert和judge函数中,作为临时变量,用于表示当前走到了第p号节点,初始值为1,代表第一步在根节点上ne[p][u],第p号节点(用idx做好标号)的u字符(一般是26字母或者是0-9数字,一般由传入的字符串或数字拆开得到)指向的下一个位置,如果有指向的下一个位置,就说明u字符是存在的,当前节点到ne[p][u]指向的节点的边就是uexist[p],用于标记第p号节点是终点节点,同时可以用它记录有多少个当前元素
对于插入一个字符串,首先根据这个字符串的字符组成来从根节点一步步拓展出相应的边,直到存储完标记出终点节点。例如样例中的a, b, c
首先对于最开始只有一个根节点,没有边,也就是没有存储任何信息。这时传入一个字符串a,首先获取字符串的长度len = str.size(),然后定义好局部变量p = 1,代表我们将从根节点(即1号节点)出发。
对于一个字符串,长度为len,所以最多拓展len条边(由于每次循环都会处理一个字符,所以循环len次),那么循环len次for (int i = 1; i <= len; i ++),对于每一次循环,都获取一下要处理的当前字符。
对于每一个当前处理的字符,首先判断当前节点是否存在前往当前字符的边(即ne[p][u]是否有值),如果存在,就走到指向的节点;不存在则建立新的节点与新边。例如在储存第一个字符串a中,原trie树的根节点并没有存储a的边,所以建立新节点++ idx(指建立了一个编号为2的节点),将代表**「从根节点走向a字符的边指向新的节点(即ne[1]['a']=idx)」**。这样便实现了一个元素的插入
IMG_0452(20221221-223355)
由于这个元素只有这一个字符,所以插入完a也就插入结束了,对于b,c过程类似,这里不做过多赘述。
IMG_0454(20221221-223541)
但这样就算成功插入了三个元素吗?其实并不是,我们还少了一个东西——终点节点。如果没有终点节点的话,如果以后又插入一个元素abc,那么走到a时我们并不会停止,会一直走到abc,这样就会出现覆盖的情况,所以在每插入一个元素后,都在要最后加上那么一句话exist[p] ++;代表p号节点是一个终点节点
IMG_0455(20221221-223618)
接下来要插入的是元素是ad,在循环到第一个元素a时,发现已经存在了从根节点出发的边a(即ne[1]['a']=2)那么就直接前往下一个节点即可(p = ne[p]['a']),到了2号节点后发现并没有存储d的边,那么就像上面说的那样再建立新边就好了
IMG_0456(20221221-223809)
对于最后的元素acd实际上也和ad的插入过程类似,就不多赘述了
IMG_0457(20221221-223903)
到这里,我们的插入也就结束了。
再解释的一下为什么p = ne[p][u]是前往下一个节点,首先在执行这一句话前,p代表的是当前第p号节点,ne[p][u]代表的是从p号节点出发经过边u到达的节点,这两个的值都是节点的编号,所以可以通过p = ne[p][u]实现让p指向下一个节点
全代码
int idx, ne[mn][60], exist[mn];
void insert(string str) {
int len = str.size();
int p = 1;
for (int i = 1; i <= len; i ++) {
int u = str[i - 1] - 'a' + 1;
if (!ne[p][u])
ne[p][u] = ++ idx;
p = ne[p][u];
}
exist[p] ++;
}
judge
judge的过程实际上是和insert差不多的,只是少了创造新节点的步骤,在judge中,我们主要需要去做的就是对当前节点进行各种判断
例如不存在从当前节点出发的边u,ne[p][u] == 0,就说明我们的树中并没有存储进这个元素,直接返回错误即可
如果一直走到了判断当前的元素的末尾,首先判断一下当前节点是不是终点节点,如果不是的话也说明这个元素并没有存储进树中,返回错误
如果是终点节点,那么判断一下他是不是第一次被访问到,如果是就返回正确,并将其标记成已经被访问过,如果访问过,就返回相应的状态。
全代码
int judge(string str) {
int len = str.size();
int p = 1;
for (int i = 1; i <= len; i ++) {
int u = str[i - 1] - 'a' + 1;
if (!ne[p][u])
return 0;
p = ne[p][u];
}
if (exist[p] == 1) {
exist[p] = 2;
return 1;
}
else if (exist[p] == 2)
return 2;
else
return 0;
}
本文使用 markdown.com.cn 排版