前缀树原理与代码详解

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

相关文章

C++11范围for初始化列表auto decltype详解

《C++11范围for初始化列表autodecltype详解》C++11引入auto类型推导、decltype类型推断、统一列表初始化、范围for循环及智能指针,提升代码简洁性、类型安全与资源管理效... 目录C++11新特性1. 自动类型推导auto1.1 基本语法2. decltype3. 列表初始化3

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、

SQL Server 中的 WITH (NOLOCK) 示例详解

《SQLServer中的WITH(NOLOCK)示例详解》SQLServer中的WITH(NOLOCK)是一种表提示,等同于READUNCOMMITTED隔离级别,允许查询在不获取共享锁的情... 目录SQL Server 中的 WITH (NOLOCK) 详解一、WITH (NOLOCK) 的本质二、工作

springboot自定义注解RateLimiter限流注解技术文档详解

《springboot自定义注解RateLimiter限流注解技术文档详解》文章介绍了限流技术的概念、作用及实现方式,通过SpringAOP拦截方法、缓存存储计数器,结合注解、枚举、异常类等核心组件,... 目录什么是限流系统架构核心组件详解1. 限流注解 (@RateLimiter)2. 限流类型枚举 (

Java Thread中join方法使用举例详解

《JavaThread中join方法使用举例详解》JavaThread中join()方法主要是让调用改方法的thread完成run方法里面的东西后,在执行join()方法后面的代码,这篇文章主要介绍... 目录前言1.join()方法的定义和作用2.join()方法的三个重载版本3.join()方法的工作原

Spring AI使用tool Calling和MCP的示例详解

《SpringAI使用toolCalling和MCP的示例详解》SpringAI1.0.0.M6引入ToolCalling与MCP协议,提升AI与工具交互的扩展性与标准化,支持信息检索、行动执行等... 目录深入探索 Spring AI聊天接口示例Function CallingMCPSTDIOSSE结束语

C语言进阶(预处理命令详解)

《C语言进阶(预处理命令详解)》文章讲解了宏定义规范、头文件包含方式及条件编译应用,强调带参宏需加括号避免计算错误,头文件应声明函数原型以便主函数调用,条件编译通过宏定义控制代码编译,适用于测试与模块... 目录1.宏定义1.1不带参宏1.2带参宏2.头文件的包含2.1头文件中的内容2.2工程结构3.条件编

PyTorch中的词嵌入层(nn.Embedding)详解与实战应用示例

《PyTorch中的词嵌入层(nn.Embedding)详解与实战应用示例》词嵌入解决NLP维度灾难,捕捉语义关系,PyTorch的nn.Embedding模块提供灵活实现,支持参数配置、预训练及变长... 目录一、词嵌入(Word Embedding)简介为什么需要词嵌入?二、PyTorch中的nn.Em

Python Web框架Flask、Streamlit、FastAPI示例详解

《PythonWeb框架Flask、Streamlit、FastAPI示例详解》本文对比分析了Flask、Streamlit和FastAPI三大PythonWeb框架:Flask轻量灵活适合传统应用... 目录概述Flask详解Flask简介安装和基础配置核心概念路由和视图模板系统数据库集成实际示例Stre

Spring Bean初始化及@PostConstruc执行顺序示例详解

《SpringBean初始化及@PostConstruc执行顺序示例详解》本文给大家介绍SpringBean初始化及@PostConstruc执行顺序,本文通过实例代码给大家介绍的非常详细,对大家的... 目录1. Bean初始化执行顺序2. 成员变量初始化顺序2.1 普通Java类(非Spring环境)(