字典树(Trie)
字典树,也称前缀树(Prefix Tree)或Trie树,是一种高效的字符串检索数据结构,特别适用于处理字符串集合的查找、插入和前缀匹配操作。
基本概念
【定义】字典树是一种多叉树结构,每个节点存储一个字符,从根节点到某一节点的路径上的字符连接起来表示一个字符串。树的每个节点可能有多个子节点,每个子节点代表不同的字符。
【特点】
- 根节点不包含字符
- 从根节点到某个节点的路径上的字符连接起来表示一个字符串
- 每个节点的子节点代表不同的字符(通常用数组或哈希表存储)
- 用额外的标志表示一个完整的单词
基本结构
节点定义
// Trie节点结构
struct TrieNode {
bool isEndOfWord; // 标记该节点是否是一个单词的结尾
TrieNode* children[26]; // 假设只处理小写字母a-z
TrieNode() {
isEndOfWord = false;
for (int i = 0; i < 26; i++) {
children[i] = nullptr;
}
}
};
Trie树类
// Trie树类
class Trie {
private:
TrieNode* root;
public:
Trie() {
root = new TrieNode();
}
// 析构函数删除所有节点
~Trie() {
clear(root);
}
// 递归删除节点
void clear(TrieNode* node) {
if (node) {
for (int i = 0; i < 26; i++) {
if (node->children[i]) {
clear(node->children[i]);
}
}
delete node;
}
}
// 插入一个单词
void insert(string word) {
TrieNode* curr = root;
for (char c : word) {
int index = c - 'a';
if (!curr->children[index]) {
curr->children[index] = new TrieNode();
}
curr = curr->children[index];
}
curr->isEndOfWord = true; // 标记单词结束
}
// 搜索一个单词
bool search(string word) {
TrieNode* node = searchPrefix(word);
return node != nullptr && node->isEndOfWord;
}
// 判断是否有以给定前缀开头的单词
bool startsWith(string prefix) {
return searchPrefix(prefix) != nullptr;
}
private:
// 查找前缀,返回最后一个匹配节点
TrieNode* searchPrefix(string prefix) {
TrieNode* curr = root;
for (char c : prefix) {
int index = c - 'a';
if (!curr->children[index]) {
return nullptr; // 前缀不存在
}
curr = curr->children[index];
}
return curr;
}
};
操作复杂度
- 插入操作:O(m),其中m是单词的长度
- 查找操作:O(m),其中m是查找词的长度
- 前缀查找:O(m),其中m是前缀的长度
- 空间复杂度:O(n*m),其中n是单词数量,m是平均单词长度
优化与变种
数组优化为Map
当字符集较大或稀疏时,可以使用map代替数组来节省空间:
// 使用map优化的Trie节点
struct TrieNode {
bool isEndOfWord;
unordered_map<char, TrieNode*> children;
TrieNode() : isEndOfWord(false) {}
};
计数Trie
在每个节点中添加计数,用于统计单词出现次数:
// 计数Trie节点
struct TrieNode {
int count; // 以此节点结尾的单词数
int prefixCount; // 以此节点对应字符串为前缀的单词数
TrieNode* children[26];
TrieNode() : count(0), prefixCount(0) {
memset(children, 0, sizeof(children));
}
};
压缩Trie (Patricia Trie)
压缩Trie合并只有一个子节点的节点,减少存储空间:
// 压缩Trie节点
struct PatriciaNode {
string segment; // 存储一个字符串段,而不是单个字符
bool isEndOfWord;
unordered_map<char, PatriciaNode*> children;
PatriciaNode(string s = "") : segment(s), isEndOfWord(false) {}
};
01-Trie
用于处理二进制串或整数,每个节点只有0和1两个子节点:
// 01-Trie节点
struct BinaryTrieNode {
BinaryTrieNode* children[2]; // 只有0和1两个子节点
bool isEndOfNumber;
BinaryTrieNode() : isEndOfNumber(false) {
children[0] = children[1] = nullptr;
}
};
// 插入整数
void insertNumber(BinaryTrieNode* root, int num, int bitLength = 31) {
BinaryTrieNode* curr = root;
for (int i = bitLength - 1; i >= 0; i--) {
int bit = (num >> i) & 1;
if (!curr->children[bit]) {
curr->children[bit] = new BinaryTrieNode();
}
curr = curr->children[bit];
}
curr->isEndOfNumber = true;
}
应用场景
1. 字符串检索与前缀匹配
// 获取所有以给定前缀开头的单词
vector<string> getWordsWithPrefix(TrieNode* root, string prefix) {
vector<string> result;
TrieNode* node = findPrefix(root, prefix);
if (!node) return result;
collectWords(node, prefix, result);
return result;
}
void collectWords(TrieNode* node, string currentPrefix, vector<string>& result) {
if (node->isEndOfWord) {
result.push_back(currentPrefix);
}
for (int i = 0; i < 26; i++) {
if (node->children[i]) {
collectWords(node->children[i], currentPrefix + char('a' + i), result);
}
}
}
2. 最长公共前缀
// 查找字符串数组的最长公共前缀
string longestCommonPrefix(vector<string>& strs) {
if (strs.empty()) return "";
Trie trie;
for (string& s : strs) {
trie.insert(s);
}
string prefix = strs[0];
TrieNode* curr = trie.getRoot();
int len = 0;
for (char c : prefix) {
int index = c - 'a';
// 如果当前节点有超过一个子节点,或者是某个单词的结尾,则停止
int childCount = 0;
for (int i = 0; i < 26; i++) {
if (curr->children[i]) childCount++;
}
if (childCount > 1 || curr->isEndOfWord) break;
curr = curr->children[index];
len++;
}
return prefix.substr(0, len);
}
3. 单词替换
// 使用字典中最短的词替换句子中的单词
string replaceWords(vector<string>& dictionary, string sentence) {
Trie trie;
for (string& root : dictionary) {
trie.insert(root);
}
stringstream ss(sentence);
string word, result;
while (ss >> word) {
string replacement = findShortestPrefix(trie.getRoot(), word);
if (!result.empty()) result += " ";
result += replacement.empty() ? word : replacement;
}
return result;
}
string findShortestPrefix(TrieNode* root, string word) {
string prefix;
TrieNode* curr = root;
for (char c : word) {
int index = c - 'a';
if (!curr->children[index]) break;
prefix += c;
curr = curr->children[index];
if (curr->isEndOfWord) return prefix; // 找到最短前缀
}
return ""; // 没有匹配的前缀
}
4. 字典序第K小数
// 查找Trie中字典序第k小的数
string findKthString(TrieNode* root, int k) {
// 计算Trie中每个节点下的单词数
countWords(root);
return findKth(root, k);
}
int countWords(TrieNode* node) {
if (!node) return 0;
int count = node->isEndOfWord ? 1 : 0;
for (int i = 0; i < 26; i++) {
if (node->children[i]) {
count += countWords(node->children[i]);
node->childrenCount[i] = count;
}
}
return count;
}
string findKth(TrieNode* node, int& k) {
if (!node) return "";
if (node->isEndOfWord) {
k--;
if (k == 0) return "";
}
for (int i = 0; i < 26; i++) {
if (node->children[i]) {
if (node->childrenCount[i] >= k) {
return char('a' + i) + findKth(node->children[i], k);
} else {
k -= node->childrenCount[i];
}
}
}
return "";
}
5. 最大异或对
在 01-Trie 中查找与给定数异或值最大的数:
// 查找与num异或值最大的数
int findMaxXOR(BinaryTrieNode* root, int num, int bitLength = 31) {
BinaryTrieNode* curr = root;
int result = 0;
for (int i = bitLength - 1; i >= 0; i--) {
int bit = (num >> i) & 1;
int oppositeBit = 1 - bit; // 相反的位
// 尝试走与当前位相反的路径,这样异或结果为1
if (curr->children[oppositeBit]) {
result |= (1 << i); // 设置结果的当前位为1
curr = curr->children[oppositeBit];
} else {
curr = curr->children[bit];
}
}
return result;
}
实现技巧与优化
1. 边界处理
- 检查空字符串和空Trie
- 处理节点不存在的情况
- 处理字符集范围外的字符
2. 减少内存使用
- 对于大量短字符串,使用位压缩表示
- 短前缀的情况下,用数组代替map提高效率
- 适当删除不再需要的Trie节点
3. 批量操作优化
- 构建Trie时可以一次性排序并插入
- 利用排序减少重复前缀的处理
练习题目推荐
- LC 208: Implement Trie (基本Trie树实现)
- LC 211: Design Add and Search Words Data Structure (支持通配符的Trie)
- LC 212: Word Search II (Trie + DFS)
- LC 421: Maximum XOR of Two Numbers in an Array (01-Trie应用)
- LC 648: Replace Words (前缀替换)
- LC 677: Map Sum Pairs (Trie计数应用)
- POJ 2001: Shortest Prefixes (最短唯一前缀)
- POJ 2513: Colored Sticks (Trie + 并查集)
总结
字典树(Trie)是一种高效的字符串检索数据结构,在处理大量字符串的前缀匹配、查询和存储方面具有显著优势。虽然相比哈希表,它在单个操作上可能并不总是最快的,但在处理字符串集合的整体操作时,特别是涉及前缀时,Trie往往能提供最优的性能。
01-Trie在处理二进制或整数问题时特别有用,如最大异或值问题。压缩Trie(Patricia Trie)则在处理有大量相同前缀的情况时能够显著节省空间。
掌握Trie数据结构及其变种,将帮助你更有效地解决各类字符串相关问题。