基本概念
- 双数组字典树是字典树的一种特殊形式,思维逻辑和字典树完全相同,只是表现形式上用两个数组来表示。 类似完全二叉树可以用一个数组来表示。
- 表示规范:
用base和check两个数组来表示一个字典树。
base数组确定父节点到子节点的映射关系;
check数组确定子节点到父节点的映射关系;
映射关系:
root_index = 1
base[1] = 1
child_i = base[father] + i
check[child_i] = father
check[child_i] = -father // 负数表示该节点独立成词
变量解释:
check: 当前节点父节点的编号
child_i: 子节点的编号
base数组:特殊确定的一组值
i值: x - 'a'
base值如何确定:不同的学术论文有不同的确定优化方案。
代码演示:
#include <iostream>
#include <algorithm>
#include <string>
#include <map>
#include <set>
#include <vector>
using namespace std;
#define BASE 26
#define MAX_CNT 10000
class node {
public:
int flag;
int next[BASE]; //每一个节点在数组中的下标
void clear() {
flag = 0;
for (int i = 0; i < BASE; i++) {
next[i] = 0;
}
}
} trie[MAX_CNT];
int cnt = 2, root = 1;
void clearTie() {
int cnt = 2; //能够使用的节点的第一个编号
int root = 1; //根节点的编号
trie[root].clear();
return;
}
int getNewNode() {
trie[cnt].clear();
return cnt++;
}
void insert(string s) {
int p = root;
for (auto x : s) {
int ind = x - 'a';
if (trie[p].next[ind] == 0) {
trie[p].next[ind] = getNewNode();
}
p = trie[p].next[ind];
//printf("insert ind %d, p %d\n", ind, p);
}
trie[p].flag = 1;
return;
}
bool search (string s) {
int p = root;
for (auto x : s){
int ind = x - 'a';
p = trie[p].next[ind];
//printf("search ind %d, p %d\n", ind, p);
if (p == 0) {
return false;
}
}
return trie[p].flag;
}
int getBaseValue(int root, int *base, int *check) {
int b = 1, flag = 0; //flag表示是否找到了一个合适的base值
while (flag == 0) {
flag = 1;
b += 1;
for (int i = 0; i < BASE; i++) {
if (trie[root].next[i] == 0) continue; //当前节点的第i条边为空
if (check[b + i] == 0) continue; //check数组b+i的位置为空
//check[b+i]值不是空的,b值出现冲突了
flag = 0;
break;
}
}
return b;
}
int *base, *check, da_root; //da_root表示双数组字典树根节点的编号
int convertToDoubleArrayTrie(int root, int da_root, int *base, int *check) {
if (root == 0) return 0; //空节点
int max_ind = da_root;
//determin base 确定当前节点的base值
base[da_root] = getBaseValue(root, base, check);
for (int i = 0; i < BASE; i++) {
if (trie[root].next[i] == 0) continue; //当前节点的第i个子节点没有值
check[base[da_root] + i] = da_root;
if (trie[trie[root].next[i]].flag) {
//当前节点的第i个子节点独立成词
check[base[da_root] + i] = -da_root;
}
}
//确定每一个节点的base值
for(int i = 0; i < BASE; i++) {
if (trie[root].next[i] == 0) continue; //root没有第i棵子树
//有第i棵子树,把第i棵子树转换成双数组的形式
max_ind = max(
max_ind,
convertToDoubleArrayTrie(
trie[root].next[i],
base[da_root] + i,
base, check
)
);
//trie[root].next[i]表示第i个节点的原节点值,base[da_root] + i表示这个节点在双数组字典中的值
}
return max_ind;
}
bool searchDATrie(string s) {
int p = da_root;
//让p节点开始往下跳
for(auto x: s) {
int ind = x - 'a'; //字母转换成相应的节点编号
if (abs(check[base[p] + ind]) != p) { //判断ind位置节点的父节点是否是p
return false;
}
//ind位置的父节点是p
p = base[p] + ind; //p跳到下一个节点
}
return check[p] < 0; //当前节点独立成词
}
int main() {
// /*** 字典树插入和查找代码
cout << "trie version 4 : " << endl;
clearTie();
int op;
string s;
int n;
cin >> n;
for (int i = 0; i < n; i++) {
cin >> s;
insert(s);
}
base = new int[MAX_CNT];
check = new int[MAX_CNT];
memset(base, 0, sizeof(int) * MAX_CNT);
memset(check, 0, sizeof(int) * MAX_CNT);
int max_ind = convertToDoubleArrayTrie(root, da_root, base, check);
//max_ind表示所使用的数组的最大下标, 原数组的最大下标存储在cnt值中
printf("trie usage : %lu byte\n", cnt * sizeof(node));
printf("double array trie usage : %lu byte\n", (max_ind + 1) * sizeof(int) * 2);
while (cin >> s) {
cout << "find " << s << " from trie : " << search(s) << endl;
cout << "find " << s << " from da trie : " << searchDATrie(s) << endl;
}
return 0;
}
代码输出:
trie version 4 :
cnt : 2, root : 1
>> 3
>> beijing
>> hello
>> world
trie usage : 2052 byte
double array trie usage : 200 byte
>> hell
find hell from trie : 0
find hell from da trie : 0
>> hello
find hello from trie : 1
find hello from da trie : 1
>> worl
find worl from trie : 0
find worl from da trie : 0
>> world
find world from trie : 1
find world from da trie : 1
>> beijing
find beijing from trie : 1
find beijing from da trie : 1
>> beijing1
find beijing1 from trie : 0
find beijing1 from da trie : 0
总结:
双数组字典树的使用场景一:节省内存。和普通字典树相比节省了10倍左右的空间。
使用场景二:双数组字典树可以存到文件中(序列化,方便传输)。从而可以在一台电脑上建立,然后在另外一台电脑上使用。