news 2026/6/15 17:45:22

数据结构 Trie树(字典树、前缀树)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据结构 Trie树(字典树、前缀树)

1.什么是Trie树


Trie树,又叫字典树、前缀树(Prefix Tree)、单词查找树 或 键树,是一种多叉树结构。如下图:

上图是一棵Trie树,表示了关键字集合{“a”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”} 。从上图可以归纳出Trie树的基本性质:

  1. 根节点不包含字符,除根节点外的每一个子节点都包含一个字符。
  2. 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
  3. 每个节点的所有子节点包含的字符互不相同。

通常在实现的时候,会在节点结构中设置一个标志,用来标记该结点处是否构成一个单词(关键字)。

可以看出,Trie树的关键字一般都是字符串,而且Trie树把每个关键字保存在一条路径上,而不是一个结点中。另外,两个有公共前缀的关键字,在Trie树中前缀部分的路径相同,所以Trie树又叫做前缀树(Prefix Tree)。


我们可以举个例子

  • 原字符串集合:{ abcde 、aced 、bcdf 、bcff }
  • 插入字符串:aced 、cdaa
  • 新字符串集合:{ abcde 、abde、aced 、bcdf 、bcff 、cdaa }


说明:图中 ※ 为 字符串结束的标记 。

  • 若某结点上有标记 ※,则说明 从根结点 到该结点为止,每个结点表示的字符 相连 组成的字符串存在于集合中。
  • 相应的,对于 没有标记 的结点,串联成的字符串只是原有字符串的 一部分 ,不属于 字符串集合。

2.字典树的应用场景


当我们在字典树的每⼀个结点位置,额外维护⼀些信息时,就可以做到很多事情:

  1. 查询某个单词是否出现过,并且出现⼏次;
  2. 查询有多少个单词是以某个字符串为前缀;
  3. 查询所有以某个前缀开头的单词;(这个作⽤可以⽤到输⼊法中,输⼊拼⾳的时候,可以提⽰可能 的单词)

当然,除了上述作⽤以外,字典树还可以解决别的问题,后续可以在做题中体会。

3.字典树的实现

基本结构

字典树的每个节点包含:

  • 子节点指针(通常是一个数组或映射)

  • 一个标志位,表示从根节点到该节点的路径是否构成一个完整的单词

  • 可能包含其他辅助信息(如词频)

这里需要理解一个非常重要的问题:怎么理解idx?

3.1. 插入 ( insert ) 操作


将字符串 逐个拆分 成 单个字符,从第一个字符开始与 Trie 树的 根结点开始 的 子结点 比对,

若有结点具有 相同 的字符,则以同样的方式对比 下一个字符 与该结点的 子结点 。

举例:

在 图 1 中插入字符串 abde ( 拆分:a/b/d/e)

  • 对于字符 a ,根结点 的 子结点 中有 a ,向后判断 下一个字符 b 与 结点 a 的 子结点;
  • 对于字符 b ,a 结点的 子结点 中有 b ,向后判断下一个字符 d 与 结点 b 的子结点;
  • 对于字符 d ,b 结点的子结点中 没有 d ,因此要创建新结点 d,向后 判断下一个字符 e 与 结点 d 的 子结点;
  • 对于字符 e ,d 结点的子结点中没有 e ,因此要创建新结点 e

( 从创建了一个新结点起,后面剩下的字符显然都要创建新的结点,但其 操作和之前的判断相同 )

举例:

在 图 2 中插入字符串 cdaa ( 拆分:c/d/a/a)

对于字符 c ,根结点的子结点中没有 c ,因此要创建新结点 c,剩下的字符 daa 以相同的操作,逐个创建,以存储字符串。
注意:在插入完字符串之后,要给末尾的结点加上 结束标记

现在我们就能写出代码了,首先我们先定义好我们需要的一些数据结构。

#include <iostream> using namespace std; const int N = 100010; int son[N][26]; // 存储每个结点的所有儿子结点 // 在题目中,字符串只包含 26 个小写字母 // 因此每个结点最多只会向外连 26 条边 ( 最多只有 26 个儿子结点 ) int e[N]; // count,记录以这个结点结尾的单词有多少个 // 它在本模板中作为 字符串结束标记 int idx; // 存储当前已经使用到的结点的下标 // 下标是 0 的结点,既是根节点,又是空结点 int p[N]; //记录每个节点被经过的次数, //p[i] 表示有多少个单词的前缀经过节点 i。例如,根节点的 p[0] 等于插入的单词总数,因为所有单词都从根节点开始。

这里先回答一个问题:如何理解idx?

idx 是字典树中节点的动态分配标识符,用于唯一标记每一个新创建的节点。它的核心作用是:

  • 唯一性:确保每个新节点有唯一的索引,避免路径冲突。
  • 动态扩展:按需分配节点,无需预先固定树的结构。

为什么需要 idx?

字典树的本质是用节点路径表示字符串,例如字符串 "apple" 的路径是:

根节点 → a → p → p → l → e

  • 根节点的索引固定为 0。
  • 其他节点需要动态创建,例如:

根节点的子节点 a 可能对应索引 1。

a 的子节点 p 可能对应索引 2,依此类推。

idx 的作用就是为这些新创建的节点分配一个全局唯一的索引,确保路径的唯一性。

idx 的工作流程

  1. 初始化:程序开始时,idx = 0(根节点已存在,无需分配)。
  2. 插入字符串:

当需要创建新节点时,执行 son[cur][path] = ++idx。

++idx 会先递增 idx,再将新值赋给节点。

例如,第一个新节点的索引是 1,第二个是 2,依此类推。

具体示例

假设插入字符串 "abc":

根节点 0:

  1. p[0]++(根节点被经过次数+1)。
  2. 字符 'a' 对应路径 0(path = 'a'-'a' = 0)。
  3. 检查 tree[0][0],若未创建(初始为0),则分配新节点 tree[0][0] = ++idx = 1。

节点 1:

  1. p[1]++(经过次数+1)。
  2. 字符 'b' 对应路径 1。
  3. 检查 tree[1][1],若未创建,分配 tree[1][1] = ++idx = 2。

节点 2:

  1. p[2]++。
  2. 字符 'c' 对应路径 2。
  3. 检查 tree[2][2],若未创建,分配 tree[2][2] = ++idx = 3。

结束:

1. e[3]++(标记字符串结尾)。

最终树的结构为:

根节点 (0) → a (1) → b (2) → c (3)



关键问题解答

为什么不用固定索引?

字符串的组合是无限的,无法预先确定需要多少节点。idx 按需分配,节省内存。

idx 会重复吗?

不会。idx 严格递增,每个新节点分配的索引唯一。

idx 和数组大小 N 的关系

N 需足够大(如 1e6),否则 idx 超过 N 会导致数组越界。

接下来我们就能写出这个代码

// 插入字符串到字典树 void insert(string& s) { int cur = 0; // 从根节点(索引0)开始遍历 p[cur]++; // 根节点被经过次数+1(总单词数统计) for (auto ch : s) // 遍历字符串的每个字符 { int path = ch - 'a'; // 将字符转换为路径编号(a=0, b=1...z=25) // 如果当前路径不存在,则创建新节点 if (son[cur][path] == 0) { son[cur][path] = ++idx; // 分配新节点,idx递增 } cur = son[cur][path]; // 移动到子节点 p[cur]++; // 当前节点经过次数+1 } e[cur]++; // 循环结束时,cur位于字符串末尾节点,标记单词结尾次数+1 }

3.2. 查询 ( query ) 操作


与插入操作的原理基本相同,

  • 将需要查询的字符串 逐个拆分 成单个字符,逐个与 Trie 树中的结点比较,直至发现 Trie 树中 不存在 的字符,或要查询的字符串的各个字符都 比较完成 。
  • 对于发现 Trie 树中不存在的字符,一旦发现,就能确定要查询的字符串不属于原集合。

举例:

在图 3 中查询 acwing ,
能找到 a → c ,但 c 的子结点中没有 w ,则确定集合中不存在该字符串

对于要查询的字符串的各个字符都比较完成,则存在两种情况:

① 要查询的字符的确属于集合;
② 要查询的字符是集合中某个字符串的前缀。

举例:

在图 3 中查询 ac ,能找到 a → c ,字符串比较完成。

  • 因为 ac 是原有字符串 aced 的一部分 ( 前缀 )。
  • 此时就需要用到结点上的 字符串结束标记 了。
  • 在 ac 的查询过程中,最后判断的结点落在了结点 c 上,但该结点没有 字符串结束标记 ,因此可以判断 ac 不属于原集合。

举例:

在图 3 中查询 aced ,最后判断的结点落在了结点 d 上,
该结点具有 字符串结束标记 ,因此可以判断 aced 属于原集合。

查询完整字符串出现的次数

// 查询完整字符串出现次数 int find(string& s) { int cur = 0; // 从根节点开始查找 for (auto ch : s) { int path = ch - 'a'; if (son[cur][path] == 0) { return 0; // 路径不存在,说明单词不存在 } cur = son[cur][path]; // 沿路径向下移动 } return e[cur]; // 返回结尾节点的单词计数 }

查询有多少个单词以字符串s为前缀

// 查询以s为前缀的单词数量 int find_pre(string& s) { int cur = 0; for (auto ch : s) { int path = ch - 'a'; // 原代码中的get_num(ch)应为ch-'a',否则无法编译 if (son[cur][path] == 0) { return 0; // 前缀路径不存在 } cur = son[cur][path]; } return p[cur]; // 返回该前缀最后一个节点的经过次数 }

复杂度分析

操作时间复杂度空间复杂度
插入O(L)O(L)
查找O(L)O(1)
前缀搜索O(P)O(1)

L: 单词长度,P: 前缀长度

变体与优化

1. 压缩字典树(Radix Tree)

  • 合并只有一个子节点的路径

  • 进一步节省空间

2. 双数组字典树

  • 用两个数组代替指针

  • 内存更紧凑,访问更快

3. 后缀树

  • 存储字符串的所有后缀

  • 用于字符串匹配、查找最长重复子串

总结

字典树是处理字符串相关问题的强大工具,特别适合前缀匹配、自动补全和字典查找场景。虽然在某些情况下内存开销较大,但其O(L)的查询时间复杂度优雅的前缀处理能力使其在许多实际应用中不可或缺。

选择使用字典树时,需要根据具体需求考虑字符集大小、内存限制和查询模式,必要时可以采用压缩优化或混合数据结构来平衡性能与资源消耗。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/15 13:52:58

YOLOFuse游乐园设施安全监控:游客违规行为识别

YOLOFuse游乐园设施安全监控&#xff1a;游客违规行为识别 在大型游乐园的运营现场&#xff0c;一个看似平静的夜晚却暗藏风险——昏暗的灯光下&#xff0c;一名游客翻越护栏试图进入维修区域&#xff0c;而传统摄像头因光线不足未能及时捕捉这一危险动作。直到安保人员巡检时才…

作者头像 李华
网站建设 2026/6/15 13:53:28

【2025最新】基于SpringBoot+Vue的新冠物资管理系统管理系统源码+MyBatis+MySQL

摘要 新冠疫情的爆发对全球公共卫生系统提出了严峻挑战&#xff0c;物资管理成为疫情防控的关键环节。传统物资管理方式依赖人工操作&#xff0c;效率低下且易出错&#xff0c;难以应对突发公共卫生事件的大规模物资调配需求。为提升物资管理的智能化水平&#xff0c;开发一套高…

作者头像 李华
网站建设 2026/6/15 11:19:47

传输门与双向开关设计:逻辑门扩展应用实战

传输门与双向开关设计&#xff1a;从晶体管到系统级互连的实战解析在数字电路的世界里&#xff0c;我们习惯于将“逻辑门”视为布尔运算的基本积木——与、或、非&#xff0c;构成了组合逻辑的基石。但当你深入芯片内部&#xff0c;真正决定数据如何流动的&#xff0c;往往不是…

作者头像 李华
网站建设 2026/6/15 14:20:22

零基础入门:Multisim安装与实验平台搭建

从零开始搭建电路实验台&#xff1a;Multisim 安装与仿真入门实战你是否曾因为手头没有示波器、信号源&#xff0c;就只能对着课本上的RC电路干瞪眼&#xff1f;你是否在做模电作业时&#xff0c;想验证一个放大电路却苦于焊接失败、元件烧毁&#xff1f;别担心——现在&#x…

作者头像 李华
网站建设 2026/6/15 12:22:41

YOLOFuse输电线路覆冰监测:形变+温度联合判断

YOLOFuse输电线路覆冰监测&#xff1a;形变温度联合判断 在高海拔、寒冷地区的电网运维现场&#xff0c;一场突如其来的冻雨往往意味着巨大挑战。导线表面逐渐堆积的冰层不仅悄然增加机械负荷&#xff0c;更可能引发断线、倒塔甚至大面积停电。传统依赖气象站数据或人工巡检的方…

作者头像 李华
网站建设 2026/6/15 11:20:00

工业环境下USB转485驱动安装与调试指南

工业现场实战&#xff1a;USB转485驱动安装与通信调试全解析 在工控一线&#xff0c;你是否遇到过这样的场景&#xff1f;——新上位机接不上老设备&#xff0c;PLC数据读不出来&#xff0c;现场排查一圈才发现是 USB转485模块没被识别 。重启、换线、重装驱动……半小时过去…

作者头像 李华