数据结构-trie树 插入,查询操作详解

trie树简介

Trie树(也称为字典树、前缀树)是一种专门用于处理字符串的数据结构。它的主要特点是能够提供高效的插入、查找操作,尤其适用于需要频繁搜索、前缀匹配和大量字符串存储的应用场景。

需要明确的是:
1.Trie树是一种树形结构,每个节点代表一个字符或者字符串的一部分。
2.每个节点通常有多个子节点,每个子节点对应一个字符,从而形成树状结构。

我们根据这两个特征来进行插入操作,通过一道模板题更直观的理解代码实现。

在这里插入图片描述

trie树的插入操作

我们先来画张图来模拟一下插入过程(记住:trie树是存储字符串的数据结构,有时也可以用来存储数据,我们稍后会讲)

在这里插入图片描述
我们想要插入这三个字符串应该怎么插入到trie树呢?如果当前节点的子节点没有该字母,就要创建新的节点插入该字母。
在这里插入图片描述
上面是插入第一个字符串
在这里插入图片描述
插入第二个字符串,特别说明一下,因为从根节点出发,根节点的子节点中没有b的节点,所以要创建新节点来存储b。

在这里插入图片描述

插入第三个字符串,因为从根节点出发,根节点已经有子节点a了,所以,进入下一个节点(也就是a)再看a节点有没有子节点和字符串中的第二个字母相同的。

以上就是整个插入的过程。
简单来说分两步
1.从字符串的第一个字母开始,从根节点出发,看看根节点的子节点有没有该字母,没有就创建新节点存储,有就更新节点,切换字符串的第二个字母,再查看子节点。
2.到字符串末尾的时候,我们需要在末尾节点做个标记,标记以该节点结尾的字符串有出现过。

来看代码实现:

int son[N][26];//son[i]代表是某节点,son[i][]表示son[i]的子节点,son[N][26]是指有N个节点,每个节点可能有26个子节点
int nums[N];//nums[i]表示以i节点结尾的字符串出现的次数
int idx;//表示如今用到第几个节点,因为没有节点时,需要创建节点
char str[N];
void insert(char* str){
    int cur=0;//说明当前所在的节点是根节点
    for(int i=0;str[i]!='\0';i++){
        //枚举字符
        int u=str[i]-'a';//将字母字符隐射到数字上,0-25
        if(!son[cur][u]){
            //当前节点的u所代表的字母没有节点,需要分配节点
            son[cur][u]=++idx;//++idx代表更新到还没用过的节点
        }
        cur=son[cur][u];//如果有节点了,更新当前要处理的节点
    }
    nums[cur]++;//当前节点结尾的字符串出现次数加一
}

trie树的查询操作

在这里插入图片描述
假设还是这张图,我想找到“abd”这个字符串应该要怎么找呢?
在这里插入图片描述
我们从根节点开始查找,依次向下查找,发现b的子节点根本就没有d节点,所以就将返回

如果查找“abc”,依照我们上文所说,我们插入的字符串中没有“abc”,但是肯定有小伙伴会疑惑,从根节点出发,从’a’到’b‘到’c‘不是可以找到abc吗?但是可惜的是,上面我们没有插入“abc”,所以以c节点的这个位置没有被标记,是不会查找到的。我们来看下面的代码

int query(char* str){
    int cur=0;//说明当前所在的节点是根节点
    for(int i=0;str[i]!='\0';i++){
        int u=str[i]-'a';
        if(!son[cur][u]){
        //如果当前节点的子节点没有该字母节点,就肯定没有出现过该字符串
        return 0;
        }
        cur=son[cur][u];//如果有节点,更新当前要处理的节点
    }
    return nums[cur];//返回以当前节点结尾的字符串数目
}

全部代码如下

#include<iostream>
#include<string>
using namespace std;
const int N=1e5+10;
int son[N][26];//son[i]代表是某节点,son[i][]表示son[i]的子节点,son[N][26]是指有N个节点,每个节点可能有26个子节点
int nums[N];//nums[i]表示以i节点结尾的字符串出现的次数
int idx;//表示如今用到第几个节点,因为没有节点时,需要创建节点
char str[N];
void insert(char* str){
    int cur=0;//说明当前所在的节点是根节点
    for(int i=0;str[i]!='\0';i++){
        //枚举字符
        int u=str[i]-'a';//将字母字符隐射到数字上,0-25
        if(!son[cur][u]){
            //当前节点的u所代表的字母没有节点,需要分配节点
            son[cur][u]=++idx;//++idx代表更新到还没用过的节点
        }
        cur=son[cur][u];//如果有节点了,更新当前要处理的节点
    }
    nums[cur]++;//当前节点结尾的字符串出现次数加一
}
int query(char* str){
    int cur=0;//说明当前所在的节点是根节点
    for(int i=0;str[i]!='\0';i++){
        int u=str[i]-'a';
        if(!son[cur][u]){
        //如果当前节点的子节点没有该字母节点,就肯定没有出现过该字符串
        return 0;
        }
        cur=son[cur][u];//如果有节点,更新当前要处理的节点
    }
    return nums[cur];//返回以当前节点结尾的字符串数目
}

int main(){
    int k;
    cin >> k;
    while(k--){
        string s;
        cin >> s;
        scanf("%s",str);
        if(s=="I"){
            insert(str);
        }
        else{
            cout << query(str)<<endl;
        }
    }
    return 0;
}

数字在trie中的存储

当用Trie树来存储数字时,一般是指存储数字的字符串表示形式,而不是直接存储数值本身。这是因为Trie树主要用于存储字符串集合,每个节点代表一个字符或者一部分字符串。

下面用一道题目来理解一下数字在trie中的存储

最大异或对
在这里插入图片描述
这道题怎么涉及trie树存储数据呢?
首先理解题目的意思,题目要求的是两个数异或运算的最大结果,那就涉及到二进制运算。

不知道的异或的小伙伴,我先来介绍一下异或的概念。
异或运算,通常用符号 ^ 表示,是一种逻辑运算符。它的定义如下:
如果两个操作数的二进制位相同,则结果为0,否则为1。

那么我们设想,如果将所有数据的二进制位存储在trie树中,再让某个数尽可能的和不同位的数异或,不就是最大的吗?
比如说1,2,3:
1的二进制表示为:0000…001
2的二进制表示为:0000…010
3的二进制表示为:0000…011
画个图模拟存储起来(因为有31个数,因为数据都是正数不看符号位,前面都是0,我这里就省略了哈)
在这里插入图片描述
我们对每个数转换成二进制和trie树存储进行异或运算,用1来举例,1的二进制是001,记住要尽力和自己位数不同的数进行异或,得出的结果最大。
第一位0只能跟0异或,这一位为0.
第二位0可以选择和0或1进行异或,我们选择不同位进形异或,这一位得到1
第三位1可以选择和0或1进行异或,我们选择不同位进形异或,这一位得到1
最终的结果是011,最终的答案也是011也就是三
在这里插入图片描述
如何将二进制转换成十进制呢,只需将有1的位移回已经偏移的量,再加回来就好了
比如说上面的第二位,第二位的1实际表示的是1*2^1也就是1<<2(位运算)
文字看起来太抽象,我们看代码,多次理解

插入函数:

//代码的细节下面有解释
void  insert(int x){
    int cur=0;
    for(int i=30;i>=0;i--){
        int u=x >> i & 1;
        if(!son[cur][u]) son[cur][u]=++idx;
        cur=son[cur][u];
    }
    
}

这个插入函数和上面字符串的插入一模一样,理解了上面的,这里也是一样的

这个for循环表示,数的31位二进制数(本来是32位,数据都是正数,不看符号位,所以只有31位),从最大位开始枚举,依次插进trie数

int u=x >> i & 1是什么意思呢?
&运算是指二进制位同为1才为1,有0就是0
x >> i &1 这个是想获取第i位的数字

举个例子:
3的二进制表示为000…011,向右移动1位,那么最低位是1,000…001
1的二进制为 000…01,前面无论是什么数,前面都是0,所以就要看最后一位的关系
3向右移动一位后最低位是1,1的最低位也是1,同为1便是1,所以3的第一位为1.

查找函数

int query(int x){
    int ans=0;//记录当前异或的值
    int cur=0;
    for(int i=30;i>=0;i--){
        int u=x >> i & 1;
        if(son[cur][!u]){
        //这点是指如果和u不同位的数存在,那么u就要和这个数异或
        ans+=1<<i;
        cur=son[cur][!u];// 更新节点,和u不同位的节点
        } 
        else{
            cur=son[cur][u];//相同位,异或为0,对异或结果无影响
        }
    }
    
    return ans;//返回该节点的最大异或的值
}

全部代码:

#include<iostream>
using namespace std;
const int N=1e5+10;
int son[N*31][2];
//这里要乘上31,因为题目说有10^5个数据,但是呢每个整数有31位二进制数,这里存放的是节点,存放的是二进制的一位,所以要乘31.
int arr[N];int idx;
void  insert(int x){
    int cur=0;
    for(int i=30;i>=0;i--){
        int u=x >> i & 1;
        if(!son[cur][u]) son[cur][u]=++idx;
        cur=son[cur][u];
    }
    
}
int query(int x){
    int ans=0;int cur=0;
    for(int i=30;i>=0;i--){
        int u=x >> i & 1;
        if(son[cur][!u]){
        ans+=1<<i;
        cur=son[cur][!u]; 
        } 
        else{
            cur=son[cur][u];
        }
    }
    
    return ans;
}
int main(){
    int k;int res=0;
    cin >> k;
    for(int i=0;i<k;i++) scanf("%d",&arr[i]);
    for(int i=0;i<k;i++){
        insert(arr[i]);//全部插入
    }
    for(int i=0;i<k;i++){
        res=max(res,query(arr[i]));//更新全部节点最大的异或值
    }
    cout << res;
    return 0;
}

不知道大家对trie树有没有更深入的了解呢,希望这篇文章能帮助到您,创作不易,如果您在这篇文章有收获的话,请您帮我点个赞吧!您们的支持是我创作的动力!如有错误,欢迎指正,有问题也可以在评论区里留言哦!感谢大家的观看。

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值