算法学习——Trie树

106 阅读6分钟

Trie树

Trie树是什么

trie树又称字典树,是一种数据结构。

对于数据的储存,并非像其他大多数的数据结构那样将数据存到节点中,而是将数据存到每一条**「边」中,从「根节点」到一个「终点节点」的路径是一条数据,节点存储的信息是「第p号节点的u字符」**指向的下一个节点的编号(这一句话会在下文详细介绍)。

以存储abb,abc,abcc,bd,cebz这五个字符串为例

IMG_0451(20221220-215024)

IMG_0451(20221220-215024)

Trie树的所有节点都由一个空根结点扩展出来,根据当前有无存在等于当前字符的边,来决定是否通过建立新的节点来创造新的边。再存储完一个元素的最后,通过一个单独的数组来表示这个节点是一个终点(即终点节点),从根节点到当前节点的所有边是一个元素。

Trie树具体用处

  1. 查询一个元素是否在一个集合中
  2. 将一个元素插入到一个集合中
  3. 统计一个元素出现的次数
  4. 求多个元素的公共前缀
  5. 对于01数组进行各种异或操作(01trie树)
  6. 配合kmp实现ac自动机

insert

这里以洛谷trie树模板题www.luogu.com.cn/problem/P25…

首先介绍一下各个变量的含义

  1. idx,初始值为1,用于给每个节点标号,通过++ idx来实现增加新的节点(有些像链表)
  2. p,在insert和judge函数中,作为临时变量,用于表示当前走到了第p号节点,初始值为1,代表第一步在根节点上
  3. ne[p][u],第p号节点(用idx做好标号)的u字符(一般是26字母或者是0-9数字,一般由传入的字符串或数字拆开得到)指向的下一个位置,如果有指向的下一个位置,就说明u字符是存在的,当前节点到ne[p][u]指向的节点的边就是u
  4. exist[p] ,用于标记第p号节点是终点节点,同时可以用它记录有多少个当前元素

对于插入一个字符串,首先根据这个字符串的字符组成来从根节点一步步拓展出相应的边,直到存储完标记出终点节点。例如样例中的a, b, c

首先对于最开始只有一个根节点,没有边,也就是没有存储任何信息。这时传入一个字符串a,首先获取字符串的长度len = str.size(),然后定义好局部变量p = 1,代表我们将从根节点(即1号节点)出发。

对于一个字符串,长度为len,所以最多拓展len条边(由于每次循环都会处理一个字符,所以循环len次),那么循环lenfor (int i = 1; i <= len; i ++),对于每一次循环,都获取一下要处理的当前字符。

对于每一个当前处理的字符,首先判断当前节点是否存在前往当前字符的边(即ne[p][u]是否有值),如果存在,就走到指向的节点;不存在则建立新的节点与新边。例如在储存第一个字符串a中,原trie树的根节点并没有存储a的边,所以建立新节点++ idx(指建立了一个编号为2的节点),将代表**「从根节点走向a字符的边指向新的节点(即ne[1]['a']=idx)」**。这样便实现了一个元素的插入

IMG_0452(20221221-223355)

IMG_0452(20221221-223355)

由于这个元素只有这一个字符,所以插入完a也就插入结束了,对于b,c过程类似,这里不做过多赘述。

IMG_0454(20221221-223541)

IMG_0454(20221221-223541)

但这样就算成功插入了三个元素吗?其实并不是,我们还少了一个东西——终点节点。如果没有终点节点的话,如果以后又插入一个元素abc,那么走到a时我们并不会停止,会一直走到abc,这样就会出现覆盖的情况,所以在每插入一个元素后,都在要最后加上那么一句话exist[p] ++;代表p号节点是一个终点节点

IMG_0455(20221221-223618)

IMG_0455(20221221-223618)

接下来要插入的是元素是ad,在循环到第一个元素a时,发现已经存在了从根节点出发的边a(即ne[1]['a']=2)那么就直接前往下一个节点即可(p = ne[p]['a']),到了2号节点后发现并没有存储d的边,那么就像上面说的那样再建立新边就好了

IMG_0456(20221221-223809)

IMG_0456(20221221-223809)

对于最后的元素acd实际上也和ad的插入过程类似,就不多赘述了

IMG_0457(20221221-223903)

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中,我们主要需要去做的就是对当前节点进行各种判断

例如不存在从当前节点出发的边une[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 排版