控制面板

操作:

可视化区域

结果与日志

匹配结果:
伪代码
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变体。