双数组字典树
今天NLP课上学习了双数组字典树的知识,听懂了但没完全懂,故总结一下。
原理
双数组字典树(Double-Array Trie,简称DAT或者Darts)是一种高效的字典树数据结构,它将字符串映射为整数值,常用于字符串匹配、字符串检索和词频统计等领域。它的原理基于两个关键思想:压缩存储和公共前缀共享。
在DAT中,每个节点都包含一个字符和对应的子节点指针数组。与普通字典树不同的是,DAT将每个节点的子节点指针数组和对应的字符拆分成两个数组:Base和Check。其中,Base数组保存子节点在DAT中的位置信息,而Check数组则用于检查该位置是否已被占用,并指向该节点的父节点。通过这种方式,DAT能够在保证存储空间最小化的同时,提高了数据访问的效率。
具体来说,DAT的构建过程分为两个阶段:trie树的构建和双数组的生成。在trie树的构建过程中,按照输入的字符串顺序,将每个字符串插入到trie树中。在插入过程中,如果当前节点存在未被使用的子节点,就将该字符放入该子节点中。如果当前节点不存在未被使用的子节点,则新建一个子节点,并将该字符放入该子节点中。在该过程中,每个节点的子节点按照字典序排序。

在trie树构建完成后,DAT通过深度优先遍历的方式,将每个节点的Base和Check数组位置信息填入对应的数组中,保证Base数组中的所有值都是唯一的,并且Check数组中每个位置最多只有一个节点指向它。

通过使用双数组,DAT实现了压缩存储和公共前缀共享,因此它的空间占用率比普通字典树低,而且在查找和插入操作时具有很高的效率。
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <algorithm>
using namespace std;
// 定义一个节点类
class Node {
public:
int base; // base值
int check; // check值
bool is_end; // 是否是单词的结尾
int parent; // 父节点在数组中的下标
char ch; // 节点存储的字符
// 默认构造函数
Node() {
base = 0;
check = 0;
is_end = false;
parent = -1;
ch = '\0';
}
// 带参数的构造函数
Node(int b, int c, bool end, int p, char chr) {
base = b;
check = c;
is_end = end;
parent = p;
ch = chr;
}
};
// 双数组字典树类
class DoubleArrayTrie {
public:
vector<Node> trie; // 存储Trie树的数组
vector<int> used; // 标记数组,用于记录被占用的check值
int root; // 根节点在数组中的下标
// 默认构造函数
DoubleArrayTrie() {
root = 0;
trie.push_back(Node());
used.push_back(0);
}
// 插入一个单词
void insert(const string& word) {
int node = root; // 从根节点开始
int parent = 0; // 记录父节点在数组中的下标
for (char c : word) {
parent = node;
int index = c - 'a';
if (trie[node].base == 0) { // 如果节点没有子节点,直接添加
int new_base = alloc();
trie[node].base = new_base;
trie[new_base].parent = parent;
trie[new_base].ch = c;
node = new_base;
} else if (trie[node].base > 0) { // 如果节点有子节点
int new_node = trie[node].base + index;
if (trie[new_node].check == node) { // 子节点已经存在
node = new_node;
} else if (trie[new_node].check != 0) { // 冲突了,需要重新分配base值
int old_node = new_node;
int new_base = alloc();
trie[new_base].parent = parent;
trie[new_base].ch = c;
trie[parent].base = new_base;
for (int i = 0; i < 26; i++) {
int tmp_node = old_node - index + i;
if (trie[tmp_node].check == old_node) {
trie[tmp_node].check = new_base;
}
}
trie[new_node].parent = new_base;
trie[new_base].base = old_node;
node = new_node;
} else { // 子节点不存在
trie[new_node].parent = node;
trie[new_node].ch = c;
trie[node].check = new_node;
node = new_node;
}
}
}
trie[node].is_end = true; // 单词结尾标记
实现了双数组字典树,通过使用类来封装。在这个类中,我们定义了节点结构体,节点包含了字符值、父节点的下标、子节点的下标、以及标记是否为某个单词的结尾。同时,我们还定义了一个数组来存储所有节点,和一个记录已使用下标的数组。这个类中包含了插入单词、查询单词、以及查询前缀的方法,这些方法都是通过在节点数组中寻找合适的节点来实现的。
数据测试
以下是一些测试用例,运行结果表明代码无误(大概
- 示例输入1
5
hello
world
hi
hell
word
3
hell
word
hi
- 示例输出1
hell: 4
hi: 3
word: 5
- 示例输入2
6
apple
application
banana
book
cat
cup
4
appl
banana
book
cat
- 示例输出2
appl: NOT FOUND
banana: 3
book: 4
cat: 5
双数组字典树是一种高效的数据结构,用于字符串映射和搜索。它通过Base和Check数组压缩存储,实现空间优化和快速查找。文章详细介绍了DAT的构建过程和插入算法,并提供了C++实现的类结构。
816

被折叠的 条评论
为什么被折叠?



