【数据结构与算法】字典树/前缀树(Python/C++)

字典树/前缀树 Trie

前缀树是一种树结构,当一系列单词有很多公共前缀时就可以用前缀树来存储和查询,如果这些单词没有公共前缀,那么和用数组存是一样的。因为前缀树中节点通常用字典(Python中的dict,C++中的map)这个数据结构来存储子节点,所以常被称为字典树。前缀树是一种空间换时间的思想,这个和哈希表还有动态规划是一样的。

前缀树常常被用于基于前缀的模糊匹配,但其不局限于存储单词,树中的节点可以是任意的数据类型或者结构,比如前缀树会被用来解决最大异或值的问题,这时前缀树为二叉树,节点的值为0或者1。

前缀树只能用作基于前缀的模糊匹配,如果要做到匹配字符串中的某一段则要借助基于前缀树的AC自动机了,AC自动机融合了前缀树和KMP算法的思想。

前缀树的实现(Python)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class TrieNode:
"""前缀树中节点的数据结构"""
def __init__(self):
# count表示有多少个单词以当前节点为结尾
self.count = 0
# pre_count表示以根节点到当前节点为前缀的单词个数
self.pre_count = 0
# children存储所有当前节点的下一个节点
self.children = {}


class Trie:
"""前缀树的实现"""
def __init__(self):
# 定义前缀树的根节点
self.root = TrieNode()

def insert(self, word):
"""向前缀树中添加单词"""
node = self.root
for ch in word:
if ch not in node.children:
node.children[ch] = TrieNode()
node = node.children[ch]
node.pre_count += 1
node.count += 1

def search(self, word):
"""查找word是否位于前缀树内"""
node = self.root
for ch in word:
if ch not in node.children:
return False
node = node.children[ch]
return node.count > 0

def start_with(self, prefix):
"""判断prefix是否为前缀树中的前缀"""
node = self.root
for ch in prefix:
if ch not in node.children:
return False
node = node.children[ch]
return node.pre_count > 0

前缀树的实现(C++类模版+智能指针)

一共有三个文件组成:main.cppTrie.hppTrie.tpp
main.cpp:测试代码
Trie.hppTrie类模版的声明
Trie.tppTrie方法的实现,我们可以看到文件后缀不是平常的.cpp,这是因为g++编译器不支持模版的分离编译,因为编译器编译时会根据模版的类型生成对应版本的类声明,但是此时在.cpp中找不到对应类型的类方法的实现。所以这里把模版类的方法定义在单独的一个.tpp文件中,其实后缀不一定要是tpp,可以随意起一个后缀,反正要在.hpp文件后#include这个文件。其实模版类方法的实现也可以直接放在.hpp文件中,但是一般非inline函数不会在头文件里定义,所以前面那种方法更好。还有一种不好的办法是在.cpp中函数定义的下面显性声明对应类型版本的模版类,但这和我们用模版的初衷相悖,所以基本不会这样做。
这里为了避免内存泄漏和主动释放动态分配内存,运用了智能指针(smart pointer)shared_ptr来指向前缀树节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//
// Trie.hpp
//
// Created by @YangZai on 26.05.21.
//
#ifndef Trie_hpp
#define Trie_hpp

// 要用智能指针须导入memory
#include <memory>
#include <map>

using namespace std;

template <typename T> struct TrieNode {
int count;
int pre_count;
map<T, shared_ptr<TrieNode>> children;
};

template <typename N, typename M> class Trie {
private:
shared_ptr<TrieNode<N>> root = make_shared<TrieNode<N>>();
public:
void insert(M&);
bool search(M&);
bool startWith(M&);
};

#include "Trie.tpp"

#endif /* Trie_hpp */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//
// Trie.tpp
template <typename N, typename M>
void Trie<N, M>::insert(M &word)
{
auto node = root;
for (auto &ch: word) {
if (node->children.find(ch) == node->children.end()) {
node->children.insert(make_pair(ch, make_shared<TrieNode<N>>()));
}
node = node->children[ch];
node->pre_count++;
}
node->count++;
}

template <typename N, typename M>
bool Trie<N, M>::search(M &word)
{
auto node = root;
for (auto &ch: word) {
if (node->children.find(ch) == node->children.end()) {
return false;
}
node = node->children[ch];
}
return node->count > 0;
}

template <typename N, typename M>
bool Trie<N, M>::startWith(M &prefix)
{
auto node = root;
for (auto &ch: prefix) {
if (node->children.find(ch) == node->children.end()) {
return false;
}
node = node->children[ch];
}
return node->pre_count > 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//
// main.cpp
//
// Created by @YangZai on 26.05.21.
//
#include "Trie.hpp"

#include <iostream>
#include <vector>

void test_string()
{
cout << "N: char, M: string:" << endl;

vector<string> list = {"hello world!", "hello"};
string a = "hello";
Trie<char, string> trie;
for (auto &i: list) {
trie.insert(i);
}
cout << "\ta" << (trie.search(a) ? " is" : " isn't") << " in the Trie" << endl;
cout << "\ta" << (trie.startWith(a) ? " is" : " isn't") << " one prefix in the Trie" << endl;
}

void test_vector()
{
cout << "N: int, M: vector<int>:" << endl;

vector<vector<int>> list = {{6, 5, 4, 3, 2, 1}, {6, 5, 4}};
vector<int> a = {6, 5, 4};
Trie<int, vector<int>> trie;
for (auto &i: list) {
trie.insert(i);
}
cout << "\ta" << (trie.search(a) ? " is" : " isn't") << " in the Trie" << endl;
cout << "\ta" << (trie.startWith(a) ? " is" : " isn't") << " one prefix in the Trie" << endl;
}

int main(int argc, const char * argv[])
{
test_string();
test_vector();
return 0;
}
/*
N: char, M: string:
a is in the Trie
a is one prefix in the Trie
N: int, M: vector<int>:
a is in the Trie
a is one prefix in the Trie
*/

Trie相关题目(持续更新中)