深入浅出双数组字典树Double-arrayTrie(附代码)

双数组字典树是一种高效的数据结构,用于字符串映射和搜索。它通过Base和Check数组压缩存储,实现空间优化和快速查找。文章详细介绍了DAT的构建过程和插入算法,并提供了C++实现的类结构。
摘要由CSDN通过智能技术生成

双数组字典树

今天NLP课上学习了双数组字典树的知识,听懂了但没完全懂,故总结一下。

原理

双数组字典树(Double-Array Trie,简称DAT或者Darts)是一种高效的字典树数据结构,它将字符串映射为整数值,常用于字符串匹配、字符串检索和词频统计等领域。它的原理基于两个关键思想:压缩存储和公共前缀共享。

在DAT中,每个节点都包含一个字符和对应的子节点指针数组。与普通字典树不同的是,DAT将每个节点的子节点指针数组和对应的字符拆分成两个数组:Base和Check。其中,Base数组保存子节点在DAT中的位置信息,而Check数组则用于检查该位置是否已被占用,并指向该节点的父节点。通过这种方式,DAT能够在保证存储空间最小化的同时,提高了数据访问的效率。

具体来说,DAT的构建过程分为两个阶段:trie树的构建和双数组的生成。在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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值