Trie树(字典树)详解及代码实现 -- 查询前缀,字符串出现的次数

40 阅读5分钟

引言

众所周知,字典的用途无非就是用来查找字的。顾名思义,字典树自然也是起到查找的作用。我们可以看看以下几个问题:

在这里插入图片描述

回答:\color{Green}回答:

利用 map 存储每个单词,调用 count 即可得出结果

在这里插入图片描述

回答:\color{Green}回答:

将单词拆分后,用 map存储每一段,调用 count 得出结果

若 n <= 2e5 时,如果继续使用map,将会 TLE,这时我们就需要使用一种高级的数据结构,即 Trie树

基本概念及性质


Tire树,又称字典树。

  • 是一种高效存储查找字符串集合的数据结构
  • 核心思想是利用空间换时间,利用字符串的公共前缀来降低查询时间的开销
  • 适用于 全是同类字符的情形:
    • 小写字母
    • 大写字母
    • 数字
    • 0 / 1

性质\color{Orange}性质

  1. 下标是 0 的节点 (头节点),即是根节点,又是空节点

  2. 树的每个节点包含的子节点字符都不相同

  3. 从根节点到某一节点,路径上所经过的字符拼接起来,就是该节点对应的字符串


实现思路及图解


int son[N][26];// 后面的 26 数字视题目而定
int idx; 

// son[][]存储树中每个节点的子节点
// idx 记录每个字符的编号

核心思路\color{Red}核心思路

  • 从前往后依次遍历每个字符,看是否有此字母作为子节点,没有则创建新的节点
  • 在每个单词结尾打上标记,表示以当前字母结尾的单词存在

插入操作


对于每个字符,我们给它指定一个插入位置,即给每个字母贴上编号

如 数组 son[ i ][ j ]=kson[~i~][~j~] =k ,表示 编号为i\color{Orange}编号为i 的节点的,j\color{Orange}第j个孩子, 是 编号为k\color{Orange}编号为k 的节点

  1. 说白了就是,i , k表示节点的位置编号,这是相对整棵树而言的\color{red}i ~,~ k 表示节点的位置编号,这是相对整棵树而言的

如图,当我们依次插入 abcd,abd,ace,bcd ,abda 时,我们将得到第一种编号,蓝色\color{blue}蓝色表示编号结果。因为先输入的是 abcd,所以 a ,b,c,d的分别是 1,2,3,然后输入的是abd,因为 a,b 此前已经输入,即是公共前缀,故从 d 开始编号,所以 d 是 4,此后输入以此类推

在这里插入图片描述

可以发现相同的字母的编号可能不同


  1. j表示孩子编号,表示节点 i 的第 j 个的孩子,这是相对节点 i 而言的\color{red}j 表示孩子编号,表示节点 ~i~ 的第 ~j ~个的孩子,这是相对节点 ~i~ 而言的

如图,当我们依次插入 abcd,abd,ace,bcd ,abda 时,得到第一种编号的同时,我们可以得到第二种编号,绿色\color{Green}绿色表示编号结果。因为每个节点最多有26个子节点,故此我们可以按他们的字典序从0 ~ 25编号,也就是字母ASCLL码  aASCLL字母ASCLL码~-~a的ASCLL码。因为先输入的是 abcd,所以 a ,b,c,d的分别是 0,1,2,3,此后输入以此类推

在这里插入图片描述

可以发现相同字母的编号相同


代码模板

void insert(char *str)
{
    int p = 0; // 从根节点出发,根节点的编号为 0
    
	for(int i = 0; str[i]; i ++ )
	{
		int u = str[i] - 'a';// 第二种编号	
        if(!son[p][u]) // 如果之前没有从 p 到 u 的前缀
            son[p][u] = ++idx; // idx 即为第一种编号 

        p = son[p][u]; // 顺着字典树往下走
    }
    
}

查找操作

(1) 查找某个单词出现次数\color{Orange}查找某个单词出现次数

对于一个给定的字符串,我们从左往右扫描每个字母,顺着字典树往下找

  • 若能走得到这个字母,往下走,当字符串扫完后,返回此字符串出现的次数cnt[ p ]\color{Red}字符串出现的次数 cnt[ ~p ~]
  • 若能走不到这个字母,结束查找,即没有这个前缀, 返回 0\color{Red}0


我们可以使用 数组cnt[ ]\color{Red}数组cnt[ ~]记录以当前字母结尾的单词次数 ,插入完毕后,我们使 数组 cnt[ p ]cnt[~p~]加 1,表示以当前字母结尾的单词数个数加 1

如下图所示,依次插入单词后,我们用 灰色\color{Gray}灰色 表示以该字母结尾的单词出现的次数

在这里插入图片描述

代码模板

int cnt[N]; // cnt[]存储每个节点结尾的单词结尾,即打标记

void insert(char *str)
{
    int p = 0;
    
    for(int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        
        if(!son[p][u])
            son[p][u] = ++idx;
        
        p = son[p][u];
    }
    
    cnt[p] ++; // 表示当前单词数 加 1
}

int query(char *str)//查找以某个单词出现的次数
{
    int p = 0;
    
    for(int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        
        if(!son[p][u]) return 0;
        
        p = son[p][u];
    }
    
    return cnt[p];
}

例如,当我们查找 ac 这个单词出现的次数时

对于数组str[ ]数组 str[~],有

 i ~i~012
str[ i ]str[~i~]'a''c'’\0‘

执行\color{Purple}执行  u=str[ i ] a~ u = str[~i~] - ~'a'

u=aa=0u = 'a' - 'a' = 0

因为此时 son[ p ][ u ]=son[ 0 ][ 0 ] =1 != 0son[~p~][~u~]= son[~0~][~0~]~ = 1~!= ~0

执行\color{Purple}执行  p=son[ p ][ u ]=son[0][0]=1~ p = son[~p~][~u~]= son[0][0] = 1(根节点 的编号为 0,它的第 0 个 孩子编号为 1)

故 如下图所示,走到编号为 1 的 a 处

在这里插入图片描述

执行\color{Purple}执行  u=str[ i ] a~ u = str[~i~] - ~'a'

u=ca=2u = 'c' - 'a' = 2

因为此时 son[ p ][ u ]=son[ 1 ][ 2 ] =6 != 0son[~p~][~u~]= son[~1~][~2~]~ = 6~!= ~0

执行\color{Purple}执行  p=son[ p ][ u ]=son[1][2]=6~ p = son[~p~][~u~]= son[1][2] = 6 (a 的编号为 1,它的第 2 个 孩子编号为 6)

故 如下图所示,走到编号为 6 的 c 处

在这里插入图片描述

最后 执行\color{Purple}执行  cnt[ p ]= cnt[ 6 ]=3~ cnt[~p~] = ~ cnt[~6~] = 3,我们即可得到单词 ac 的数量为 3

(2) 查找某个前缀出现次数\color{Orange}查找某个前缀出现次数

对于一个给定的前缀字符串,我们从左往右扫描每个字母,顺着字典树往下找

  • 若能走得到这个字母,往下走,当字符串扫完后,返回此前缀出现的次数 sum[ p ]\color{Red}前缀出现的次数 ~sum[ ~p ~]
  • 若能走不到这个字母,结束查找,即没有这个前缀, 返回 0\color{Red}0

我们可以使用 数组sum[ ]\color{Red}数组sum[ ~]记录以此前缀出现的次数 ,插入过程中,我们使 数组 sum[ son[ p ][ u ] ]sum[~son[~p~][~u~]~]加 1,表示此前缀的某段部分的出现的次数加 1

依次插入单词后,我们用 灰色\color{Gray}灰色 表示此单词出现的次数,

相应地,我们可以得到各个前缀段出现的次数,我们用 黄色\color{Yellow}黄色 表示以该前缀段出现(从 该字母根节点 所经过的字母)的次数,如下图所示

代码模板

int sum[N];// sum[] 记录某段前缀出现的次数

void insert(char *str)
{
    int p = 0;
    for(int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';

        if(!son[p][u])
            son[p][u] = ++idx;
        
        sum[son[p][u]] ++;//表示此前缀某段部分出现次数加 1

        p = son[p][u];
    }

}

int find(char *str)//查询某个前缀出现次数
{
    int p = 0;
    
    for(int i = 0; str[i]; i ++ )
    {
        int u = str[i] - 'a';
        
        if(!son[p][u]) return 0;
        
        p = son[p][u];
    }
    
    return sum[p];
}