算法与数据结构-Trie树

文章目录

  • 什么是“Trie 树”?
  • 如何实现一棵 Trie 树?
  • Trie 树真的很耗内存吗?
  • Trie 树与散列表、红黑树的比较


什么是“Trie 树”?

Trie 树,也叫“字典树”。顾名思义,它是一个树形结构。它是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。

当然,这样一个问题可以有多种解决方法,比如散列表、红黑树,或者我们前面几节讲到的一些字符串匹配算法,但是,Trie 树在这个问题的解决上,有它特有的优点。不仅如此,Trie 树能解决的问题也不限于此,我们一会儿慢慢分析。

现在,我们先来看下,Trie 树到底长什么样子。

我举个简单的例子来说明一下。我们有 6 个字符串,它们分别是:how,hi,her,hello,so,see。我们希望在里面多次查找某个字符串是否存在。如果每次查找,都是拿要查找的字符串跟这 6 个字符串依次进行字符串匹配,那效率就比较低,有没有更高效的方法呢?

这个时候,我们就可以先对这 6 个字符串做一下预处理,组织成 Trie 树的结构,之后每次查找,都是在 Trie 树中进行匹配查找。Trie 树的本质,就是利用字符串之间的公共前缀,将重复的前缀合并在一起。最后构造出来的就是下面这个图中的样子。
在这里插入图片描述
其中,根节点不包含任何信息。每个节点表示一个字符串中的字符,从根节点到红色节点的一条路径表示一个字符串(注意:红色节点并不都是叶子节点)。

为了让你更容易理解 Trie 树是怎么构造出来的,我画了一个 Trie 树构造的分解过程。构造过程的每一步,都相当于往 Trie 树中插入一个字符串。当所有字符串都插入完成之后,Trie 树就构造好了。
在这里插入图片描述
在这里插入图片描述
当我们在 Trie 树中查找一个字符串的时候,比如查找字符串“her”,那我们将要查找的字符串分割成单个的字符 h,e,r,然后从 Trie 树的根节点开始匹配。如图所示,绿色的路径就是在 Trie 树中匹配的路径。

在这里插入图片描述
如果我们要查找的是字符串“he”呢?我们还用上面同样的方法,从根节点开始,沿着某条路径来匹配,如图所示,绿色的路径,是字符串“he”匹配的路径。但是,路径的最后一个节点“e”并不是红色的。也就是说,“he”是某个字符串的前缀子串,但并不能完全匹配任何字符串。
在这里插入图片描述

如何实现一棵 Trie 树?

知道了 Trie 树长什么样子,我们现在来看下,如何用代码来实现一个 Trie 树。

从刚刚 Trie 树的介绍来看,Trie 树主要有两个操作,一个是将字符串集合构造成 Trie 树。这个过程分解开来的话,就是一个将字符串插入到 Trie 树的过程。另一个是在 Trie 树中查询一个字符串。

了解了 Trie 树的两个主要操作之后,我们再来看下,如何存储一个 Trie 树?

从前面的图中,我们可以看出,Trie 树是一个多叉树。我们知道,二叉树中,一个节点的左右子节点是通过两个指针来存储的,如下所示 Java 代码。那对于多叉树来说,我们怎么存储一个节点的所有子节点的指针呢?

class BinaryTreeNode {char data;BinaryTreeNode left;BinaryTreeNode right;  
}

我先介绍其中一种存储方式,也是经典的存储方式,大部分数据结构和算法书籍中都是这么讲的。还记得我们前面讲到的散列表吗?借助散列表的思想,我们通过一个下标与字符一一映射的数组,来存储子节点的指针。这句话稍微有点抽象,不怎么好懂,我画了一张图你可以看看。
在这里插入图片描述
假设我们的字符串中只有从 a 到 z 这 26 个小写字母,我们在数组中下标为 0 的位置,存储指向子节点 a 的指针,下标为 1 的位置存储指向子节点 b 的指针,以此类推,下标为 25 的位置,存储的是指向的子节点 z 的指针。如果某个字符的子节点不存在,我们就在对应的下标的位置存储 null。

class TrieNode {char data;TrieNode children[26];
}

当我们在 Trie 树中查找字符串的时候,我们就可以通过字符的 ASCII 码减去“a”的 ASCII 码,迅速找到匹配的子节点的指针。比如,d 的 ASCII 码减去 a 的 ASCII 码就是 3,那子节点 d 的指针就存储在数组中下标为 3 的位置中。

描述了这么多,有可能你还是有点懵,我把上面的描述翻译成了代码,你可以结合着一块看下,应该有助于你理解。

public class Trie {private TrieNode root = new TrieNode('/'); // 存储无意义字符// 往Trie树中插入一个字符串public void insert(char[] text) {TrieNode p = root;for (int i = 0; i < text.length; ++i) {int index = text[i] - 'a';if (p.children[index] == null) {TrieNode newNode = new TrieNode(text[i]);p.children[index] = newNode;}p = p.children[index];}p.isEndingChar = true;}// 在Trie树中查找一个字符串public boolean find(char[] pattern) {TrieNode p = root;for (int i = 0; i < pattern.length; ++i) {int index = pattern[i] - 'a';if (p.children[index] == null) {return false; // 不存在pattern}p = p.children[index];}if (p.isEndingChar == false) return false; // 不能完全匹配,只是前缀else return true; // 找到pattern}public class TrieNode {public char data;public TrieNode[] children = new TrieNode[26];public boolean isEndingChar = false;public TrieNode(char data) {this.data = data;}}
}

Trie 树的实现,你现在应该搞懂了。现在,我们来看下,在 Trie 树中,查找某个字符串的时间复杂度是多少?

如果要在一组字符串中,频繁地查询某些字符串,用 Trie 树会非常高效。构建 Trie 树的过程,需要扫描所有的字符串,时间复杂度是 O(n)(n 表示所有字符串的长度和)。但是一旦构建成功之后,后续的查询操作会非常高效。

每次查询时,如果要查询的字符串长度是 k,那我们只需要比对大约 k 个节点,就能完成查询操作。跟原本那组字符串的长度和个数没有任何关系。所以说,构建好 Trie 树后,在其中查找字符串的时间复杂度是 O(k),k 表示要查找的字符串的长度。

Trie 树真的很耗内存吗?

前面我们讲了 Trie 树的实现,也分析了时间复杂度。现在你应该知道,Trie 树是一种非常独特的、高效的字符串匹配方法。但是,关于 Trie 树,你有没有听过这样一种说法:“Trie 树是非常耗内存的,用的是一种空间换时间的思路”。这是什么原因呢?

刚刚我们在讲 Trie 树的实现的时候,讲到用数组来存储一个节点的子节点的指针。如果字符串中包含从 a 到 z 这 26 个字符,那每个节点都要存储一个长度为 26 的数组,并且每个数组元素要存储一个 8 字节指针(或者是 4 字节,这个大小跟 CPU、操作系统、编译器等有关)。而且,即便一个节点只有很少的子节点,远小于 26 个,比如 3、4 个,我们也要维护一个长度为 26 的数组。

我们前面讲过,Trie 树的本质是避免重复存储一组字符串的相同前缀子串,但是现在每个字符(对应一个节点)的存储远远大于 1 个字节。按照我们上面举的例子,数组长度为 26,每个元素是 8 字节,那每个节点就会额外需要 26*8=208 个字节。而且这还是只包含 26 个字符的情况。

如果字符串中不仅包含小写字母,还包含大写字母、数字、甚至是中文,那需要的存储空间就更多了。所以,也就是说,在某些情况下,Trie 树不一定会节省存储空间。在重复的前缀并不多的情况下,Trie 树不但不能节省内存,还有可能会浪费更多的内存。

当然,我们不可否认,Trie 树尽管有可能很浪费内存,但是确实非常高效。那为了解决这个内存问题,我们是否有其他办法呢?

我们可以稍微牺牲一点查询的效率,将每个节点中的数组换成其他数据结构,来存储一个节点的子节点指针。用哪种数据结构呢?我们的选择其实有很多,比如有序数组、跳表、散列表、红黑树等。

假设我们用有序数组,数组中的指针按照所指向的子节点中的字符的大小顺序排列。查询的时候,我们可以通过二分查找的方法,快速查找到某个字符应该匹配的子节点的指针。但是,在往 Trie 树中插入一个字符串的时候,我们为了维护数组中数据的有序性,就会稍微慢了点。

实际上,Trie 树的变体有很多,都可以在一定程度上解决内存消耗的问题。比如,缩点优化,就是对只有一个子节点的节点,而且此节点不是一个串的结束节点,可以将此节点与子节点合并。这样可以节省空间,但却增加了编码难度。这里我就不展开详细讲解了,你如果感兴趣,可以自行研究下。

在这里插入图片描述

Trie 树与散列表、红黑树的比较

实际上,字符串的匹配问题,笼统上讲,其实就是数据的查找问题。对于支持动态数据高效操作的数据结构,我们前面已经讲过好多了,比如散列表、红黑树、跳表等等。实际上,这些数据结构也可以实现在一组字符串中查找字符串的功能。我们选了两种数据结构,散列表和红黑树,跟 Trie 树比较一下,看看它们各自的优缺点和应用场景。

在刚刚讲的这个场景,在一组字符串中查找字符串,Trie 树实际上表现得并不好。它对要处理的字符串有极其严苛的要求。

  • 第一,字符串中包含的字符集不能太大。我们前面讲到,如果字符集太大,那存储空间可能就会浪费很多。即便可以优化,但也要付出牺牲查询、插入效率的代价。
  • 第二,要求字符串的前缀重合比较多,不然空间消耗会变大很多。
  • 第三,如果要用 Trie 树解决问题,那我们就要自己从零开始实现一个 Trie 树,还要保证没有 bug,这个在工程上是将简单问题复杂化,除非必须,一般不建议这样做。
  • 第四,我们知道,通过指针串起来的数据块是不连续的,而 Trie 树中用到了指针,所以,对缓存并不友好,性能上会打个折扣。

综合这几点,针对在一组字符串中查找字符串的问题,我们在工程中,更倾向于用散列表或者红黑树。因为这两种数据结构,我们都不需要自己去实现,直接利用编程语言中提供的现成类库就行了。

实际上,Trie 树只是不适合精确匹配查找,这种问题更适合用散列表或者红黑树来解决。Trie 树比较适合的是查找前缀匹配的字符串,也就是类似开篇问题的那种场景。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

你真的会自动化吗?Web自动化测试-PO模式实战,一文通透...

前言 PO模式 Page Object(简称PO)模式&#xff0c;是Selenium实战中最为流行&#xff0c;并且是自动化测试中最为熟悉和推崇的一种设计模式。在设计自动化测试时&#xff0c;把页面元素和元素的操作方法按照页面抽象出来&#xff0c;分离成一定的对象&#xff0c;然后再进行组…

《DATASET CONDENSATION WITH GRADIENT MATCHING》

本文提出了一种用于数据效率学习的训练集合成技术&#xff0c;称为“数据集凝聚”(Dataset)&#xff0c;它学习将大数据集压缩成一个小的信息合成样本集&#xff0c;用于从头开始训练深度神经网络。我们将这个目标表述为在原始数据和合成数据上训练的深度神经网络权值的梯度之间…

提高效率!掌握项目管理工具中任务优先级的使用技巧

在项目管理中&#xff0c;我们经常会遇到一些具有强制依赖关系的任务。这些任务之间的关系是绝对的&#xff0c;并且毫无疑问必须首先完成什么。例如&#xff0c;您必须先设计一个产品&#xff0c;然后才能构建它&#xff0c;并且必须先构建它&#xff0c;然后才能绘制它。然而…

汽车电子——产品标准规范汇总和梳理(信息安全)

文章目录 前言 一、整车 二、充电接口 三、诊断接口 四、远程接口 五、实施指南 总结 前言 见《汽车电子——产品标准规范汇总和梳理》 一、整车 《GB/T 40861-2021 汽车信息安全通用技术要求》 《GB XXXXX—XXXX 汽车整车信息安全技术要求》 《GB/T 41871-2022 信息…

Goby 漏洞发布|Cockpit 平台 upload 文件上传漏洞(CVE-2023-1313)

漏洞名称&#xff1a;Cockpit 平台 upload 文件上传漏洞&#xff08;CVE-2023-1313&#xff09; English Name&#xff1a; Cockpit File Upload Vulnerability(CVE-2023-1313) CVSS core:7.2 影响资产数&#xff1a;3185 漏洞描述&#xff1a; Cockpit 是一个自托管、灵活…

基于Spring Boot的宠物咖啡馆平台的设计与实现

目录 前言 一、技术栈 二、系统功能介绍 用户信息管理 看护师信息管理 宠物寄养管理 健康状况管理 点单 宠物体验 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已…

需求堆积,如何排序产品优先极

面对堆积的产品需求&#xff0c;到底该如何排序优先极呢&#xff1f; 需求排期的目标 在谈具体的排期方法之前&#xff0c;有必要先探讨一下——合理的需求排期应该达到什么的目标呢&#xff1f;如果站在与项目相关的利益人员的角度来看&#xff0c;至少应该使以下四方面的收…

基于SpringBoot+Bootstrap的旅游管理系统的设计与实现

目录 前言 一、技术栈 二、系统功能介绍 登录模块的实现 景点信息管理界面 订票信息管理界面 用户评价管理界面 用户管理界面 景点资讯界面 系统主界面 用户注册界面 景点信息详情界面 订票信息界面 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言…

基于SpringBoot网上超市的设计与实现【附万字文档(LW)和搭建文档】

主要功能 前台登录&#xff1a; 注册用户&#xff1a;用户名、密码、姓名、联系电话 用户&#xff1a; ①首页、商品信息推荐、商品资讯、查看更多 ②商品信息、商品详情、评论、点我收藏、添加购物车、立即购买 ③个人中心、余额、点我充值、更新信息、我的订单、我的地址、我…

ubuntu 20 安装 CUDA

1. 查看需要安装的cuda版本 nvidia-smi cuda的版本信息如下图所示 2. 去官网下载对应版本的CUDA 官网&#xff1a;CUDA Toolkit Archive | NVIDIA Developer 弹出以下界面&#xff0c;依次点击以下按钮 得到以下内容&#xff1a; 复制下载链接&#xff0c;下载cuda11到本…

嵌入式Linux应用开发-基础知识-第十七章异常与中断的概念及处理流程

嵌入式Linux应用开发-基础知识-第十七章异常与中断的概念及处理流程 第十七章 异常与中断的概念及处理流程17.1 中断的引入17.1.1 妈妈怎么知道孩子醒了17.1.2 嵌入系统中也有类似的情况 17.2 中断的处理流程17.3 异常向量表17.4 参考资料 第十七章 异常与中断的概念及处理流程…

WPF绑定单变量Binding和绑定多变量MultiBinding 字符串格式化 UI绑定数据,数据变化自动更新UI,UI变化自动更新数据

UI绑定数据&#xff0c;数据变化自动更新UI&#xff0c;UI变化自动更新数据。 支持多设备&#xff0c;同时下载。 绑定单变量 在WPF (Windows Presentation Foundation) 中&#xff0c;您可以使用数据绑定来将变量绑定到界面元素。这允许您在界面上显示变量的值&#xff0c;…

CYEZ 模拟赛 7

A 弹珠 妙妙题。 先每个组分一个小球。等价于 n − k n-k n−k 拆分为任意个 [ 1 , k ] [1,k] [1,k] 的数的方案数。 本质是根据面积的转换&#xff0c;直观解释&#xff1a; 完全背包即可。代码。 B C 总结

深入理解传输层协议:TCP与UDP的比较与应用

目录 前言什么是TCP/UDPTCP/UDP应用TCP和UDP的对比总结 前言 传输层是TCP/IP协议栈中的第四层&#xff0c;它为应用程序提供服务&#xff0c;定义了主机应用程序之间端到端的连通性。在本文章&#xff0c;我们将深入探讨传输层协议&#xff0c;特别是TCP和UDP协议的原理和区别…

Cruise 建立自己的文件路径

每当建立一个模型&#xff0c;自然要将模型和相关文件存放在一个文件夹内&#xff0c;Cruise 软件自带的模型存放的目录在Cruise 软件的安装目录下面而且较深&#xff0c;找起来很不方便&#xff0c;通常情况下&#xff0c;我们会自己创建一个目录&#xff0c;然后指定这个目录…

【JAVA】飞机大战

代码和图片放在这个地址了&#xff1a; https://gitee.com/r77683962/fighting/tree/master 最新的代码运行&#xff0c;可以有两架飞机&#xff0c;分别通过WASD&#xff08;方向&#xff09;&#xff0c;F&#xff08;发子弹&#xff09;&#xff1b;上下左右&#xff08;控…

二十,镜面IBL--打印BRDF积分贴图

比起以往&#xff0c;这节应该是最轻松的了&#xff0c; 运行结果如下 代码如下&#xff1a; #include <osg/TextureCubeMap> #include <osg/TexGen> #include <osg/TexEnvCombine> #include <osgUtil/ReflectionMapGenerator> #include <osgDB/Re…

kafka环境搭建以及基本原理

kafka最先是作为日志数据采集&#xff0c;后用于消息传递&#xff0c;kafka能承担tb级别数据存储&#xff0c;确保服务的可用性&#xff0c;允许少量数据的丢失 作为消息中间件就有异步、解耦、削峰三个作用 一、单机搭建 单机ip&#xff1a;192.168.64.133 下载地址&#…

启动 React APP 后经历了哪些过程

本文作者为 360 奇舞团前端开发工程师 前言 本文中使用的React版本为18&#xff0c;在摘取代码的过程中删减了部分代码&#xff0c;具体以源代码为准。 在React 18里&#xff0c;通过ReactDOM.createRoot创建根节点。并且通过调用原型链上的render来渲染。 本文主要是从以下两个…

刘强东再次拿起低价武器,杀入这个万亿市场

京东的低价策略也要在汽车后市场打起来了&#xff1f; 9月26日&#xff0c;途虎养车于港交所挂牌上市当天&#xff0c;京东集团副总裁、京东零售汽车事业部总裁缪钦在朋友圈发文祝贺&#xff0c;同时表示京东养车“所有‘震虎价’商品都比友商低5%”。贺词与战书&#xff0c;同…