【数据结构与算法】Trie

news2024/11/18 20:27:08

😀大家好,我是白晨,一个不是很能熬夜😫,但是也想日更的人✈。如果喜欢这篇文章,点个赞👍,关注一下👀白晨吧!你的支持就是我最大的动力!💪💪💪

在这里插入图片描述

文章目录

  • 📘前言
  • 📙Trie
    • 🧧Trie的定义
    • 🎈Trie字符串统计
    • 🎆Trie数字储存
  • 📗后记

📘前言


大家好呀,我是白晨🕶️。又到了白晨日常摆烂,不定时更新的时间了😂。

img

本次要带大家认识的的是 Trie ,这是一个便于快速查询字符串等数据出现次数的数据结构,在算法竞赛中也是一个比较好用的结构。虽然使用 unordered_map 也能实现类似功能,但是 Trie 更加节省空间并且在存储数字等结构,它还有 unordered_map 无法代替的优势,这里先买个关子,在下面文章中我会详细讲解。


📙Trie


🧧Trie的定义


Trie字典树又叫前缀树(prefix tree),用以较快速地进行单词或前缀查询。Trie树本质上就是一棵多叉树,用来存储字符串或者其他数据。

  • 下图为一棵Trie树:

上图为一棵已经构建好的Trie树,观察可得,Trie树从按单词从左到右,从上到下构建一棵树,每层为一个字母。如果插入的单词有相同的前缀(前面为相同字母),那么相同的前缀只会出现一次,并且在相同前缀的最后一个字母分叉出不同子节点。

从根结点到叶结点就为一个单词,但是,如果我们插入 the 这个单词并且查找该单词,我们应该怎么办呢?

观察得,the 的前缀和 there 前缀无法区分,所以我们可以在每个结点上增设一个数cnt用于保存以该结点结尾的单词数量。

  • 查找操作:

上图为一个已经构建好的Trie树,例如,我们要查找 there 这个单词,从根节点开始,t这个子节点存在,跳转到t这个结点,继续查找t结点的子节点h,查找成功继续跳转,如果每个字符都有子节点并且在末尾字母e对应的结点的中cnt数据不为0,那么此节点存在。

如果为查找目标th,虽然每个结点都可以找到,但是结尾字母hcnt为0,此单词不存在。

  • 插入操作

image-20221226204154645

按照单词从左到右,层数从上到下插入字符即可,前缀相同的单词只会被插入一次(见下例)。

See the source image

接下来,我们用几道例题来讲解Trie树的实现以及使用。


🎈Trie字符串统计


image-20221226201219977

🍬原题链接:Trie字符串统计

🪅算法思想

按照字典树的结构进行插入和查询即可,主要注意实现。

具体实现见下面代码。

🪆代码实现

  • 树形实现

此种实现较为好理解,我们使用静态结点,每一个结点固定开辟26个子节点指针,方便存储子节点和维护数据结构。

// 使用树形类实现
#include <iostream>
#include <string>
#include <vector>

using namespace std;
// Trie树结点
struct Node
{
    Node()
        :cnt(0)
    {
        for (int i = 0; i < 26; ++i) son[i] = nullptr;
    }
    Node* son[26]; // 0~25 分别对应 a~z
    int cnt; // 以当前结点结尾的单词数量
}; 
// Trie树结构
class Trie
{
public:
    Trie()
    {
        head = new Node;
    }
	
    // 插入字符串
    void insert(const string& s)
    {
        // 当前遍历的结点
        Node* cur = head;
        for (int i = 0; i < s.size(); ++i)
        {
            // 得到当前字符对应的映射位置
            int pos = s[i] - 'a';
            // 没有子节点时,插入子节点
            if (cur->son[pos] == nullptr) cur->son[pos] = new Node;
            cur = cur->son[pos]; // 遍历到对应映射的结点
        }
        cur->cnt++; // 以当前结点为结尾的单词数++
    }

    int query(const string& s)
    {
        // 当前遍历的结点
        Node* cur = head;
        for (int i = 0; i < s.size(); ++i)
        {
            int pos = s[i] - 'a';
            // 没有子节点时,返回0
            if (cur->son[pos] == nullptr) return 0;
            cur = cur->son[pos];
        }
        return cur->cnt;
    }
private:
    Node* head;
};

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    Trie t;

    while (n--)
    {
        string op, s;
        cin >> op >> s;

        if (op == "I") t.insert(s);
        else cout << t.query(s) << endl;
    }
    return 0;
}
  • 模拟树形结构实现

这种结构比较抽象,是使用数组模拟树形结构,这种实现方法不易理解,但是代码较短,速度更快,更适合算法竞赛使用。

这里我们先来了解一下数组模拟单链表:

#include <iostream>

using namespace std;

const int N = 100010;

int v[N], ne[N]; // v数组存放结点值,ne存放下一个结点的下标
// 相当于把一个单链表结点拆开,前面v数组存储数据,后面ne结点存储子节点的指针
int head, idx; // head为头节点的下标,idx为当前可使用结点的下标
// idx是理解模拟单链表的重点,我们提前开辟了两个数组v和ne,这两个数组可以看为一个结点集合
// 我们应该如何使用结点集合呢?
// 我们可以选择顺序使用,先使用前面的结点,后使用后面的结点,如何确定我们使用到哪一个结点了呢?
// 我们需要一个idx存储当前使用到哪一个结点了,这样可以保证我们使用结点都是顺序使用的

// 初始化
void init()
{
    head = -1;
    idx = 0;
}

// 在头节点前插入结点
void add_to_head(int x)
{
    // idx为使用的新结点下标
    v[idx] = x;
    ne[idx] = head; // idx结点现在为头结点,所以它的子节点为以前的子节点
    head = idx++;// 头节点更新,idx转移到下一个待使用的结点
}

// 在下标为k的结点后面插入
void add(int k, int x)
{
    v[idx] = x;
    ne[idx] = ne[k]; // 新节点存储k结点原来的孩子
    ne[k] = idx++; // k结点的子节点改为idx结点,idx转移到下一个待使用的结点
}

// 删除下标为k节点后面的结点
// 我们是单链表,所以方便寻找k节点的子节点,不方便删除前驱节点
void remove(int k)
{
    ne[k] = ne[ne[k]]; // k节点的子节点指向k节点子节点的子节点
}

在上面模拟单链表的基础上再看模拟Trie树:

#include <iostream>
#include <string>

using namespace std;

const int N = 100010;

int Trie[N][26]; // Trie树,利用类似于单链表的方式模拟树形结构,Trie[i]就是一个结点,而Trie[i][26]为每个节点存储的子节点下标,相当于静态节点的26个子节点指针
int cnt[N]; // 统计以Trie树的第N个结点所代表的字母结尾的单词数量
int idx = 0; // 与单链表的idx类似,由于Trie树是利用二维数组模拟的,每一个Trie[i]为一个结点
// 所以要让二维数组表示出树形关系,就得让父节点指向子节点的下标,idx代表当前使用到了哪一个结点,每使用一个结点,idx++。
// 初始除了头结点(Trie[0])以外,其他结点都没有使用,所以idx = 0。
int n;

void insert(const string& s)
{
    int p = 0; // 从头结点开始遍历
    for (int i = 0; i < s.size(); ++i)
    {
        int pos = s[i] - 'a'; // 下标映射
        // 如果该字母没有被插入到当前结点下,将其插入(启用一个新节点,将新节点的下标保存到父节点)
        // 当Trie[p][pos] 的值为0时,代表当前位置没有子节点
        if (!Trie[p][pos]) Trie[p][pos] = ++idx;
        p = Trie[p][pos]; // 跳转到子节点,从子节点继续插入
    }
    cnt[p]++; // 以下标为p结点结尾的单词数量加1
}

int query(const string& s)
{
    int p = 0;
    for (int i = 0; i < s.size(); ++i)
    {
        int pos = s[i] - 'a';
        // 当Trie[p][pos] 的值为0时,代表当前位置没有子节点,也说明当前字母查找失败
        if (!Trie[p][pos]) return 0;
        p = Trie[p][pos]; // 有子节点,继续查找
    }
    return cnt[p];
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n;

    while (n--)
    {
        string op, s;
        cin >> op >> s;
        if (op == "I") insert(s);
        else cout << query(s) << endl;
    }
    return 0;
}

🎆Trie数字储存

image-20221226201937205

🍬原题链接:最大异或对

🪅算法思想

  • 类比字符串存储,一个int类型的整数为32位,规定存储的顺序为 数字二进制高位对应树中上层,低位对应树下层

以二进制为8位的数举例,存储如下图:

image-20221226205432864

  • 异或要获得最大值,两个数字二进制最好每一位都是相反的,这样就是全1,但是可能没有完美符合条件的数对,所以我们要找到高位尽量不同,低位可以相同的数对。

    翻译成Trie树的操作就是:确定一个数x,从高位开始遍历这个数的二进制,如果该节点存在和 该数二进制位逻辑取反的子节点对应的数 的子节点,那么跳转到该子节点继续重复上述逻辑;如果没有,跳转到存在的子节点继续按上述逻辑遍历。

🪆代码实现

  • 树形结构实现
#include <iostream>
#include <string>
#include <vector>

using namespace std;

struct Node
{
    Node()
    {
        for (int i = 0; i < 2; ++i) son[i] = nullptr;
    }
    Node* son[2];
};

class Trie
{
public:
    Trie()
    {
        head = new Node;
    }

    void insert(int x)
    {
        Node* cur = head;
        // 按位保存,高位为父节点
        for (int i = 30; i >= 0; --i)
        {
            int pos = x >> i & 1;
            if (!cur->son[pos]) cur->son[pos] = new Node;
            cur = cur->son[pos];
        }
    }

    int query(int x)
    {
        Node* cur = head;
        int ret = 0;
        // 按位保存,高位为父节点
        for (int i = 30; i >= 0; --i)
        {
            int pos = x >> i & 1;
            // 找尽量与x每一位相反的数
            if (cur->son[!pos])
            {
                ret += 1 << i;
                cur = cur->son[!pos];
            }
            else cur = cur->son[pos];
        }
        return ret;
    }
private:
    Node* head;
};

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    Trie t;
    vector<int> v(n);
    int ans = 0;

    for (int i = 0; i < n; ++i) cin >> v[i], t.insert(v[i]);
    for (int i = 0; i < n; ++i) ans = max(t.query(v[i]), ans);

    cout << ans << endl;

    return 0;
}
  • 模拟树形结构实现
// 模拟实现

#include <iostream>

using namespace std;

const int N = 100010;
const int M = N * 32; // 一个数要31个结点,为了保险,一个数算32个结点,最多要用 32 * N个结点

int Trie[M][2], idx = 0; // Trie[i][2]是因为每个节点的子节点只有 0 和 1 两种,每个节点开辟两个空间存储对应子节点即可
int a[N]; // 存储输入的数据

void insert(int x)
{
    int p = 0;
    for (int i = 30; i >= 0; --i)
    {
        int pos = x >> i & 1;
        if (!Trie[p][pos]) Trie[p][pos] = ++idx;
        p = Trie[p][pos];
    }
}

int query(int x)
{
    int p = 0, ret = 0;
    for (int i = 30; i >= 0; --i)
    {
        int pos = x >> i & 1;
        if (Trie[p][!pos])
        {
            ret += 1 << i;
            p = Trie[p][!pos];
        }
        else p = Trie[p][pos];
    }
    return ret;
}

int main()
{
    int n, ans = 0;
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) scanf("%d", &a[i]), insert(a[i]);
    for (int i = 0; i < n; ++i) ans = max(ans, query(a[i]));

    printf("%d", ans);
    return 0;
}

📗后记


如果想了解哈希算法在于字符串上的应用,可以参考【算法】哈希表这篇文章,不同于Trie对于字符串的存储,哈希算法在处理字符串前缀时又是另外一种思路,两个思路分别有不同的的独特用处。

哈希算法除了可以处理字符串前缀以外,还可以快速查找字符串,时间复杂度可以达到O(1),比Trie快的多。但是,Trie的思想是很重要的,未来我们将会在更复杂的数据结构和算法中常常看到Trie的思想,还请大家一定要体会Trie结构中的前缀思想。


如果解析有不对之处还请指正,我会尽快修改,多谢大家的包容。

如果大家喜欢这个系列,还请大家多多支持啦😋!

如果这篇文章有帮到你,还请给我一个大拇指 👍和小星星 ⭐️支持一下白晨吧!喜欢白晨【算法】系列的话,不如关注👀白晨,以便看到最新更新哟!!!

我是不太能熬夜的白晨,我们下篇文章见。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/157364.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

集合引用类型 下

目录 Map Map.set() Map.get() Map.delete() Map.has() Map.values() Map.entries() Map.clear() 选择Object 还是Map 数据转换 转为数组 转为 JSON 对象转为 Map 数组转为 Map 转为Object WeakMap 基本API 弱键 不可迭代 Set 创建Set实例 Set实例转数组 si…

STM32-启动文件详解

✅作者简介&#xff1a;嵌入式入坑者&#xff0c;与大家一起加油&#xff0c;希望文章能够帮助各位&#xff01;&#xff01;&#xff01;&#xff01; &#x1f4c3;个人主页&#xff1a;rivencode的个人主页 &#x1f525;系列专栏&#xff1a;玩转FreeRTOS &#x1f4ac;推荐…

python基础篇之数字类型(下)

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a;lqj_本人的博客_CSDN博客-微信小程序,前端,vue领域博主lqj_本人擅长微信小程序,前端,vue,等方面的知识https://blog.csdn.net/lbcyllqj?spm1000.2115.3001.5343 哔哩哔哩欢迎关注&…

在vue2使用百度脑图的kityminder-core进行二次开发思维导图,在源码中添加新的命令

需求说明&#xff1a;最近在搞kityminder-core的思维导图&#xff0c;需要增加一个给节点添加文件的功能&#xff0c;一直在研究源码&#xff0c;发现都是通过执行命令的方式实现的。一直卡在新增命令的步骤&#xff0c;搞了好多天了今天找到了如何在源码里新增命令&#xff0c…

leetcode 1807. 替换字符串中的括号内容【python3双指针+哈希表】实现过程分析以及思路整理

题目 给你一个字符串s&#xff0c;它包含一些括号对&#xff0c;每个括号中包含一个非空的键。 比方说&#xff0c;字符串"(name)is(age)yearsold"中&#xff0c;有两个括号对&#xff0c;分别包含键"name"和"age"。 你知道许多键对应的值&…

android实现侧边栏:解决header控件无法操作和底部menuitem点击无效的问题

1&#xff1a;目录结构&#xff1a;&#xff08;源码和总结都放在b站&#xff0c;链接在底部&#xff09; 2&#xff1a;实现的大概逻辑&#xff1a; 使用drawerlayout抽屉布局实现&#xff0c;并使用navigationview加载头部和底部 3&#xff1a;核心问题一&#xff1a;header…

用Python来创建7种不同的文件格式

用Python来创建7种不同的文件格式一、用Python来创建7种不同的文件格式1.1、文本文件1.2、CSV文件1.3、Excel文件1.4、压缩文件1.5、XML文件1.6、JSON文件1.7、PDF文件一、用Python来创建7种不同的文件格式 1.1、文本文件 写入 file_name "my_text_file.txt"# 将…

微信小程序——WXML模板语法-条件渲染,列表渲染

一.条件渲染1.wx:if在小程序中&#xff0c;使用wx:if"{{condition}}"来判断是否需要渲染该代码块&#xff1a;也可以用wx:elif和wx:else来添加else判断&#xff1a;实例如下&#xff1a;1.在js文件中定义一个typedata:{type:1 },此时虚拟页面上显示的就是&#xff1a…

二十四、Kubernetes中Deployment(Deploy)控制器详解

1、概述 在kubernetes中&#xff0c;有很多类型的pod控制器&#xff0c;每种都有自己的适合的场景&#xff0c;常见的有下面这些&#xff1a; ReplicationController&#xff1a;比较原始的pod控制器&#xff0c;已经被废弃&#xff0c;由ReplicaSet替代 ReplicaSet&#xff…

kafka/bin/kafka-run-class.sh: line 342: exec: java: not found

本来jps看了下&#xff0c;kafka和zookeeper都起来了&#xff0c;手痒&#xff0c;非要换宝塔的进程守护管理器&#xff0c;选目录为/home/kafka&#xff0c;命令为/home/kafka/bin/zookeeper-server-start.sh /home/kafka/config/zookeeper.properties 就在日志里看到 kafk…

马蹄集 整除的总数

整除的总数 难度&#xff1a;白银 时间限制&#xff1a;1秒 巴占用内存&#xff1a;64M 输入正整数N和M,其中N<M。求区间[N,M]中可被K整除的总数。 格式 输入格式&#xff1a;输入正整数N,M和K,空格分隔。 输出格式&#xff1a;输出整型 #include <bits/stdc.h&g…

RabbitMQ(二)使用Docker安装

目录1. 拉取 RabbitMQ 镜像2.启动 RabbitMQ 容器3.查看 RabbitMQ 是否启动官网地址&#xff1a;https://www.rabbitmq.com/ 下载地址&#xff1a;https://www.rabbitmq.com/download.html 这篇文章为了方便初学者入门&#xff0c;在 linux 环境下用 docker 直接安装 RabbitMQ&…

【JavaSE】String相关知识

String \ StringBuilder \ StringBufferString的值是不可变的&#xff0c;使用“”或者“”的方法尝试改变String的值并不是在原本的基础上修改&#xff0c;而是赋值给了新的字符串常量引用StringBuffer是线程安全的&#xff0c;使用的是无脑加synchronized的方法这三者的运行速…

10分钟上手一款好用的服务器节点监测工具(Server 酱)

Server 酱简介 Server酱&#xff0c;英文名「ServerChan」&#xff0c;是一款「手机」和「服务器」、「智能设备」之间的通信软件。说人话&#xff1f;就是从服务器、路由器等设备上推消息到手机的工具。开通并使用上它&#xff0c;只需要一分钟&#xff1a; 微信扫码登入设置…

Mysql可视化软件-Navicat和SQLyog

Navicat 可以将mysql可视化的一个软件 可以避免一直在命令行里面敲代码&#xff0c;很难绷 连接 密码写一个你能记住的&#xff0c;不然打不开连接 对应的IP可以是localhost或者127.0.0.1 都是本机 端口号就我们在my.ini写的那个 然后进行我们上面说的操作-新建一个数据库先…

Linux操作系统常用命令

✅作者简介&#xff1a;热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏&#xff1a;Java案例分…

【uniapp】记录地址管理页面

uniapp中的地址管理页面 <template><view class"container"><view class"oldaddress" v-for"(item,index) in cardInfo" :key"index"><view class"topview"><view class"name">{{i…

线缆行业单绞机控制算法(详细图解+代码)

在了解单绞机之前需要大家对收放卷以及排线控制有一定的了解,不清楚的可以参看下面几篇博客,这里不再赘述,受水平和能力所限,文中难免出现错误和不足之处,诚恳的欢迎大家批评和指正。 收放卷行业开环闭环控制算法 PLC张力控制(开环闭环算法分析)_RXXW_Dor的博客-CSDN博…

Win10专业版系统Docker安装、配置和使用详细教程

一、win10专业版系统首先需要开启硬件虚拟化及Hyper-V功能&#xff0c;才能进行Docker for Windows软件安装。 如何开启硬件虚拟化&#xff0c;自行百度。可在任务栏中查看虚拟化是否开启。 win10系统&#xff0c;打开控制面板-“应用”-“程序和功能”&#xff0c;开启Hyper-V…

webpack 的基本使用

webpack 的基本使用配置 npm 镜像源创建列表隔行变色案例新建空白项目目录&#xff0c;初始化 package.json 配置文件通过 npm 安装 jquery新建 src 源代码目录index.htmlindex.js检查网页效果webpack 的安装webpack 的安装dependencies 与 devDependencies参数 -S 及 --save参…