当前位置: 首页 > news >正文

【数据结构与算法】第43篇:Trie树(前缀树/字典树)

目录

一、什么是Trie树

1.1 定义

1.2 特点

二、Trie树的实现

2.1 节点结构

2.2 创建节点

2.3 插入单词

2.4 搜索单词

2.5 前缀匹配

2.6 统计以某前缀开头的单词数

三、完整代码演示

四、Trie树的变体

4.1 压缩Trie(Radix Tree)

4.2 三叉Trie(Ternary Search Tree)

五、Trie vs 哈希表

六、实际应用

七、小结

八、思考题


一、什么是Trie树

1.1 定义

Trie树(前缀树/字典树)是一种多叉树,每个节点代表一个字符,从根到某个节点的路径构成一个字符串。

示例:插入"cat","car","dog","do","doggy"

text

root / \ c d / \ a o / \ \ t r g | | | (cat)(car) (dog) | g | (doggy)

1.2 特点

优点缺点
插入/查找时间复杂度 O(L)空间消耗大(每个节点固定大小数组)
支持前缀查询字符集大时内存爆炸
自动排序(字典序)不适合频繁删除

二、Trie树的实现

2.1 节点结构

c

#include <stdio.h> #include <stdlib.h> #include <string.h> #define ALPHABET_SIZE 26 // 小写字母 typedef struct TrieNode { struct TrieNode *children[ALPHABET_SIZE]; int isEnd; // 是否是一个单词的结尾 int count; // 经过该节点的单词数(可选,用于统计) } TrieNode;

2.2 创建节点

c

TrieNode* createNode() { TrieNode *node = (TrieNode*)malloc(sizeof(TrieNode)); for (int i = 0; i < ALPHABET_SIZE; i++) { node->children[i] = NULL; } node->isEnd = 0; node->count = 0; return node; }

2.3 插入单词

c

void insert(TrieNode *root, const char *word) { TrieNode *cur = root; int len = strlen(word); for (int i = 0; i < len; i++) { int idx = word[i] - 'a'; if (cur->children[idx] == NULL) { cur->children[idx] = createNode(); } cur = cur->children[idx]; cur->count++; } cur->isEnd = 1; }

2.4 搜索单词

c

int search(TrieNode *root, const char *word) { TrieNode *cur = root; int len = strlen(word); for (int i = 0; i < len; i++) { int idx = word[i] - 'a'; if (cur->children[idx] == NULL) { return 0; } cur = cur->children[idx]; } return cur->isEnd; }

2.5 前缀匹配

c

int startsWith(TrieNode *root, const char *prefix) { TrieNode *cur = root; int len = strlen(prefix); for (int i = 0; i < len; i++) { int idx = prefix[i] - 'a'; if (cur->children[idx] == NULL) { return 0; } cur = cur->children[idx]; } return 1; }

2.6 统计以某前缀开头的单词数

c

int countPrefix(TrieNode *root, const char *prefix) { TrieNode *cur = root; int len = strlen(prefix); for (int i = 0; i < len; i++) { int idx = prefix[i] - 'a'; if (cur->children[idx] == NULL) { return 0; } cur = cur->children[idx]; } return cur->count; }

三、完整代码演示

c

#include <stdio.h> #include <stdlib.h> #include <string.h> #define ALPHABET_SIZE 26 typedef struct TrieNode { struct TrieNode *children[ALPHABET_SIZE]; int isEnd; int count; } TrieNode; TrieNode* createNode() { TrieNode *node = (TrieNode*)malloc(sizeof(TrieNode)); for (int i = 0; i < ALPHABET_SIZE; i++) { node->children[i] = NULL; } node->isEnd = 0; node->count = 0; return node; } void insert(TrieNode *root, const char *word) { TrieNode *cur = root; int len = strlen(word); for (int i = 0; i < len; i++) { int idx = word[i] - 'a'; if (cur->children[idx] == NULL) { cur->children[idx] = createNode(); } cur = cur->children[idx]; cur->count++; } cur->isEnd = 1; } int search(TrieNode *root, const char *word) { TrieNode *cur = root; int len = strlen(word); for (int i = 0; i < len; i++) { int idx = word[i] - 'a'; if (cur->children[idx] == NULL) { return 0; } cur = cur->children[idx]; } return cur->isEnd; } int startsWith(TrieNode *root, const char *prefix) { TrieNode *cur = root; int len = strlen(prefix); for (int i = 0; i < len; i++) { int idx = prefix[i] - 'a'; if (cur->children[idx] == NULL) { return 0; } cur = cur->children[idx]; } return 1; } int countPrefix(TrieNode *root, const char *prefix) { TrieNode *cur = root; int len = strlen(prefix); for (int i = 0; i < len; i++) { int idx = prefix[i] - 'a'; if (cur->children[idx] == NULL) { return 0; } cur = cur->children[idx]; } return cur->count; } // 获取所有以某前缀开头的单词(DFS收集) void collectWords(TrieNode *root, char *prefix, int depth, char **result, int *count) { if (root->isEnd) { result[*count] = (char*)malloc((depth + 1) * sizeof(char)); strncpy(result[*count], prefix, depth); result[*count][depth] = '\0'; (*count)++; } for (int i = 0; i < ALPHABET_SIZE; i++) { if (root->children[i] != NULL) { prefix[depth] = 'a' + i; collectWords(root->children[i], prefix, depth + 1, result, count); } } } char** autoComplete(TrieNode *root, const char *prefix, int *returnSize) { // 先找到前缀节点 TrieNode *cur = root; int len = strlen(prefix); for (int i = 0; i < len; i++) { int idx = prefix[i] - 'a'; if (cur->children[idx] == NULL) { *returnSize = 0; return NULL; } cur = cur->children[idx]; } // 收集所有单词 int capacity = cur->count; char **result = (char**)malloc(capacity * sizeof(char*)); char *buffer = (char*)malloc(100 * sizeof(char)); strcpy(buffer, prefix);
*returnSize = 0; collectWords(cur, buffer, len, result, returnSize); free(buffer); return result; } void freeTrie(TrieNode *root) { if (root == NULL) return; for (int i = 0; i < ALPHABET_SIZE; i++) { freeTrie(root->children[i]); } free(root); } int main() { TrieNode *root = createNode(); // 插入单词 char *words[] = {"cat", "car", "dog", "do", "doggy", "apple", "app", "application"}; int n = sizeof(words) / sizeof(words[0]); for (int i = 0; i < n; i++) { insert(root, words[i]); } printf("=== Trie树测试 ===\n\n"); // 搜索测试 printf("搜索 'cat': %s\n", search(root, "cat") ? "找到" : "未找到"); printf("搜索 'dog': %s\n", search(root, "dog") ? "找到" : "未找到"); printf("搜索 'do': %s\n", search(root, "do") ? "找到" : "未找到"); printf("搜索 'cafe': %s\n", search(root, "cafe") ? "找到" : "未找到"); // 前缀匹配测试 printf("\n以 'ca' 开头的单词: %s\n", startsWith(root, "ca") ? "存在" : "不存在"); printf("以 'app' 开头的单词: %s\n", startsWith(root, "app") ? "存在" : "不存在"); // 统计前缀数量 printf("\n以 'ca' 开头的单词数: %d\n", countPrefix(root, "ca")); printf("以 'do' 开头的单词数: %d\n", countPrefix(root, "do")); printf("以 'app' 开头的单词数: %d\n", countPrefix(root, "app")); // 自动补全测试 int size; char *prefix = "ca"; char **suggestions = autoComplete(root, prefix, &size); printf("\n自动补全 '%s':\n", prefix); for (int i = 0; i < size; i++) { printf(" %s\n", suggestions[i]); free(suggestions[i]); } free(suggestions); prefix = "app"; suggestions = autoComplete(root, prefix, &size); printf("\n自动补全 '%s':\n", prefix); for (int i = 0; i < size; i++) { printf(" %s\n", suggestions[i]); free(suggestions[i]); } free(suggestions); freeTrie(root); return 0; }

运行结果:

text

=== Trie树测试 === 搜索 'cat': 找到 搜索 'dog': 找到 搜索 'do': 找到 搜索 'cafe': 未找到 以 'ca' 开头的单词: 存在 以 'app' 开头的单词: 存在 以 'ca' 开头的单词数: 2 以 'do' 开头的单词数: 3 以 'app' 开头的单词数: 3 自动补全 'ca': ca car cat 自动补全 'app': app apple application

四、Trie树的变体

4.1 压缩Trie(Radix Tree)

将只有一个孩子的节点压缩成一个边,节省空间。

text

普通Trie: 压缩Trie: c c | | a at → isEnd | | t → isEnd ar → isEnd | a (另一个)

4.2 三叉Trie(Ternary Search Tree)

每个节点有三个孩子(小于、等于、大于),适合内存受限场景。

c

typedef struct TSTNode { char ch; struct TSTNode *left; // 小于 struct TSTNode *mid; // 等于 struct TSTNode *right; // 大于 int isEnd; } TSTNode;

五、Trie vs 哈希表

对比项Trie树哈希表
查找时间复杂度O(L)O(1) 平均
前缀查询支持不支持
有序遍历支持(字典序)不支持
空间消耗大(每个节点指针数组)
哈希冲突
适用场景前缀匹配、自动补全精确查找

六、实际应用

应用说明
搜索引擎自动补全根据输入前缀推荐搜索词
拼写检查快速判断单词是否存在
IP路由最长前缀匹配
单词统计统计文本中单词出现次数
敏感词过滤高效匹配敏感词列表

七、小结

这一篇我们学习了Trie树:

操作时间复杂度实现要点
插入O(L)逐字符创建节点
搜索O(L)逐字符查找
前缀匹配O(L)同搜索,不检查isEnd
自动补全O(L + K)DFS收集所有单词

核心结构

  • 每个节点有26个指针(小写字母)

  • isEnd标记单词结尾

  • count统计经过节点单词数

适用场景

  • 大量字符串的前缀匹配

  • 自动补全、拼写检查

  • 字典序输出

下一篇我们讲堆的实现(优先队列)。


八、思考题

  1. Trie树的根节点是否代表空字符串?它需要存储字符吗?

  2. 如果要处理大小写字母和数字,节点结构如何修改?

  3. 如何用Trie树实现单词的删除操作?

  4. Trie树的空间消耗主要在哪里?如何优化?

欢迎在评论区讨论你的答案。

http://www.cnnetsun.cn/news/1876881.html

相关文章:

  • 通义千问3-4B真实体验:本地部署生成测试用例,效率提升实测
  • 压力测试下的心态管理:当线上告警电话响起时
  • 别再瞎测了!手把手教你用泰克/安捷伦示波器搞定USB2.0信号质量(Device/Hub模式实战)
  • 从HTTP到K8s探针:彻底搞懂HTTPGet健康检查
  • 全网最全:零基础学深度学习需要学哪些框架?PyTorch 和 TensorFlow 选哪个?
  • 告别手写脚本!用Frida-Trace自动Hook Android App的Java方法(附实战Demo)
  • ToClaw真的能让AI Agent落地吗?先看清它的代价与边界
  • Python语言的12个基础知识点小结
  • 基于STM32的智能家居安防系统设计与实现
  • LangChain4j 1.0.0-beta2踩坑记:从社区版DashScope依赖到SpringBoot自动配置的完整避坑指南
  • 扣子(Coze)实战:10万+治愈奶奶图文,Coze一键生成
  • Simulink信号解析避坑指南:为什么你的‘蓝色鱼叉’图标不出现?
  • [Unity] ShaderGraph实战:动态水面倒影与镜面反射效果优化
  • SDXL 1.0电影级绘图工坊:Mathtype公式渲染与科学图表生成
  • SQL如何获取分组最后一条数据_LAST_VALUE的滑动窗口陷阱
  • Kubernetes v1.36 云原生架构新特性详解:生产级集群升级指南
  • devops系列(二) Git 工作流与版本控制:团队协作不踩坑
  • Java 从入门到精通(十五):线程同步与 synchronized,为什么多个线程改同一个变量时结果总会乱?
  • 收藏 | 零基础小白也能看懂:Transformer大模型是如何炼成的
  • HJ175 小红的整数配对
  • 短视频商城APP源码开发:技术、功能与运营全链路解决方案
  • 华为OD机试 - 魔法收积木 - 二进制(Python/JS/C/C++ 新系统 200分)
  • VS Code 插件系统深度剖析
  • SpringCloud微服务进阶-Nacos更加全能的注册中心澈
  • 消息队列Kafka与RabbitMQ深度解析:把分布式消息核心讲透,吊打面试官
  • ASTM D4169视网膜下注射套件的包装运输验证方案
  • 三相UVW的时间分配
  • MT6826S磁编码器:高精度与强抗干扰的工业级解决方案
  • AI Agent岗位面试通过率有多低:真实数据
  • 三维地图可视化 ThreeJS vue 开源项目