经典数据结构——前缀树

引言

前缀树——trie /ˈtraɪ//树,也叫作“单词查找树”、“字典树”。

它属于多叉树结构,典型应用场景是统计、保存大量的字符串,经常被搜索引擎系统用于文本词频统计。它的优点是利用字符串的公共前缀来减少查找时间,最大限度的减少无谓字符串的比较和存储空间。

trie 来自于 retrieval 的中间部分。在wiki百科中,有关于 trie 一词的由来:

Tries were first described by René de la Briandais in 1959.The term trie was coined two years later by Edward Fredkin, who pronounces it /ˈtriː/(as "tree"), after the middle syllable of retrieval.However, other authors pronounce it/ˈtraɪ/(as "try"), in an attempt to distinguish it verbally from "tree".

一、前缀树的逻辑结构

前缀树是一个由“路径”和“节点”组成多叉树结构。由根节点出发,按照存储字符串的每个字符,创建对应字符路径。

由“路径”记载字符串中的字符,由节点记载经过的字符数以及结尾字符结尾数,例如一个简单的记录了"abc"、"abd"、"bcf"、"abcd" 这四个字符串的前缀树如下图所示:

二、前缀树的添加过程

经典的前缀树都是以“路径”(以下简称路)记录字符的,由节点记录统计信息 pass 代表经过的字符个数,end代表有多少个字符串以这条路径结尾。

2.1 逻辑过程

前缀树添加字符串的过程,以 "abc"、"bcd"、"abcd"为例:

1、首先会有一个初始化的根节点 root :

2、添加 'a' 字符,根节点 pass++ , 由于字符串还没有结束,后面还有 'b'、'c',end 不变,并在路的另一端创建一个节点(因为两个节点才能形成一条路),并将新节点的 pass++,end 同样不变:

3、添加 'b' 字符,同样创建一个新的节点,来表示 b 的路,并在新节点上 pass++,end不变:

4、添加'c' 字符,创建一个新的节点,表示 c 的路,在新节点上 pass++,此时字符串已经结束,end++:

到此为止,就完成了'abc'字符串的添加,以同样的方法添加 'bcd'、'abcd',注意,每次添加都要从 root 开始,root 作为前缀树的第一个节点,其 pass 可以表示树中一共存储了多少字符串。前缀树的最大特点就是复用字符,如果从 root 没有可复用的前缀,那么就需要创建新的路径,如果有就需要复用已有路径,并标记经过的字符个数:

2.2 代码实现

前缀树的路是一种抽象结构,无法用具体的代码直接描述,在代码中,基于最简单的小写英文的前缀树,通常就是以一个数组表示 26 个字母的通道,以每个通道是否存在 Node 节点来表示到达这个Node节点的路是否存在:

private static class Node {public int pass;public int end;public Node[] nexts;public Node() {pass = 0;end = 0;// 26种可能nexts = new Node[26];}
}

那么添加的过程就是:

public class Code01_TrieTree {private Node root;public Code01_TrieTree() {root = new Node();}private static class Node {// ...}public void insert(String word) {if (word == null)return;char[] chars = word.toCharArray();Node node = root;// 以 root 出发node.pass++;// 初始化路径int path = 0;// 遍历字符数组for (int i = 0; i < chars.length; i++) {// 26个槽位与26个字母对应,'a'->0,字符相减就可以获得路径的偏移量path = chars[i] - 'a';// 计算出偏移量后判断是否存在节点,如果是 null 表示不存在这条路径if (node.nexts[path] == null)node.nexts[path] = new Node();// 指针移动到路径的尾节点node = node.nexts[path];// 路径的尾节点pass++node.pass++;}// 遍历完成后,node一定就是最后一个路径的尾节点,此时记录结尾数量node.end++;}
}

三、前缀树的查找

前缀树有两种常用查找,第一种是最普通的字符串出现的次数,第二种是某个字符串前缀出现的次数。

3.1 指定字符串出现次数

前缀树的查找思路是,先获取到 root 引用,由此出发。

遍历整个字符串,如果在遍历途中发现某个路径不存在(即路径的尾节点==null),则表示前缀树从未存储过该字符串。

经过遍历后,节点指针一定会来到最后一个字符路径的尾节点,这个节点的 end 记录了总共有多少个字符串以这个字符路径结尾,所以直接返回 end 即可,代码如下:

public int search(String word) {if (word == null)return 0;char[] chars = word.toCharArray();// 由 root出发的节点指针Node node = root;// 初始化路径int path = 0;// 遍历字符串for (int i = 0; i < chars.length; i++) {// 计算路径偏移path = chars[i] - 'a';// 路径不存在,表示该字符串不存在if (node.nexts[path] == null)return 0;// 移动节点指针node = node.nexts[path];}// 遍历结束后,节点指针来到字符串的尾节点,直接返回end统计值return node.end;
}

3.2 指定前缀出现次数

前缀查找统计的逻辑和字符串查找的逻辑几乎完全一样,唯一不同的是,在最后返回时,返回的是字符串尾节点的 pass 值,它代表有多少个字符串经过了这个节点。

public int searchPre(String pre) {if (pre == null) return 0;Node node = root;char[] chars = pre.toCharArray();int path = 0;for (int i = 0; i < chars.length; i++) {path = chars[i] - 'a';if (node.nexts[path] == null) {return 0;}node = node.nexts[path];}return node.pass;
}

四、前缀树的删除

前缀树的删除逻辑主体结构和插入、查找相同,都是遍历参数字符串,在途径的节点上分别 count down pass 和end 两个变量。

需要注意的两个点:

1、在开始执行真正的删除逻辑之前,一定要先调用 search 方法判断是否存在该字符串。

2、如果 node 的pass 属性-1 后是0,那么需要将节点引用置为 null,以便回收内存,同时也契合 insert、search等逻辑中 判断路径是否存在的方式。

public void delete(String word) {if (search(word) == 0)return;char[] chars = word.toCharArray();Node node = root;// 根节点 -1node.pass--;int path = 0;// 遍历字符串for (int i = 0; i < chars.length; i++) {// 计算路径偏移path = chars[i] - 'a';// 如果路径的尾节点 pass-1 后为 0,将这个尾节点置为 null,直接返回if (--node.nexts[path].pass == 0) {node.nexts[path] = null;return;}// 移动节点指针node = node.nexts[path];}// 遍历后沿途的 pass 都已经 -1,最后 end - 1node.end--;
}

五、以HashMap描述路径的前缀树

数组的适用场景较为局限,由于数组本身扩容不便,在实际的前缀树实现上,往往可以用HashMap作为路径的替代,这种方式还可以实现中文字符的存储。

public class Code02_TrieTree {private Node root;public Code02_TrieTree() {this.root = new Node();}private static class Node {public int pass;public int end;public HashMap<Integer, Node> nexts;public Node() {this.pass = 0;this.end = 0;this.nexts = new HashMap<>();}}public void insert(String word) {if (word == null || "".equals(word)) return;char[] chars = word.toCharArray();Node node = root;node.pass++;Integer path = 0;for (int i = 0; i < chars.length; i++) {// 以字符的ASCII码作为路径path = (int) chars[i];if (!node.nexts.containsKey(path))node.nexts.put(path, new Node());node = node.nexts.get(path);node.pass++;}node.end++;}public int search(String word) {if (word == null || "".equals(word)) return 0;char[] chars = word.toCharArray();Node node = root;Integer path = 0;for (int i = 0; i < chars.length; i++) {// 计算路径path = (int) chars[i];if (node.nexts.get(path) == null)return 0;node = node.nexts.get(path);}return node.end;}public int searchPre(String pre) {if (pre == null || "".equals(pre)) return 0;char[] chars = pre.toCharArray();Node node = root;Integer path = 0;for (int i = 0; i < chars.length; i++) {path = (int) chars[i];if (node.nexts.get(path) == null)return 0;node = node.nexts.get(path);}return node.pass;}public void delete(String word) {if (search(word) == 0) return;char[] chars = word.toCharArray();Node node = root;node.pass--;Integer path = 0;for (int i = 0; i < chars.length; i++) {path = (int) chars[i];if (--node.nexts.get(path).pass == 0) {node.nexts.remove(path);return;}node = node.nexts.get(path);}node.end--;}public int size() {return root.pass;}
}

六、前缀的时间复杂度

如果一个字符串长度为 k,那么插入前缀树需要的时间,就是 O(k)。

因为每个字符都需要比较一遍,在比较和计算路径的逻辑中,不论是数组实现的方式还是HashMap实现的方式,其路径的计算前者是使用相减得到的偏移量,后者是 containsKey 的 hashcode计算,都是常数时间复杂度。

而其余的代码步骤也都是常数时间复杂度,因此整个insert的过程就是 O(k)。

而查找的时间复杂度同样是 O(k),依然取决于字符串长度。

总结

前缀树是一种非常有用的字符串存储结构,它解决了像 HashMap 这种存储结构无法实现的问题——前缀统计,并且由于是复用节点,也很好的节约了存储空间。

它的增删逻辑是遍历参数字符串,找到对应路径,如果没有则创建节点。

路径是以节点是否存在的形式来抽象表示的。

在删除字符串的时候,一定要先判断是否存在该字符串,以免造成误操作途经节点的 pass属性。

对于 pass 属性 -1后为0,此时可以将这个节点置为null,并直接返回,这是因为 pass 如果 = 0,就表明 end 一定=0,反之 end =0,pass 不一定 =0。

插入、查询的大体流程是:

由根节点出发,通过参数中的每个字符,计算 path 路径,判断路径是否存在,如果是添加操作,就创建节点,如果是查询操作,就根据情况判断是否要直接返回结果。

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

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

相关文章

排序算法 —— 计数排序

引言 计数排序是桶排序思想的一种具体实现&#xff0c;针对一些具有特殊限制的样本数据&#xff0c;如公司员工年龄&#xff0c;那么样本数据本身就一定在0~200之间&#xff0c;针对这样的数据&#xff0c;使用从0到200 的桶数组&#xff0c;桶的位置已经是有序的&#xff0c;…

Java多线程 —— 线程状态迁移

引言 线程状态迁移&#xff0c;又常被称作线程的生命周期&#xff0c;指的是线程从创建到终结需要经历哪些状态&#xff0c;什么情况下会出现哪些状态。 线程的状态直接关系着并发编程的各种问题&#xff0c;本文就线程的状态迁移做一初步探讨&#xff0c;并总结在何种情况下…

Java中的Unsafe

Java和C语言的一个重要区别就是Java中我们无法直接操作一块内存区域&#xff0c;不能像C中那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C手动管理内存的能力。 Unsafe类&#xff0c;全限定名是sun.misc.Unsafe&#xff0c;从名字中我们可以看出来这个类对…

arm中断保护和恢复_浅谈ARM处理器的七种异常处理

昨天的文章&#xff0c;我们谈了ARM处理器的七种运行模式&#xff0c;分别是&#xff1a;用户模式User(usr)&#xff0c;系统模式System(sys)&#xff0c;快速中断模式(fiq)&#xff0c;管理模式Supervisor(svc)&#xff0c;外部中断模式(irq)&#xff0c;数据访问中止模式Abor…

Queue —— JUC 的豪华队列组件

目录引言一、Queue 的继承关系1.1 Queue 定义基础操作1.2 AbstractQueue 为子类减负1.3 BlockingQueue 阻塞式Queue1.4 Deque 两头进出二、Queue 的重要实现三、BlockingQueue 的实现原理四、Queue 在生产者消费者模式中的应用五、Queue 在线程池中的应用六、ConcurrentLinkedQ…

daad转换器实验数据_箔芯片电阻在高温应用A/D转换器中的应用

工业/应用领域高温&#xff1a;地震数据采集系统、石油勘探监测、高精度检测仪产品采用&#xff1a;V5X5 Bulk Metal (R) Foil芯片电阻案例介绍TX424是一个完整的4通道24位模数转换器&#xff0c;采用40脚封装。该设计采用最先进设计方案&#xff0c;两个双通道24位调节器和一个…

excel分段排序_学会这个神操作,报表填报不再五花八门,效率远超Excel

在报表工作人员的的日常工作中&#xff0c;常常要面临统计混乱的终端用户输入的问题。由于无法准确限制用户的输入内容&#xff0c;所以在最终进行数据统计时&#xff0c;常常会出现数据不合法的情况。为此需要花费大量的人力和时间核对校验数据。举个简单的例子&#xff0c;某…

IDEA——必备插件指南

目录一、Free-Mybatis-Plugin二、Lombok三、jclasslib Bytecode Viewer一、Free-Mybatis-Plugin 二、Lombok 三、jclasslib Bytecode Viewer 学习 class 文件的必备插件。 使用简单&#xff0c;安装后可以在菜单 View 中看到 show bytecode with jclasslib&#xff1a; 效果…

jitter 如何优化网络_如何做好关键词优化网络?

越来越多的传统企业开始建立自己的网站&#xff0c;进而不断的推广自己的产品。为了能够让自己的企业网站出现在搜索引擎的首页&#xff0c;现在最常用的手段就是竞价排名和关键词优化网络。往往很多企业会选择关键词优化网络这种方式来推广自己的网站&#xff0c;对于新手seoe…

python学生名片系统_Python入门教程完整版400集(懂中文就能学会)快来带走

如何入门Python&#xff1f;权威Python大型400集视频&#xff0c;学了Python可以做什么&#xff1f;小编今天给大家分享一套高老师的python400集视频教程&#xff0c;里面包含入门进阶&#xff0c;源码&#xff0c;实战项目等等&#xff0c;&#xff0c;不管你是正在学习中&…

JVM——详解类加载过程

导航一、过程概述二、Loading2.1 类加载器2.2 双亲委派机制2.3 类在内存中的结构三、Linking四、Initializing一、过程概述 java 源文件编译后会生成一个 .class文件存储在硬盘上。 在程序运行时&#xff0c;会将用到的类文件加载到 JVM 内存中。从磁盘到内存的过程总共分为三…

下载 Java 学习的权威文档

JVMS 和 JLS 文档的下载 快速直达&#xff1a; https://docs.oracle.com/javase/8/ --> Java Language and Virtual Machine Specifications jvm specification 和 java language specification 是Java 学习的两个最权威的文档。如果你用的是 Java 8&#xff0c;就可以去下载…

iso图像测试卡_4700万像素 五轴防抖 徕卡正式发布SL2无反相机

出自蜂鸟网-器材频道&#xff0c;原文链接&#xff1a;https://m.fengniao.com/document/5358989.html徕卡于今日正式发布SL2相机&#xff0c;搭载4700万像素CMOS感光元件、通过感光元件移位实现光学图像稳定的五轴防抖技术、全新徕卡物距探测式自动对焦技术以及576万像素分辨率…

JVM——对象的创建与内存布局

导航一、对象的创建过程二、对象的内存布局2.1 内存布局2.2 计算对象的内存大小三、对象的定位3.1 句柄池3.2 直接指针四、对象的分配过程一、对象的创建过程 对象&#xff0c;又叫实例&#xff0c;是 OOP 的最常用角色。 如何创建一个对象&#xff1f;一般都是使用 new 关键…

JVM垃圾收集器——G1

导航引言一、G1 介绍1.1 适用场景1.2 设计初衷1.3 关注焦点1.4 工作模式1.5 堆的逻辑结构1.6 主要收集目标1.7 停顿预测模型1.8 拷贝和压缩1.9 与 CMS 和 Parallel 收集器的比较1.10 固定停顿目标二、堆的逻辑分区2.1 region2.2 CSet2.3 RSet2.4 Card Table三、G1 的工作原理3.…

的mvc_简述PHP网站开发的MVC模式

为了提高开发时候的代码重用和开发速度&#xff0c;php使用了mvc的模式&#xff0c;主要是对代码的功能进行了分类&#xff0c;M&#xff1a;model主要是对数据库进行操作&#xff0c;v&#xff1a;view主要是前端html文件操作&#xff0c;c&#xff1a;controller主要是编写基…

CAP 原则与 BASE 理论

导航引言一、CAP 原则1.1 Consistency 一致性1.2 Available 可用性1.3 Partition tolerance 分区容错性1.4 CAP 的矛盾1.5 CAP 的组合场景二、BASE 理论2.1 基本可用2.2 软状态2.3 最终一致性2.3.1 因果一致性2.3.2 读自身所写2.3.3 会话一致性2.3.4 单调读一致性2.3.5 单调写一…

java 教室借用管理系统_[内附完整源码和文档] 基于JAVA语言的学生选课信息管理系统...

摘 要本系统运用Java面向对象的方法设计而成。近年来&#xff0c;学生选课系统越来越在高校学生群体中得到普及&#xff0c;其所承担的功能也变得越来越丰富&#xff0c;所起到的作用也变得越来越重要&#xff0c;在被学校学生重视的同时&#xff0c;也意味着它的功能要更加完善…

jMeter 模拟 web 高并发请求

导航一、jmeter 简介与下载二、接口压测设置三、实战演示一、jmeter 简介与下载 Apache JMeter是Apache组织开发的基于Java的压力测试工具。 最初被设计用于Web应用测试&#xff0c;但后来扩展到其他测试领域。JMeter 可以用于对服务器、网络或对象模拟巨大的负载&#xff0c…

实施文档_建设工程监理全套资料范本,Word文档附百份案例表格,超实用

建设工程监理全套资料范本&#xff0c;Word文档附百份案例表格&#xff0c;超实用在日常工作中&#xff0c;监理人员不仅需要经常跑腿儿检查&#xff0c;同时还需要提交许许多多的资料存档&#xff0c;甚至可能需要熬夜码字。今天整理的监理资料范本&#xff0c;既能让监理人员…