目录
什么是前缀树
Trie树,即字典树,又称单词查找树或键树,是一种多叉树结构,是一种哈希树的变种。典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:最大限度地减少无谓的字符串比较。如下图:
好比假设有b,abc,abd,bcd,abcd,efg,hii 这6个单词,那我们创建trie树就得到
上图可以归纳出 Trie 树的基本性质:
1. 根节点不包含字符,除根节点外的每一个子节点都包含一个字符。
2. 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
3. 每个节点的所有子节点包含的字符互不相同。
4. 若实现前缀树时用的是hash数组,如vector<Node*> child; Node : child(26);则每个节点的子节点都是按字典序的,如上图根节点的孩子a->b->e->h。(这种前缀树就是字典树,可以用于按字典序输出树种的字符串。优点:可以按字典树输出字符串。缺点:占用空间大)
若实现前缀树时用的是标准库hashmap:unordered_map<char, Node*> child,则每个节点的子节点不是按字典序的(优点:占用空间小。缺点:不能按字典树输出字符串),如下图:
通常在实现的时候,会在节点结构中设置一个标志,用来标记该结点处是否构成一个单词(关键字)。
可以看出,Trie 树的关键字一般都是字符串,而且 Trie 树把每个关键字保存在一条路径上,而不是一个结点中。另外,两个有公共前缀的关键字,在 Trie 树中前缀部分的路径相同,所以 Trie 树又叫做前缀树(Prefix Tree)。
前缀树的优缺点
Trie 树的核心思想是空间换时间,利用字符串的公共前缀来减少无谓的字符串比较以达到提高查询效率的目的。
优点:
- 存储和查询的都很高效,都为
O(m)
,其中m
是待插入/查询的字符串的长度。常用于:- 向前缀树中插入字符串word;
- 查询前缀串prefix是否为已经插入到前缀树中的任意一个字符串word的前缀;
-
Trie 树中不同的关键字不会产生冲突。
-
Trie 树只有在允许一个关键字关联多个值的情况下才有类似 hash 碰撞发生。
-
Trie 树不用求 hash 值,对短字符串有更快的速度。通常,求 hash 值也是需要遍历字符串的。
-
Trie 树可以对关键字按字典序排序(需要用hash数组实现)。
缺点:
-
当 hash 函数很好时,Trie 树的查找效率会低于哈希搜索。
-
空间消耗比较大。
前缀树的应用
1,字符串检索
检索/查询功能是Trie树最原始的功能,给定一组字符串,查找某个字符串是否出现过。
思路就是从根节点开始一个一个字符进行比较:
(1) 如果沿路比较,发现不同的字符,则表示该字符串在集合中不存在。
(2) 如果所有的字符全部比较完并且全部相同,还需判断最后一个节点的标志位(标记该节点是否代表一个关键字)。
2,词频统计
Trie树常被搜索引擎系统用于文本词频统计 。
思路: 用整型变量 count
来计数。对每一个关键字执行插入操作,若已存在,计数加1,若不存在,插入后 count
置1。
3,字符串排序
Trie 树可以对大量字符串按字典序进行排序,思路也很简单:遍历一次所有关键字,将它们全部插入 Trie
树,树的每个结点所有子节点很显然地按照字母表排序,然后先序遍历输出 Trie
树中所有关键字即可。
4,前缀匹配
例如:找出一个字符串集合中所有以 ab
开头的字符串。我们只需要用所有字符串构造一个 Trie
树,然后输出以 a->b->
开头的路径上的关键字即可。
Trie
树前缀匹配常用于搜索提示。如当输入一个网址,可以自动搜索出可能的选择。当没有完全匹配的搜索结果,可以返回前缀最相似的可能。
前缀树基本结构及实现:
// 字典树 -- hash 数组实现
// 不释放内存版
// 法1:树本身就是节点
class Trie {
public:
Trie() : child(26) , isEnd(false) {
}
void insert(string word) {
Trie* node = this;
for (auto ch : word) {
ch -= 'a';
if (node->child[ch] == nullptr) {
node->child[ch] = new Trie;
}
node = node->child[ch];
}
node->isEnd = true;
}
bool search(string word) {
Trie* matchNode = SearchPrefix(word);
return matchNode != nullptr && matchNode->isEnd;
}
bool startsWith(string prefix) {
Trie* matchNode = SearchPrefix(prefix);
return matchNode != nullptr ? true : false;
}
private:
vector<Trie*> child; // hash数组足额申请26节点内存, 可使子节点有序
bool isEnd;
Trie* SearchPrefix(string word)
{
Trie* node = this;
for (auto ch : word) {
ch -= 'a';
if (node->child[ch] == nullptr) {
return nullptr;
}
node = node->child[ch];
}
return node;
}
};
// 法2:树本身只包含根节点,节点结构在树外部实现
namespace TrieTest1 {
struct Node {
vector<Node*> child; // hash数组足额申请26节点内存, 可使子节点有序
Node() : child(26), isEnd(false) {};
bool isEnd;
};
class Trie {
public:
Trie() {}
void insert(string word) {
Node* node = &root;
for (auto ch : word) {
ch -= 'a';
if (node->child[ch] == nullptr) {
node->child[ch] = new Node;
}
node = node->child[ch];
}
node->isEnd = true;
}
bool search(string word) {
Node* matchNode = SearchPrefix(word);
return matchNode != nullptr && matchNode->isEnd;
}
bool startsWith(string prefix) {
Node* matchNode = SearchPrefix(prefix);
return matchNode != nullptr ? true : false;
}
private:
Node root;
Node* SearchPrefix(string word) {
Node* node = &root;
for (auto ch : word) {
ch -= 'a';
if (node->child[ch] == nullptr) {
return nullptr;
}
node = node->child[ch];
}
return node;
}
};
};
// 释放内存版
// 树内部用pool记录所有申请过的节点指针,析构函数中遍历释放
namespace TrieTest2 {
struct Node {
vector<Node*> child;
Node() : child(26), isEnd(false) {};
bool isEnd;
};
class Trie {
public:
Trie() {}
~Trie()
{
for (auto& nodePtr : pool) {
delete nodePtr;
}
}
void insert(string word) {
Node* node = &root;
for (auto ch : word) {
ch -= 'a';
if (node->child[ch] == nullptr) {
node->child[ch] = new Node;
pool.push_back(node->child[ch]);
}
node = node->child[ch];
}