基础算法–Trie树
trie
,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。 – 维基百科
存储多个字符串
暴力存储多个字符串,每次读入一个串就相应地开辟存储空间。但如果这些字符串中有大量重复串,我们又该如何节省空间
上面字符串其实又一些共同前缀,因此我们可以将上面结构进一步压缩
其实上面的结构已经和我们传统定义的Trie
已经很接近了,只需要解决最后一个问题,如何组织子节点
构造Trie
传统定义的Trie
需要满足三个条件:
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
- 每个节点的所有子节点包含的字符都不相同
节点
struct TrieNode {
char val; // 节点字符
int end_cnt; // 以val字符为结尾的字符串的个数
int pre_cnt; // 以val字符为前缀的字符串的个数
std::map<char, TrieNode *> chidren;
TrieNode(char v) {
end_cnt = 0;
pre_cnt = 0;
val = v;
}
};
我们将根节点的val
设置为空,根节点是每个字符串的前缀,因此根节点的pre_cnt
即为Trie
中字符串的个数
插入字符串
void insert(TrieNode *root, std::string &str) {
if (!root || str.empty()) return;
for (size_t i = 0; i < str.size(); ++i) {
if (root->chidren.find(str[i]) == root->chidren.end()) {
root->chidren.insert(std::make_pair(str[i], new TrieNode(str[i])));
}
root->pre_cnt += 1;
root = root->chidren[str[i]];
}
root->end_cnt += 1;
}
查找
在不考虑hash
的情况下,Trie
对字符串的查找效率还是非常高的。不管集合是多大,查到字符串的时间复杂度都是
O
(
n
)
O(n)
O(n)其中
n
n
n为要查找字符串的长度
bool find(TrieNode *root, std::string &str) {
if (!root) return false;
for (size_t i = 0; i < str.size(); ++i) {
if (root->chidren.find(str[i]) == root->chidren.end()) return false;
root = root->chidren[str[i]];
}
return root->end_cnt > 0;
}
释放
因为我们使用了堆内存,因此别忘了释放内存
void free(TrieNode *root) {
for (auto &it : root->chidren) {
free(it.second);
}
root->chidren.clear();
delete root;
}
显然,如果数据字符串数据量非常大,并且这些字符串中有大量重复串,我们使用Trie
这种结构存储可以大量节省内存。当然有些时候对每个字符都建立一个节点也会照成一些空间上的浪费,因此在实际使用中往往会根据具体实际情况对Trie
中节点做一些合并和压缩,这里就不展开了,有兴趣的同学可以查阅相关资料
应用举例
前缀匹配
给定一个字符串集合
N
N
N,如果我想获取指定前缀的所有字符串集合
R
(
R
⊂
N
)
R(R\subset N)
R(R⊂N)的数量。很显然暴力求解的话时间复杂度是
O
(
h
∗
N
)
O(h*N)
O(h∗N)其中
h
h
h为要查找前缀的长度。那么用Trie
就不一样了,它可以做到
O
(
h
)
O(h)
O(h)
int find_prefix_cnt(TrieNode *root, std::string &prefix) {
if (!root) return 0;
for (size_t i = 0; i < prefix.size(); ++i) {
if (root->chidren.find(prefix[i]) == root->chidren.end()) return 0;
root = root->chidren[prefix[i]];
}
return root->pre_cnt;
}
这个在我们词典中对单词的检索其实是非常有用的,当然,我们还可以数据匹配到的字符串集合,理解原理之后其实也很简单,这里就不展开了