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
}
这是字典树最核心的操作。
- 从根节点
cur = 1
开始。 - 遍历单词的每一个字符。
- 对于每个字符,计算它对应的路径(
'a'
-> 0,'b'
-> 1, …)。 - 检查
tree[当前节点][路径]
是否存在。- 如果不存在(值为0),说明这是第一次遇到这个前缀,需要创建一个新节点。新节点的编号由
++cnt
分配。 - 如果存在,就直接沿着路走下去。
- 如果不存在(值为0),说明这是第一次遇到这个前缀,需要创建一个新节点。新节点的编号由
- 每经过一个节点(包括根节点和新创建的节点),都将该节点的
pass
值加 1。 - 当整个单词遍历完成后,当前所在的节点
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值
}
这个函数用于查找一个完整的单词在字典树中出现了多少次。
- 从根节点
cur = 1
开始。 - 沿着单词的字符路径往下走。
- 如果在任何一步发现路不存在 (
tree[cur][path] == 0
),说明这个单词肯定没有被插入过,直接返回0
。 - 如果成功走完了整个单词的路径,到达了结尾节点
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}
}
删除操作相对复杂一些。
- 首先调用
searchWord
确认要删除的单词确实存在。 - 从根节点开始,沿着单词路径向下走。
- 在每一步移动之前,先将下一个节点的
pass
值减 1。 - 关键的剪枝操作:如果在减 1 后,下一个节点的
pass
值变成了 0,这意味着除了当前要删除的这个单词外,再也没有任何其他单词会经过这个节点了。因此,我们可以安全地把这条路径tree[cur][path]
直接设为 0(相当于删除了从这个节点开始的后续所有分支),然后直接返回。 - 如果
pass
值不为 0,说明还有其他单词共享这个前缀,不能删除路径,只能继续往下走。 - 走完整个单词路径后,将结尾节点的
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
),将它们的 pass
、end
值和所有出边(路径)都清零。 注意:原代码中没有重置 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;
}