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

Trie树


引言

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

在这里插入图片描述

回答 : \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}核心思路 核心思路

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

插入操作


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

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

  1. 说白了就是, i   ,   k 表示节点的位置编号,这是相对整棵树而言的 \color{red}i ~,~ k 表示节点的位置编号,这是相对整棵树而言的 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~ 而言的 j表示孩子编号,表示节点 i 的第 j 个的孩子,这是相对节点 i 而言的

如图,当我们依次插入 abcd,abd,ace,bcd ,abda 时,得到第一种编号的同时,我们可以得到第二种编号, 绿色 \color{Green}绿色 绿色表示编号结果。因为每个节点最多有26个子节点,故此我们可以按他们的字典序从0 ~ 25编号,也就是 字母 A S C L L 码  −   a 的 A S C L L 码 字母ASCLL码~-~a的ASCLL码 字母ASCLL  aASCLL。因为先输入的是 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}查找某个单词出现次数 查找某个单词出现次数

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

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


我们可以使用 数组 c n t [   ] \color{Red}数组cnt[ ~] 数组cnt[ ]记录以当前字母结尾的单词次数 ,插入完毕后,我们使 数组 c n t [   p   ] 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 这个单词出现的次数时

对于 数组 s t r [   ] 数组 str[~] 数组str[ ],有

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

执行 \color{Purple}执行 执行   u = s t r [   i   ] −   ′ a ′ ~ u = str[~i~] - ~'a'  u=str[ i ] a

u = ′ a ′ − ′ a ′ = 0 u = 'a' - 'a' = 0 u=aa=0

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

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

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

在这里插入图片描述

执行 \color{Purple}执行 执行   u = s t r [   i   ] −   ′ a ′ ~ u = str[~i~] - ~'a'  u=str[ i ] a

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

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

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

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

在这里插入图片描述

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

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

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

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

我们可以使用 数组 s u m [   ] \color{Red}数组sum[ ~] 数组sum[ ]记录以此前缀出现的次数 ,插入过程中,我们使 数组 s u m [   s o n [   p   ] [   u   ]   ] 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];
}
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值