前缀树原理与代码详解

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

相关文章

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

poj 1258 Agri-Net(最小生成树模板代码)

感觉用这题来当模板更适合。 题意就是给你邻接矩阵求最小生成树啦。~ prim代码:效率很高。172k...0ms。 #include<stdio.h>#include<algorithm>using namespace std;const int MaxN = 101;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int n

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能

K8S(Kubernetes)开源的容器编排平台安装步骤详解

K8S(Kubernetes)是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。以下是K8S容器编排平台的安装步骤、使用方式及特点的概述: 安装步骤: 安装Docker:K8S需要基于Docker来运行容器化应用程序。首先要在所有节点上安装Docker引擎。 安装Kubernetes Master:在集群中选择一台主机作为Master节点,安装K8S的控制平面组件,如AP

代码随想录冲冲冲 Day39 动态规划Part7

198. 打家劫舍 dp数组的意义是在第i位的时候偷的最大钱数是多少 如果nums的size为0 总价值当然就是0 如果nums的size为1 总价值是nums[0] 遍历顺序就是从小到大遍历 之后是递推公式 对于dp[i]的最大价值来说有两种可能 1.偷第i个 那么最大价值就是dp[i-2]+nums[i] 2.不偷第i个 那么价值就是dp[i-1] 之后取这两个的最大值就是d