Trie树结构与实现
概述
在平时使用搜索引擎或者一些其他工具时,常会有一些智能提示,或许我们需要搜索的内容并不存在,但这些工具都会尽量选择前缀尽可能相似的结果给我们。这里使用到的前缀匹配,大多则是使用Trie树进行处理。
Trie树基本定义
Trie树,又被称为前缀树或字典树,与其用途有关,Trie树是一种有序树,用于保存关联数组,其中的Key通常是字符串。与二叉查找树不同,Key并不直接存储在Trie树种,而是由节点在树中位置决定。一个节点的所有子节点具有相同前缀,也就是这个节点对应的字符串,即Key,根节点对应空字符串。
上图即为一颗Trie树,由关键字集合{“a”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”} 构造而成。先来说明Trie树的特性:
- 根节点不包含任何字符,除根节点外,任一节点都包含一个字符;
- 从根节点到任一子节点的路径上,连接之后可以得到一个字符串,为该节点对应的字符串;
- 每个节点的子节点包含的字符必须互不相同。
由上图不难发现,若两个关键字具有相同前缀,那到达完整关键字节点的路径必有一段是相同的,所以Trie树的一个应用可以用来求解多关键字的最长公共前缀。下面则是求解最长公共前缀具体的代码实现,相关需要注意的点在注释中可以找到。
实现
typedef struct trie_tree {
int cnt; // 当前节点所代表字符出现次数
struct trie_tree *next[26]; // 记录子节点的指针数组
trie_tree() : cnt(0) {
for (int i = 0; i < 26; ++i) {
next[i] = 0;
}
}
} trie_t;
指针数组大小为26,仅是用于简单表示关键字由26个小写字母组合的情况,这个可以根据问题去做修改。根据需要求解的问题的不同,也可以针对结构体做一些相应修改,如词频统计或许需要一个bool值来判别是否为一个关键字。
void trie_create(const std::string &str, trie_t *root)
{
int i;
int len = (int) str.length();
trie_t *cur = root;
for (i = 0; i < len; ++i) {
if (cur->next[str[i] - 'a'] == 0) {
cur->next[str[i] - 'a'] = new trie_t();
}
cur = cur->next[str[i] - 'a'];
++(cur->cnt);
}
}
对于一个字符串将其添加进Trie树,若遇到不存在表示某字符的节点,则应创建一个。
std::string trie_search_lcp(trie_t *root, size_t strs_cnt, size_t min_len)
{
std::string res;
trie_t *cur = root;
int i;
int index;
int scnt; // 表示子节点个数
while (cur != 0) {
scnt = 0;
for (i = 0; i < 26; ++i) { // 寻找next路径,以构成最长公共前缀
if (cur->next[i] != 0) {
++scnt;
index = i;
}
}
if (strs_cnt == 0) { // 字符串集合为空,最长公共前缀为空
return res;
}
else if (strs_cnt == 1) { // 字符串集合仅有1个元素,最长公共前缀即为该元素
if (scnt == 1) { // next仅有1个节点
res.push_back('a' + index);
cur = cur->next[index];
}
else { // next无节点
cur = 0;
}
}
else { // 字符串集合有多于1个元素
if (scnt == 1) { // next仅有1个节点
// 节点字符出现次数与集合元素数量相等
if (cur->next[index]->cnt == strs_cnt) {
// 最长公共前缀的最大长度一定小于等于字符串集合中最短的字符串
if (res.length() < min_len) {
res.push_back('a' + index);
}
}
cur = cur->next[index];
}
else { // 搜索到next不止有1个节点
cur = 0;
}
}
}
return res;
}
scnt用于记录当前节点的子节点数,求解最长公共前缀时,子节点数超过1个,肯定就不是公共前缀了,注意这点。一些细节看代码注释即可。