目录
[2.双数组字典树(Double-Array Trie)的原理](#2.双数组字典树(Double-Array Trie)的原理)
[4.核心 API 使用示例](#4.核心 API 使用示例)
1.简介
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.安装方法
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.注意事项
- 字符编码:默认仅支持 UTF-8 编码,若需处理其他编码(如 GBK),需先转换为 UTF-8。
- 内存管理:插入的字符串会被 trie 树内部复制,无需手动管理,但需确保传入的字符串是合法的 UTF-8。
- 线程安全:libdatrie 本身不是线程安全的,多线程环境下需加锁保护。
- 数据持久化:保存的 trie 文件是二进制格式,不可直接编辑,需通过 API 加载 / 修改。
8.总结
libdatrie 是一款 高性能、轻量级 的字符串处理库,在大规模字符串检索场景下(如字典、关键词过滤)表现远超传统的数据结构(哈希表、红黑树)。其 API 简洁直观,支持多语言和丰富的查询功能,是 C 语言项目中处理字符串集合的优选方案。
如果需要进一步优化性能,还可以关注其衍生库 libdatrie++ (C++ 封装)或 pygtrie(Python 绑定)。