libdatrie: 一个高效的 基于双数组字典树(Double-Array Trie)的C语言函数库

目录

1.简介

[2.双数组字典树(Double-Array Trie)的原理](#2.双数组字典树(Double-Array Trie)的原理)

3.安装方法

[4.核心 API 使用示例](#4.核心 API 使用示例)

5.进阶用法

6.应用场景

7.注意事项

8.总结


1.简介

字符串检索算法:KMP和Trie树

libdatrie是一个用于实现双数组Trie树 的C语言函数库,它提供了一种高效存储和检索键值对的数据结构。Trie树(也称为字典树前缀树 )是一种专门用于处理字符串集合的树形数据结构,它通过字符串的公共前缀来共享存储空间,从而节省内存并提高查询效率。libdatrie实现的双数组结构将Trie树的时间效率和空间紧凑性 结合得淋漓尽致,特别适合处理大规模字符串集合(如字典、关键词库、路由表等)。

该库最初是为泰语语言支持 而开发的,因为泰语有着复杂的字符集和拼写规则,传统的哈希表或平衡二叉树在处理泰语文字时往往效率不高。但随着开发进展,libdatrie证明了它在其他语言和环境同样有效,如今已被广泛应用于各种自然语言处理任务中。无论是构建拼写检查器、实现输入法引擎,还是进行中文分词,libdatrie都能提供出色的性能。

libdatrie的主要特点包括:

1.高效的数据结构: 基于双数组字典树实现,兼顾了 trie 树的前缀匹配能力和数组的随机访问效率,查询 / 插入 / 删除操作的时间复杂度均为 O (字符串长度)

2.**低内存占用:**双数组结构通过紧凑存储转移状态,避免了传统 trie 树的指针冗余,内存占用仅为红黑树 / 哈希表的 1/3~1/5(大规模数据下优势更明显)。

3.**Unicode 支持:**原生支持 UTF-8 编码,可直接处理中文、日文、韩文等多语言字符串。

4.**丰富的查询功能:**支持精确匹配、前缀匹配、预测查询(自动补全)、通配符匹配等。

5.**持久化存储:**支持将 trie 树保存到文件,后续可直接加载,避免重复构建的开销。

6.**轻量级:**代码简洁(核心代码仅几千行),无依赖,可跨平台(Linux、Windows、macOS 等)。

2.双数组字典树(Double-Array Trie)的原理

双数组字典树(Double-Array Trie) 是一种空间高效前缀树(Trie)实现,核心思想是:** ****用两个数组(base[]check[])替代传统前缀树的指针链接,通过数学计算**实现状态转移,从而在保持前缀树前缀匹配能力的同时,大幅减少内存占用并提升查询效率。

解决的核心问题

传统前缀树的缺陷:

  • 每个节点需存储多个指针(指向子节点),空间开销大(尤其是字符集大时,大量指针为 NULL);
  • 指针跳转导致缓存不友好,查询效率受影响。

双数组字典树的改进:

  • 用数组下标替代指针,通过 base[] 计算子节点位置,check[] 验证合法性;
  • 紧凑存储有效状态,无冗余指针,内存占用仅为传统前缀树的 1/5~1/10;
  • 数组连续存储,缓存命中率高,查询速度更快。

双数组字典树的核心是两个长度相同的数组,所有操作都围绕这两个数组base[]check[]展开:

数组 作用说明
base[] 存储「状态转移的基准地址」:对于当前状态 s 和字符 c,子节点状态 = base[s] + code(c)code(c) 是字符 c 的编码值)。
check[] 存储「状态的前驱验证信息」:对于状态 t,若 check[t] == s,说明 t 是状态 s 通过某个字符转移而来的合法子节点。

这种设计的巧妙之处在于,它将树结构扁平化为两个数组,既保留了Trie树的前缀搜索特性,又大大减少了内存使用。同时,由于数据的连续存储,CPU缓存命中率更高,从而进一步提升了访问速度。

与哈希表相比,双数组Trie不仅查询速度相当,还支持前缀搜索、按字典序遍历等额外功能,使其在文本处理领域更具优势。

3.安装方法

源码地址: https://github.com/tlwg/libdatrie

1.源码编译安装(Linux/macOS)

cpp 复制代码
# 下载源码(官网:https://linux.thai.net/projects/libdatrie/)
wget https://linux.thai.net/pub/thailinux/software/libdatrie/libdatrie-0.2.13.tar.xz
tar -xf libdatrie-0.2.13.tar.xz
cd libdatrie-0.2.13

# 编译安装
./configure --prefix=/usr/local
make
sudo make install

2.包管理器安装(Ubuntu/Debian)

cpp 复制代码
sudo apt-get install libdatrie-dev

3.Windows 编译

通过 MinGW 或 MSVC 编译源码,生成 datrie.lib 静态库或 datrie.dll 动态库。

4.核心 API 使用示例

以下是一个简单的 字典查询 + 前缀匹配 示例,演示 libdatrie 的基本用法。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <datrie/trie.h>

int main() {
    // 1. 创建 trie 树(指定字符集为 UTF-8)
    Trie *trie = trie_new(NULL);
    if (!trie) {
        fprintf(stderr, "Failed to create trie\n");
        return 1;
    }

    // 2. 插入字符串(支持 UTF-8 中文)
    const char *words[] = {"apple", "app", "application", "banana", "bear", "中国", "中国人"};
    int word_count = sizeof(words) / sizeof(words[0]);
    for (int i = 0; i < word_count; i++) {
        // 插入成功返回 0,已存在返回 1
        if (trie_insert(trie, words[i]) == 0) {
            printf("Inserted: %s\n", words[i]);
        } else {
            printf("Already exists: %s\n", words[i]);
        }
    }

    // 3. 精确匹配查询
    const char *query1 = "app";
    TrieData data; // 存储查询结果(可关联自定义数据,此处用默认)
    if (trie_lookup(trie, query1, &data)) {
        printf("Found: %s\n", query1);
    } else {
        printf("Not found: %s\n", query1);
    }

    // 4. 前缀匹配查询(遍历所有以 "app" 开头的字符串)
    printf("\nPrefix match for 'app':\n");
    TrieIterator *iter = trie_prefix_iterator_new(trie, "app");
    if (iter) {
        const char *prefix_word;
        while ((prefix_word = trie_iterator_next(iter))) {
            printf("  - %s\n", prefix_word);
        }
        trie_iterator_free(iter);
    }

    // 5. 删除字符串
    const char *del_word = "app";
    if (trie_delete(trie, del_word) == 0) {
        printf("\nDeleted: %s\n", del_word);
    } else {
        printf("\nFailed to delete: %s\n", del_word);
    }

    // 6. 保存 trie 到文件(持久化)
    if (trie_save(trie, "mydict.trie") == 0) {
        printf("Trie saved to mydict.trie\n");
    }

    // 7. 加载 trie 从文件
    Trie *loaded_trie = trie_new_from_file("mydict.trie");
    if (loaded_trie) {
        printf("Trie loaded from mydict.trie\n");
        // 验证加载结果
        if (trie_lookup(loaded_trie, "apple", &data)) {
            printf("Loaded trie has: apple\n");
        }
        trie_free(loaded_trie);
    }

    // 8. 释放 trie 内存
    trie_free(trie);
    return 0;
}

编译:

cpp 复制代码
# 编译(链接 libdatrie 库)
gcc trie_demo.c -o trie_demo -ldatrie

# 运行
./trie_demo

输出结果:

cpp 复制代码
Inserted: apple
Inserted: app
Inserted: application
Inserted: banana
Inserted: bear
Inserted: 中国
Inserted: 中国人
Found: app

Prefix match for 'app':
  - app
  - apple
  - application

Deleted: app
Trie saved to mydict.trie
Trie loaded from mydict.trie
Loaded trie has: apple

5.进阶用法

1.关联自定义数据

每个字符串可关联一个 TrieData 类型的数据(本质是 void*,支持任意数据指针),例如给每个单词绑定一个 ID:

cpp 复制代码
// 插入时关联数据
int word_id = 1001;
trie_insert_data(trie, "apple", (TrieData)&word_id);

// 查询时获取数据
TrieData data;
if (trie_lookup(trie, "apple", &data)) {
    printf("Apple ID: %d\n", *(int*)data);
}

2.通配符匹配

支持 ?(匹配单个字符)和 *(匹配任意长度字符):

cpp 复制代码
// 查找所有以 "app" 开头、后面跟任意字符的字符串
TrieIterator *iter = trie_wildcard_iterator_new(trie, "app*");
while ((const char *word = trie_iterator_next(iter))) {
    printf("Wildcard match: %s\n", word);
}

3.迭代遍历与模式匹配

libdatrie提供了灵活的迭代器API,可以遍历整个Trie树或特定子树。下面的示例展示了如何使用迭代器进行高级遍历:

cpp 复制代码
// 查找所有以特定前缀开头的单词
void find_words_by_prefix(Trie *trie, const char *prefix) {
    AlphaChar *aprefix = cstr_to_alpha(prefix);
    TrieState *state = trie_root(trie);
    if (trie_state_walk(state, aprefix)) {
        printf("以'%s'开头的单词:\n", prefix);
        TrieIterator *iter = trie_iterator_new(state);
        int count = 0;
        while (trie_iterator_next(iter)) {
            AlphaChar *key;
            TrieData data;
            trie_iterator_get_key(iter, &key);
            trie_iterator_get_data(iter, &data);
            printf("  - %ls\n", key);
            count++;
        }
        printf("共找到 %d 个单词\n", count);
        trie_iterator_free(iter);
    } else {
        printf("没有找到以'%s'开头的单词\n", prefix);
    }
    trie_state_free(state);
}

// 遍历整个Trie树并统计信息
void traverse_entire_trie(Trie *trie) {
    TrieIterator *iter = trie_iterator_new(trie_root(trie));
    int total_words = 0;
    printf("字典内容:\n");
    while (trie_iterator_next(iter)) {
        AlphaChar *key;
        TrieData data;
        trie_iterator_get_key(iter, &key);
        trie_iterator_get_data(iter, &data);
        printf("  %ls -> %d\n", key, (int)data);
        total_words++;
    }
    printf("总单词数: %d\n", total_words);
   trie_iterator_free(iter);
}

4.批量构建优化

大规模数据插入时,先排序字符串再插入,可减少双数组的重构开销:

cpp 复制代码
// 对字符串数组排序
qsort(words, word_count, sizeof(char*), strcmp);

// 批量插入
for (int i = 0; i < word_count; i++) {
    trie_insert(trie, words[i]);
}

6.应用场景

  • 字典查询(如英汉词典、拼音输入法词库)
  • 关键词过滤(如敏感词检测)
  • 自动补全 / 拼写检查(如搜索框提示)
  • 网络路由表(最长前缀匹配)
  • DNA / 蛋白质序列匹配(生物信息学)
  • 日志关键词提取

7.注意事项

  1. 字符编码:默认仅支持 UTF-8 编码,若需处理其他编码(如 GBK),需先转换为 UTF-8。
  2. 内存管理:插入的字符串会被 trie 树内部复制,无需手动管理,但需确保传入的字符串是合法的 UTF-8。
  3. 线程安全:libdatrie 本身不是线程安全的,多线程环境下需加锁保护。
  4. 数据持久化:保存的 trie 文件是二进制格式,不可直接编辑,需通过 API 加载 / 修改。

8.总结

libdatrie 是一款 高性能、轻量级 的字符串处理库,在大规模字符串检索场景下(如字典、关键词过滤)表现远超传统的数据结构(哈希表、红黑树)。其 API 简洁直观,支持多语言和丰富的查询功能,是 C 语言项目中处理字符串集合的优选方案。

如果需要进一步优化性能,还可以关注其衍生库 libdatrie++ (C++ 封装)或 pygtrie(Python 绑定)。

相关推荐
程序猿_极客2 小时前
【2025最新】 Java入门到实战:包装类、字符串转换、equals/toString + 可变字符串,一篇搞定开发高频场景(含案例解析)
java·开发语言·java进阶·面试核心·java快速入门
侯小啾2 小时前
【23】C语言 左移(<<) 与 右移(>>) 位运算符在处理像素中的应用
c语言·算法·位运算·右移·左移
U***e632 小时前
Python测试
开发语言·python
EXtreme352 小时前
C语言自定义类型详解:结构体、联合体、位段与内存对齐实战指南
c语言·结构体·内存对齐
yi碗汤园2 小时前
Visual Studio常用的快捷键
开发语言·ide·c#·编辑器·visual studio
Elias不吃糖3 小时前
NebulaChat:C++ 高并发聊天室服务端
开发语言·c++·redis·sql·项目文档
haofafa3 小时前
JavaScript性能优化实战
开发语言·javascript·性能优化
帅中的小灰灰3 小时前
C++编程策略设计模式
开发语言·c++·设计模式
O***p6043 小时前
JavaScript增强现实开发
开发语言·javascript·ar