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

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.xdnf.cn/news/19324.html

相关文章:

  • 人工智能加速漏洞利用,15分钟即可完成概念验证?
  • 神州数码VRRP 原理与配置篇
  • 应用开发使用缓存
  • macos调用chrome后台下载wasm-binaries.tar.xz
  • 对于牛客网—语言学习篇—编程初学者入门训练—复合类型:BC136 KiKi判断上三角矩阵及BC139 矩阵交换题目的解析
  • Nginx四层负载均衡实战指南
  • 基于 YOLOv11n 的无人机航拍小目标检测算法学习
  • 鸿蒙搭配前端开发:应用端与WEB端交互
  • Go学习1:常量、变量的命名
  • 2025.8.31基于UDP的网络聊天室项目
  • LangChain Prompt管理核心:PromptTemplate与ChatPromptTemplate全解析
  • JVM学习总结
  • shell脚本(略)
  • KMP 算法相关练习题
  • 7.0elementplus布局容器详解
  • WebSocket的基本使用方法
  • 让你的App与众不同打造独特品牌展示平台
  • C语言学习笔记(自取)
  • 从新能源汽车看产品逻辑与认知系统
  • QML Chart组件之图例
  • 根据Excel数据表快速创建Word表格(标签)
  • 后向投影合成孔径辐射源定位方法(一)
  • 基于计算机视觉的海底图像增强系统:技术详述与实现
  • VMWare Tools (Linux)安装教程
  • 性能优化三剑客:`memo`, `useCallback`, `useMemo` 详解
  • Java异常处理:掌握优雅捕获错误的艺术
  • 远程管理协议telnet、ssh
  • 智能制造——解读装备制造业智能工厂解决方案【附全文阅读】
  • 【IO学习】IO基础和标准IO函数
  • 拉长视频时长的两种方法