控制面板
操作:
可视化区域
结果与日志
匹配结果:
伪代码
C++ 实现
扩展知识
Trie树基本操作伪代码
1. 节点结构
类 TrieNode:
// 子节点映射,键是字符,值是子节点
子节点 = 新 映射<字符, TrieNode>
// 标记当前节点是否代表一个完整单词的结束
是否单词结尾 = 假
2. 插入操作
函数 插入(根节点, 单词):
当前节点 = 根节点
对于 单词 中 的 每个字符 c:
如果 当前节点.子节点 不 包含键 c:
// 如果字符路径不存在,创建新节点
当前节点.子节点[c] = 新 TrieNode()
// 移动到下一个节点
当前节点 = 当前节点.子节点[c]
// 单词遍历完成,标记结尾
当前节点.是否单词结尾 = 真
3. 查找操作
函数 查找(根节点, 单词):
当前节点 = 根节点
对于 单词 中 的 每个字符 c:
如果 当前节点.子节点 不 包含键 c:
// 路径中断,单词不存在
返回 假
// 继续沿路径前进
当前节点 = 当前节点.子节点[c]
// 检查路径走完后,是否是单词结尾
返回 当前节点.是否单词结尾
4. 前缀搜索操作
函数 前缀搜索(根节点, 前缀):
当前节点 = 根节点
对于 前缀 中 的 每个字符 c:
如果 当前节点.子节点 不 包含键 c:
// 前缀路径不存在
返回 假
// 继续沿路径前进
当前节点 = 当前节点.子节点[c]
// 前缀路径存在
返回 真
// --- 获取所有此前缀开头的单词 (扩展) ---
函数 查找此前缀节点(根节点, 前缀):
当前节点 = 根节点
对于 前缀 中 的 每个字符 c:
如果 当前节点.子节点 不 包含键 c: 返回 空
当前节点 = 当前节点.子节点[c]
返回 当前节点
函数 收集单词(节点, 当前前缀, 结果列表):
如果 节点.是否单词结尾:
结果列表.添加(当前前缀)
对于 每个键值对 (字符 c, 子节点 nextNode) 在 节点.子节点 中:
收集单词(nextNode, 当前前缀 + c, 结果列表)
函数 获取此前缀所有单词(根节点, 前缀):
前缀末尾节点 = 查找此前缀节点(根节点, 前缀)
结果列表 = 新 列表<字符串>
如果 前缀末尾节点 != 空:
收集单词(前缀末尾节点, 前缀, 结果列表)
返回 结果列表
Trie树C++完整实现 (基于 `unordered_map`)
#include <iostream>
#include <unordered_map>
#include <vector>
#include <string>
#include <memory> // 为了使用智能指针 std::unique_ptr
// Trie 节点定义
struct TrieNode {
std::unordered_map<char, std::unique_ptr<TrieNode>> children; // 子节点映射,使用智能指针管理内存
bool isEndOfWord; // 标记是否为单词结尾
TrieNode() : isEndOfWord(false) {} // 构造函数
};
// Trie 树类定义
class Trie {
private:
std::unique_ptr<TrieNode> root; // 根节点,使用智能指针
// 辅助函数:递归收集以某个节点开头的所有单词
void collectWords(TrieNode* node, std::string currentPrefix, std::vector<std::string>& results) {
if (node==nullptr) {
return; // 如果节点为空,直接返回
}
if (node->isEndOfWord) {
results.push_back(currentPrefix); // 如果是单词结尾,添加到结果列表
}
// 遍历所有子节点
for (const auto& pair : node->children) {
collectWords(pair.second.get(), currentPrefix + pair.first, results); // 递归调用
}
}
// 辅助函数:查找给定前缀的最后一个节点
TrieNode* findPrefixNode(const std::string& prefix) {
TrieNode* currentNode = root.get();
for (char c : prefix) {
if (currentNode->children.find(c) == currentNode->children.end()) {
return nullptr; // 路径不存在
}
currentNode = currentNode->children[c].get();
}
return currentNode; // 返回前缀的最后一个节点
}
public:
// 构造函数:初始化根节点
Trie() : root(new TrieNode()) {}
// 插入单词
void insert(const std::string& word) {
TrieNode* currentNode = root.get(); // 从根节点开始
for (char c : word) {
// 如果字符对应的子节点不存在,则创建
if (currentNode->children.find(c) == currentNode->children.end()) {
currentNode->children[c] = std::make_unique<TrieNode>();
}
// 移动到子节点
currentNode = currentNode->children[c].get();
}
currentNode->isEndOfWord = true; // 标记单词结束
}
// 查找完整单词
bool search(const std::string& word) {
TrieNode* node = findPrefixNode(word);
return node != nullptr && node->isEndOfWord; // 节点存在且是单词结尾
}
// 检查是否存在指定前缀
bool startsWith(const std::string& prefix) {
return findPrefixNode(prefix) != nullptr; // 只要前缀节点存在即可
}
// 获取所有以指定前缀开头的单词
std::vector<std::string> getWordsWithPrefix(const std::string& prefix) {
std::vector<std::string> results;
TrieNode* prefixNode = findPrefixNode(prefix);
if (prefixNode != nullptr) {
collectWords(prefixNode, prefix, results); // 从前缀末尾节点开始收集
}
return results;
}
};
// --- 主函数示例 ---
// int main() {
// Trie trie;
// trie.insert("apple");
// trie.insert("apply");
// trie.insert("application");
//
// std::cout << "Search 'apple': " << (trie.search("apple") ? "Found" : "Not Found") << std::endl; // Found
// std::cout << "Search 'app': " << (trie.search("app") ? "Found" : "Not Found") << std::endl; // Not Found
// std::cout << "Starts with 'app': " << (trie.startsWith("app") ? "Yes" : "No") << std::endl; // Yes
//
// std::vector<std::string> words = trie.getWordsWithPrefix("app");
// std::cout << "Words starting with 'app': ";
// for(const auto& word : words) {
// std::cout << word << " ";
// } // apple apply application
// std::cout << std::endl;
//
// return 0;
// }
Trie树扩展知识
标准的Trie树虽然直观且在某些场景下高效,但也存在一些缺点和可以优化的方向:
1. 空间效率问题
当字符集很大或字符串很长时,Trie树可能会占用大量内存,因为每个节点可能需要存储指向所有可能字符的指针(或数组索引),即使很多指针是空的。
2. 压缩Trie (Patricia Tree / Radix Tree)
为了解决空间问题,可以对Trie进行压缩。压缩Trie(基数树)会将只有一个子节点的中间节点链合并。节点之间的边不再代表单个字符,而是代表一个字符序列(子串)。这大大减少了节点数量,尤其是在处理长字符串或稀疏数据集时。
- 优点:显著节省空间。
- 缺点:实现相对复杂,插入和删除操作可能需要拆分或合并节点。
3. 双数组Trie (Double-Array Trie)
双数组Trie是一种极其节省空间的Trie实现方式,常用于需要极高性能和低内存占用的场景(如输入法引擎、自然语言处理库)。它使用两个一维数组(`base` 和 `check`)来紧凑地表示整个Trie的结构和转移。
- 优点:极高的空间效率和查找速度(接近 O(1) 的字符访问)。
- 缺点:构建和更新(插入/删除)非常复杂,通常用于静态或很少更新的词典。
4. Aho-Corasick 算法
Aho-Corasick算法是在Trie树的基础上构建一个有限自动机,用于在一个文本中高效地同时查找多个模式串(关键字)。它通过添加“失败链接”(failure links)来优化匹配过程,当当前路径匹配失败时,可以快速跳转到可能匹配的次长前缀,避免了从头重新匹配。
- 应用:文本过滤、多模式匹配、生物信息学等。
- 核心:Trie结构 + 失败函数。
5. 性能优化考虑
- 节点数据结构:根据字符集大小和稀疏性选择合适的子节点表示(固定数组 vs. 哈希表 vs. 平衡树)。对于小而密集的字符集(如小写字母),数组可能更快;对于大而稀疏的字符集(如Unicode),哈希表更节省空间。
- 内存分配:频繁的节点创建和销毁可能导致内存碎片和开销。可以考虑使用内存池(Memory Pool)来预分配和管理节点内存。
- 缓存友好性:数组表示(尤其是双数组Trie)通常比基于指针和哈希表的实现具有更好的缓存局部性,从而提高访问速度。
理解这些扩展知识有助于根据具体应用场景选择或设计更合适的Trie变体。