C++数据结构与算法分析——Trie树

Trie树介绍

Trie树,也叫字典树,前缀树。是一种用于实现字符串快速检索多叉树数据结构。它支持两种操作:

  1. 在树中插入一个字符串
  2. 字符串检索

Trie树的每个节点都拥有若干个字符指针,它可以用来插入字符串或者进行字符串检索。

Trie树的基本操作

初始化

Trie树刚开始时只有一个根节点,它的所有指针均指向NULL
根节点并不存储信息,只作为树的标识,进行字符串插入和检索时从根节点进入并扫描其节点,原因也容易理解,根节点只有一个,所以只能存储一个字符,如果要在根节点存入字符的话,就需要开多棵Trie树分别存入不同的开头字母,这显然有些多此一举了。

插入

假设我们此时需要插入一个字符串s,起初我们令一个指针p指向根节点,依次扫描s的每个字符c,此时我们会发现有两种情况:

  1. pc字符指针指向了一个已存在的节点q,则p直接走到q,然后更新p,即p = q
  2. pc指针指向空,此时我们新建一个节点q,然后将p走到q,然后更新p,即p = q

当字符串s扫描完毕时,在当前节点打上标记。因为如果不打上标记的话,假如此时存入了一个字符串 b a t m a n batman batman,然后我们要查询是否有存入过 b a t bat bat,没有标记的话,就无法判断是否插入过 b a t bat bat了。

检索

检索和插入很像。
假设我们此时需要检索一个字符串s,起初我们令一个指针p指向根节点,依次扫描s的每个字符c,此时我们会发现有两种情况:

  1. pc字符指针指向了一个已存在的节点q,则p直接走到q,然后更新p,即p = q
  2. pc指针指向空,则代表该字符串没有插入过,此时我们结束检索

s中的字符扫描完毕时,若当前p有被标记过,则说明s在Trie中存在,否则不存在。

示例

假设此时我们需要插入一个字符串cat

  1. 创建指针p进入根节点,扫描其子节点,发现并没有c这个节点,因此创建一个c节点,然后将p移动到c
    1

  2. p扫描c的子节点,发现并没有a字符,创建并移动到a
    2

  3. p扫描a的子节点,发现并没有t,创建并移动到t
    3

  4. 此时我们发现字符串已经扫描完毕,标记p
    4

假设我们此时还需要插入一个字符串cab

  1. 创建指针p进入根节点,扫描其子节点有没有字符c,发现是有的,直接进入c
    1

  2. 扫描p的子节点有没有a,发现是有的,直接进入a
    2

  3. 扫描p的子节点,看看有没有b,发现并没有,创建并将p移动到节点b
    7

  4. 此时发现字符串cab已扫描完毕,标记p
    8
    其余也是类似,此处就不再过多说明。

例题

问题描述

维护一个字符串集合,支持两种操作:

I x:向集合中插入一个字符串 x
Q x :询问一个字符串在集合中出现了多少次。
共有N个操作,输入的字符串总长度不超过 1 0 5 10^5 105,字符串仅包含小写英文字母

输入格式

第一行包含整数N,表示操作数。
接下来N行,每行包含一个操作指令,指令为I xQ x中的一种。

输出格式

对于每个询问指令Q x,都要输出一个整数作为结果,表示x在集合中出现的次数。
每个结果占一行。

数据范围

1 ≤ N ≤ 2 ∗ 1 0 4 1≤N≤2∗10^4 1N2104

输入样例:

5
I abc
Q abc
Q ab
I ab
Q ab

输出样例:

1
0
1

解题思路

思路1:unordered_map(哈希表)

此题只有插入和查询字符串次数两个操作,那么用哈希表显然是一种快准狠的方法,在此就不多赘述了,读者感兴趣可以自己写写

思路1代码
#include<iostream>
#include<unordered_map>
#include<cstring>
using namespace std;

int n;
unordered_map<string,int> mp;

int main(){
    cin >> n;
    
    while(n --){
        char op[2];
        string str;
        cin >> op >> str;
        if(op[0] == 'I') mp[str] ++;
        else printf("%d\n",mp[str]);
    }
    
    return 0;
}

思路2:

使用Trie树,此时我们会发现询问操作是询问我们字符串出现次数,那么插入结束时的标记操作就可以转换为计数操作(插入结束时在尾部 + 1),其余并没有什么变化。

代码
#include<iostream>
#include<cstring>
using namespace std;

const int N = 100010;
int n;
char str[N];
int son[N][26],cnt[N],idx;

void insert(char str[]){ // 插入字符串str
    int p = 0; // 创建p指向根节点
    for(int i = 0; str[i]; i ++){ // 遍历待插入字符串
        int u = str[i] - 'a'; // 将 a ~ z 映射为 0 ~ 25
        if(!son[p][u]) son[p][u] = ++ idx; // 如果p没有u这个子节点,则创建出该子节点
        p = son[p][u]; // 将p移动到该子节点中
    }
    
    cnt[p] ++; // 表示该字符串出现次数 + 1
}

int query(char str[]){ // 检索
    int p = 0; // 创建指针p指向根节点
    for(int i = 0; str[i]; i ++){ // 遍历待检索字符串
        int u = str[i] - 'a'; // 将 a ~ z 映射为 0 ~ 25
        if(!son[p][u]) return 0; // 如果p没有u这个子节点,说明没有该字符串,返回0
        p = son[p][u]; // 将p移动到该子节点中
    }
    
    return cnt[p]; // 返回字符串出现的次数
}

int main(){
    scanf("%d",&n);
    
    while(n --){
        char op[2];
        scanf("%s%s",op,str);
        if(op[0] == 'I') insert(str); // 插入
        
        else printf("%d\n",query(str)); // 检索
    }
    
    return 0;
}
  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

L_Hygen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值