前缀树原理与代码详解

2024-09-02 01:20
文章标签 代码 详解 原理 前缀

本文主要是介绍前缀树原理与代码详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前置知识
  1. 了解什么是树结构,比如二叉树、多叉树。
  2. 了解为什么推荐静态数组的方式实现各种结构。【联想到静态链表,省时间省空间】
  3. 知道哈希表怎么用。【 O ( 1 ) O(1) O(1)复杂度】

前缀树又称为字典树,英文名 t r i e trie trie

每个样本都从头结点开始,根据前缀字符或者前缀数字建出来的一棵大树,就是前缀树。

**前缀树的使用场景:**一般都用在需要信息前缀的场景中。

**前缀树的优点:**利用前缀信息选择树上的分支,可以节省大量的时间。

前缀树的缺点: 前缀树需要大量的空间来存储树上的节点。并且前缀树上是【树枝】表示具体字符(待会看例子你就明白什么意思了)

前缀树的定制: pass,end 等信息。

前缀树的类实现

前缀树Trie类的相关函数:

  • struct TrieNode{}; :前缀树的某个节点。
  • class Trie{};:整个前缀树类对象。
  • void insert(string word) :向前缀树中插入字符串word
  • int serach(string word) :在前缀树中字符串word出现的次数。
  • int prefixNumber(string prefix) {} :以prefix为开头的字符串的个数(包含出现次数)。
  • void erase(string word) {} :在前缀树中删除字符串word

前缀树的类实现代码很简单,为节省篇幅,我只给重要代码,其余代码可以在评论区找我讨论。

class Trie {
private:struct TrieNode {int pass;int end;TrieNode* nexts[26];  // next为指针数组,存的是指向子节点的指针//大小为什么是26呢?因为英文字符只有26个,这就是之前提到的“前缀树所需空间与字符种类有关”的原因TrieNode() { //无参pass = 0;end = 0;for (int i = 0; i < 26; i++) {nexts[i] = nullptr;}}TrieNode(int p, int e) { //有pass,endpass = p;end = e;for (int i = 0; i < 26; i++) {nexts[i] = nullptr;}}TrieNode(TrieNode& copyNode) {  // 拷贝构造函数this->pass = copyNode.pass;this->end = copyNode.end;for (int i = 0; i < 26; i++) {this->nexts[i] = copyNode.nexts[i];}}};TrieNode root; //root是最上层一个节点,pass 和 end 都为0。public:Trie() {}  // 构造函数,声明一个前缀树对象时只需要一个无子节点的root即可void insert(string word);int serach(string word);int prefixNumber(string prefix) {}void erase(string word) {}
};
void Trie::insert(string word) {TrieNode temp = root;  // 当前节点for (int i = 0; i < word.size(); i++) {temp.pass++;int path = word[i] - 'a'; //字符相减,本质上是ACSII码相减if (temp.nexts[i] == nullptr) {  // 如果此时的path不在前缀树中,则新建节点temp.nexts[i] = new TrieNode(0, 0);}temp = *temp.nexts[i];}//循环结束时,temp指向word的最后一个字符temp.end++;
}int Trie::serach(string word) { //找word出现了几次,其实就是先找word,//再取word最后一个字符所对应节点的end
// 从根结点开始找TrieNode temp = root;//找的过程其实就是从word[0]开始往下走,看看有没有一直到word[n-1]的分支,最后返回该分支结尾节点的endfor (int i = 0;i < word.size(); i++) {int path = word[i] - 'a';if (temp.nexts[path] == nullptr) {//没有wordreturn 0;}temp = *temp.nexts[path];}return temp.end;
}

但是前缀树用类实现的话,工作效率并不高,接下来我们来看前缀树静态数组实现。

前缀树的静态数组实现
静态数组的优势

为什么提倡使用静态数组来实现前缀树呢,这得益于静态结构比较节约空间。利用动态结构实现的前缀树只适用于单个样例,更换样例之后,之前样例对应的前缀树根本用不了,只能销毁并新消耗一些空间给当前样例,如此反复下去,通过所有样例所需要的空间是巨大的。而静态数组只需要在定义时占用足够的空间,所有样例都可以在这片空间上建立前缀树。

前缀树静态数组实现示例

向前缀树中依次插入字符串“abc”“ac”“abb”。我们需要借助一个二维矩阵和两个一维数组: T r i e [ n ] [ k ] Trie[n][k] Trie[n][k] (k是字符的种类,在此示例中,k为3), p a s s [ n ] pass[n] pass[n] e n d [ n ] end[n] end[n] 以及最关键的空间计数器cnt

在最初时刻,未插入字符时, T r i e [ n ] [ k ] Trie[n][k] Trie[n][k]以及 p a s s [ n ] , e n d [ n ] pass[n],end[n] pass[n],end[n],cnt的状态如下:

在这里插入图片描述

实现代码

我这里先直接给出实际静态数组实现代码:

class Trie {
private://静态空间static const int n = INT_MAX;static const int k = 26; //k表示字符种类个数int trie[n][k];int pass[n];int end[n];int cnt;    //cnt表示申请的空间总数目,其实也就是节点数量
public:Trie() :cnt(0) {memset(trie, 0, sizeof(trie));memset(pass, 0, sizeof(pass));memset(end, 0, sizeof(end));}void build() {cnt = 1; //建树初始便有节点1}void insert(string word) {int cur = 1; //cur指向当前节点pass[cur - 1]++; //pass按照编号进行计数,即节点编号1,2,3,...for (int i = 0;i < word.size();i++) {int path = word[i] - 'a';if (trie[cur][path] == 0) { //无分支则新建分支trie[cur][path] = ++cnt;cur = cnt;pass[cur - 1]++;}else {cur = trie[cur][path]; //trie[cur][path]存储的是path分支节点的编号pass[cur - 1]++;}}end[cur - 1]++;}//查找的原理在类实现中已经讲过:找到最后一个节点的end即可。int search(string word) {int cur = 1;for (int i = 0;i < word.size();i++) {int path = word[i] - 'a';if (trie[cur][path] == 0)   return 0;cur = trie[cur][path];}return end[cur - 1];}int prefixNumber(string word) {int cur = 1;for (int i = 0;i < word.size();i++) {int path = word[i] - 'a';if (trie[cur][path] = 0) return 0;cur = trie[cur][path];}return pass[cur - 1];}void erase(string word) {if (search(word) == 0)   return;int cur = 1;pass[cur - 1]--;for (int i = 0;i < word.size();i++) {int path = word[i] - 'a';cur = trie[cur][path];pass[cur - 1]--;}end[cur - 1]--;}
};

插入字符串“abc”“ac”“abb”之后, T r i e [ n ] [ k ] Trie[n][k] Trie[n][k]以及 p a s s [ n ] , e n d [ n ] pass[n],end[n] pass[n],end[n],cnt的状态如下:

在这里插入图片描述

这篇关于前缀树原理与代码详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


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

相关文章

深入解析 Java Future 类及代码示例

《深入解析JavaFuture类及代码示例》JavaFuture是java.util.concurrent包中用于表示异步计算结果的核心接口,下面给大家介绍JavaFuture类及实例代码,感兴... 目录一、Future 类概述二、核心工作机制代码示例执行流程2. 状态机模型3. 核心方法解析行为总结:三

Nacos注册中心和配置中心的底层原理全面解读

《Nacos注册中心和配置中心的底层原理全面解读》:本文主要介绍Nacos注册中心和配置中心的底层原理的全面解读,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录临时实例和永久实例为什么 Nacos 要将服务实例分为临时实例和永久实例?1.x 版本和2.x版本的区别

Spring @RequestMapping 注解及使用技巧详解

《Spring@RequestMapping注解及使用技巧详解》@RequestMapping是SpringMVC中定义请求映射规则的核心注解,用于将HTTP请求映射到Controller处理方法... 目录一、核心作用二、关键参数说明三、快捷组合注解四、动态路径参数(@PathVariable)五、匹配请

git stash命令基本用法详解

《gitstash命令基本用法详解》gitstash是Git中一个非常有用的命令,它可以临时保存当前工作区的修改,让你可以切换到其他分支或者处理其他任务,而不需要提交这些还未完成的修改,这篇文章主要... 目录一、基本用法1. 保存当前修改(包括暂存区和工作区的内容)2. 查看保存了哪些 stash3. 恢

python获取cmd环境变量值的实现代码

《python获取cmd环境变量值的实现代码》:本文主要介绍在Python中获取命令行(cmd)环境变量的值,可以使用标准库中的os模块,需要的朋友可以参考下... 前言全局说明在执行py过程中,总要使用到系统环境变量一、说明1.1 环境:Windows 11 家庭版 24H2 26100.4061

java String.join()方法实例详解

《javaString.join()方法实例详解》String.join()是Java提供的一个实用方法,用于将多个字符串按照指定的分隔符连接成一个字符串,这一方法是Java8中引入的,极大地简化了... 目录bVARxMJava String.join() 方法详解1. 方法定义2. 基本用法2.1 拼接

Java中的record使用详解

《Java中的record使用详解》record是Java14引入的一种新语法(在Java16中成为正式功能),用于定义不可变的数据类,这篇文章给大家介绍Java中的record相关知识,感兴趣的朋友... 目录1. 什么是 record?2. 基本语法3. record 的核心特性4. 使用场景5. 自定

MyBatis编写嵌套子查询的动态SQL实践详解

《MyBatis编写嵌套子查询的动态SQL实践详解》在Java生态中,MyBatis作为一款优秀的ORM框架,广泛应用于数据库操作,本文将深入探讨如何在MyBatis中编写嵌套子查询的动态SQL,并结... 目录一、Myhttp://www.chinasem.cnBATis动态SQL的核心优势1. 灵活性与可

pandas实现数据concat拼接的示例代码

《pandas实现数据concat拼接的示例代码》pandas.concat用于合并DataFrame或Series,本文主要介绍了pandas实现数据concat拼接的示例代码,具有一定的参考价值,... 目录语法示例:使用pandas.concat合并数据默认的concat:参数axis=0,join=

Python struct.unpack() 用法及常见错误详解

《Pythonstruct.unpack()用法及常见错误详解》struct.unpack()是Python中用于将二进制数据(字节序列)解析为Python数据类型的函数,通常与struct.pa... 目录一、函数语法二、格式字符串详解三、使用示例示例 1:解析整数和浮点数示例 2:解析字符串示例 3:解