当前位置: 首页 > news >正文

Trie树(静态数组实现)

代码实现了一个功能完备的字典树,包括了核心的插入、查询、删除以及查询前缀数量的功能。代码风格是典型的算法竞赛(Competitive Programming)风格,通过静态全局数组来管理内存,简洁高效,但也有其特定的使用场景和局限性。


1. 核心数据结构 (私有成员变量)

private:static const int MAX = 1e5 + 7;  // 最大节点数static int tree[MAX][26];       // 核心数组,存储节点关系static int end[MAX];            // 记录以某个节点结尾的单词数量static int pass[MAX];           // 记录经过某个节点的单词数量static int cnt;                 // 节点计数器,用于分配新节点

这段代码定义了字典树所需的所有数据。这里的关键是 static 关键字。

  • static 的含义与影响

    • static 成员变量意味着它们不属于任何一个 Trie 对象实例,而是被所有 Trie 对象共享。整个程序中,无论你创建多少个 Trie 对象(例如 Trie t1, t2;),它们都将使用同一套 tree, end, pass 数组和 cnt 计数器。
    • 优点:在算法竞赛中,通常一个问题只需要一个字典树,这样做可以方便地管理全局唯一的资源,避免了通过参数传来传去的麻烦。
    • 缺点:这不是一个真正面向对象的设计。如果你想在程序中同时维护多个独立的字典树,这种实现方式就不行了,因为它们会互相干扰。
  • 各个数组的含义

    • tree[MAX][26]:这是字典树的核心。tree[u][c] 表示从节点 u 出发,沿着代表字符 c (0-25 对应 ‘a’-‘z’) 的边,应该到达哪个节点。如果 tree[u][c] = 0,则表示这条路不存在。
    • pass[i]:记录有多少个字符串经过了节点 i。换句话说,有多少个字符串的前缀是以节点 i 所代表的路径结尾的。
    • end[i]:记录有多少个字符串完整地结束在节点 i
    • cnt:一个全局计数器,用来给新节点分配唯一的编号。代码中从 1 开始作为根节点。

2. 类外静态成员的定义和初始化

int Trie::tree[Trie::MAX][26] = {0};
int Trie::end[Trie::MAX] = {0};
int Trie::pass[Trie::MAX] = {0};
int Trie::cnt = 1;

这是 C++ 的语法要求。static 成员变量必须在类外进行定义和初始化。这里将所有数组初始化为 0,并将节点计数器 cnt 初始化为 1。这意味着节点编号从 1 开始,我们通常把节点 1 当作字典树的根节点(root)0 在这里被用作一个特殊值,表示“空”或“不存在的节点”。


3. 方法详解 (成员函数)

Trie() - 构造函数
public:Trie() {// 初始化}

构造函数是空的。因为真正的初始化工作已经在类外的静态成员定义中完成了。如果你想在每次创建对象时重置字典树,应该调用 clear() 方法。

insertWord(const string& word) - 插入字符串
void insertWord(const string& word) {int cur = 1; // 从根节点1开始pass[cur]++; // 根节点的pass值加1int path = 0;for(int i = 0; i < word.size(); i++) {path = word[i] - 'a'; // 计算字符对应的路径编号if(tree[cur][path] == 0) { // 如果这条路不存在tree[cur][path] = ++cnt; // 创建一个新节点,并连接起来}cur = tree[cur][path]; // 移动到下一个节点pass[cur]++; // 经过这个新节点的字符串数加1}end[cur]++; // 循环结束,当前cur在单词的末尾节点,end值加1
}

这是字典树最核心的操作。

  1. 从根节点 cur = 1 开始。
  2. 遍历单词的每一个字符。
  3. 对于每个字符,计算它对应的路径('a' -> 0, 'b' -> 1, …)。
  4. 检查 tree[当前节点][路径] 是否存在。
    • 如果不存在(值为0),说明这是第一次遇到这个前缀,需要创建一个新节点。新节点的编号由 ++cnt 分配。
    • 如果存在,就直接沿着路走下去。
  5. 每经过一个节点(包括根节点和新创建的节点),都将该节点的 pass 值加 1。
  6. 当整个单词遍历完成后,当前所在的节点 cur 就是该单词的结尾节点,将其 end 值加 1。
searchWord(const string& word) - 查询单词数量
int searchWord(const string& word) {int cur = 1; // 从根节点开始int path = 0;for(int i = 0; i < word.size(); i++) {path = word[i] - 'a';if(tree[cur][path] == 0) return 0; // 如果中途路断了,说明单词不存在cur = tree[cur][path]; // 继续往下走}return end[cur]; // 返回最终节点的end值
}

这个函数用于查找一个完整的单词在字典树中出现了多少次。

  1. 从根节点 cur = 1 开始。
  2. 沿着单词的字符路径往下走。
  3. 如果在任何一步发现路不存在 (tree[cur][path] == 0),说明这个单词肯定没有被插入过,直接返回 0
  4. 如果成功走完了整个单词的路径,到达了结尾节点 cur,就返回 end[cur] 的值。这个值表示该单词被完整插入的次数。
prefixNumber(const string& pre) - 查询前缀数量
int prefixNumber(const string& pre) {// ... (代码逻辑和 searchWord 几乎一样) ...return pass[cur]; // 唯一的区别是返回 pass[cur]
}

这个函数用于查找以 pre 为前缀的单词有多少个。
逻辑和 searchWord 完全一样,都是沿着前缀的路径往下走。唯一的区别是,当走完前缀路径并到达节点 cur 后,它返回的是 pass[cur]。根据 pass 的定义,pass[cur] 正是所有经过该节点(即拥有该前缀)的字符串总数。

deleteWord(const string& word) - 删除单词
void deleteWord(const string& word){if(searchWord(word) > 0){ // 先确认单词存在int cur = 1;int path = 0;for(int i = 0; i < word.size(); i++){path = word[i] - 'a';// 如果下一个节点的pass值减1后为0,说明没有其他单词共用这条边了if(--pass[tree[cur][path]] == 0){tree[cur][path] = 0; // 直接把路断掉(剪枝)return; // 后面的节点可以不用管了,因为已经没有路径能到达它们}cur = tree[cur][path]; // 移动到下一个节点}end[cur]--; // 单词结尾节点的end值减1}
}

删除操作相对复杂一些。

  1. 首先调用 searchWord 确认要删除的单词确实存在。
  2. 从根节点开始,沿着单词路径向下走。
  3. 在每一步移动之前,先将下一个节点pass 值减 1。
  4. 关键的剪枝操作:如果在减 1 后,下一个节点的 pass 值变成了 0,这意味着除了当前要删除的这个单词外,再也没有任何其他单词会经过这个节点了。因此,我们可以安全地把这条路径 tree[cur][path] 直接设为 0(相当于删除了从这个节点开始的后续所有分支),然后直接返回。
  5. 如果 pass 值不为 0,说明还有其他单词共享这个前缀,不能删除路径,只能继续往下走。
  6. 走完整个单词路径后,将结尾节点的 end 值减 1。
clear() - 清空字典树
void clear(){for(int i = 0;i <= cnt;i ++){pass[i] = 0;end[i] = 0;for(int j = 0;j < 26;j ++){tree[i][j] = 0;}}// 别忘了重置 cntcnt = 1; 
}

这个函数用于重置整个字典树的状态,以便重新使用。它遍历所有已经使用过的节点(从0到cnt),将它们的 passend 值和所有出边(路径)都清零。 注意:原代码中没有重置 cnt = 1;,这是一个小缺陷,我已经在这里补充上了。 如果不重置 cnt,下一次使用时节点编号会接着上次的继续增长,可能会很快超出 MAX 的限制。


4. main 函数

int main(){Trie trie;trie.insertWord("hello");cout << trie.searchWord("hello"); // 输出应该是 1return 0;
}

主函数是一个简单的示例,创建了一个 Trie 对象,插入了 “hello”,然后查询 “hello” 并打印其出现次数。

完整代码

#include <iostream>
#include <string>
using namespace std;class Trie {
private:static const int MAX = 1e5 + 7;  // 使用static conststatic int tree[MAX][26];static int end[MAX];static int pass[MAX];static int cnt;public:Trie() {//}void insertWord(const string& word) {int cur = 1;pass[cur] ++;int path = 0;for(int i = 0;i < word.size();i ++){path = word[i] - 'a';if(tree[cur][path] == 0){tree[cur][path] = ++cnt;}cur = tree[cur][path];pass[cur] ++;}end[cur] ++;}int searchWord(const string& word) {int cur = 1;int path = 0;for(int i = 0;i < word.size();i ++){path = word[i] - 'a';if(tree[cur][path] == 0)return 0;cur = tree[cur][path];}return end[cur];}int prefixNumber(const string& pre) {int cur = 1;int path = 0;for(int i = 0;i < pre.size();i ++){path = pre[i] - 'a';if(tree[cur][path] == 0)return 0;cur = tree[cur][path];}return pass[cur];}void deleteWord(const string& word){if(searchWord(word) > 0){int cur = 1;int path = 0;for(int i = 0;i < word.size();i ++){path = word[i] - 'a';if(--pass[tree[cur][path]] == 0){tree[cur][path] = 0;return;}cur = tree[cur][path];}end[cur] --;}}void clear(){for(int i = 0;i <= cnt;i ++){pass[i] = 0;end[i] = 0;for(int j = 0;j < 26;j ++){tree[i][j] = 0;}}cnt = 1;}
};// 类外定义静态成员
int Trie::tree[Trie::MAX][26] = {0};
int Trie::end[Trie::MAX] = {0};
int Trie::pass[Trie::MAX] = {0};
int Trie::cnt = 1;int main(){Trie trie;trie.insertWord("hello");cout << trie.searchWord("hello");return 0;
}
http://www.dtcms.com/a/360360.html

相关文章:

  • 云渲染如何重新定义视觉艺术的边界
  • JS接口请求的基本方法
  • FastAPI 核心实战:精通路径参数、查询参数与数据交互
  • 第25章学习笔记|额外的提示、技巧与技术(PowerShell 实战版)
  • 蓓韵安禧活性叶酸源于上市企业生产
  • 网站漏洞早发现:cpolar+Web-Check安全扫描组合解决方案
  • 5w2h构建数据仓库与sow
  • H264几个参数说明
  • 大话 IOT 技术(4) -- 答疑篇
  • [光学原理与应用-355]:ZEMAX - 设置 - 系统检查与系统测试
  • k8s三阶段项目
  • 物理气相沉积(PVD)技术及应用现状和发展趋势
  • FreeRTOS深入理解
  • 数据库索引abc,请问查询哪些字段能命中索引
  • 平滑滤波器(Smooth Filter)的MATLAB与Verilog仿真设计与实现
  • 关于Ctrl+a不能全选的问题
  • 封装哈希表
  • 机器视觉opencv教程(四):图像颜色识别与颜色替换
  • 【开题答辩全过程】以 基于SpringBoot的流浪猫狗领养系统为例,包含答辩的问题和答案
  • C语言中如何使用NULL
  • 【Linux】系统部分——ELF文件格式与动态库加载
  • Asible管理变量和事实和实施任务控制
  • 科学研究系统性思维的方法体系:研究设计相关模版
  • 【Unity3D实例-功能-切换武器】切换武器(一)动画配置
  • IAR 用JLINK 下载代码设置步骤
  • FLEXPART 拉格朗日粒子扩散模式建模技术及研究大气污染物源-汇关系中的实践
  • 订餐后台管理系统-day06菜品分类模块
  • 工业软件领域SAAS模式为何不能得到普及?
  • 上海交大具身导航中的感知智能、社会智能和运动智能全面综述
  • 网络端口与服务对应表 - 白帽子安全参考指南