news 2026/2/22 7:31:53

Java版LeetCode热题100之实现 Trie(前缀树):从原理到实战的深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java版LeetCode热题100之实现 Trie(前缀树):从原理到实战的深度解析

Java版LeetCode热题100之实现 Trie(前缀树):从原理到实战的深度解析

摘要:本文将深入剖析 LeetCode 热题 100 中的经典题目——实现 Trie(前缀树)。我们将从原题出发,逐步拆解其背后的算法思想、数据结构设计,并提供完整可运行的 Java 实现代码。文章涵盖时间/空间复杂度分析、常见面试问题、实际应用场景、相关题目推荐等模块,旨在帮助读者全面掌握 Trie 的核心原理与工程实践。


一、原题回顾

题目名称:实现 Trie(前缀树)
题目编号:LeetCode 208
难度等级:中等

题目描述

Trie(发音类似 “try”)或者说前缀树是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。

请你实现Trie类:

  • Trie()初始化前缀树对象。
  • void insert(String word)向前缀树中插入字符串word
  • boolean search(String word)如果字符串word在前缀树中,返回true(即,在检索之前已经插入);否则,返回false
  • boolean startsWith(String prefix)如果之前已经插入的字符串word的前缀之一为prefix,返回true;否则,返回false

示例

输入:["Trie","insert","search","search","startsWith","insert","search"][[],["apple"],["apple"],["app"],["app"],["app"],["app"]]输出:[null,null,true,false,true,null,true]解释:Trietrie=newTrie();trie.insert("apple");trie.search("apple");// 返回 Truetrie.search("app");// 返回 Falsetrie.startsWith("app");// 返回 Truetrie.insert("app");trie.search("app");// 返回 True

提示

  • 1 <= word.length, prefix.length <= 2000
  • wordprefix仅由小写英文字母组成
  • insertsearchstartsWith调用次数总计不超过3 * 10^4

二、原题分析

本题要求我们实现一个支持三种基本操作的数据结构:插入字符串精确查找字符串判断是否存在以某前缀开头的字符串

乍看之下,这似乎可以用哈希表(HashMap)解决:插入时存入 key,查找时直接判断 key 是否存在。但这种方法无法高效支持“前缀匹配”操作(startsWith)。例如,若已插入"apple""application""apply",当查询前缀"app"时,哈希表需要遍历所有 key 才能判断是否存在匹配项,效率低下。

Trie(前缀树)正是为解决此类问题而生的数据结构。它通过共享公共前缀的方式组织字符串,使得前缀匹配可以在 O(L) 时间内完成(L 为前缀长度),无需遍历整个数据集。


三、答案构思

3.1 Trie 的核心思想

Trie 是一棵多叉树,每个节点代表一个字符。从根节点到任意节点的路径构成一个字符串前缀。关键特性如下:

  • 根节点不存储字符,仅作为起点。
  • 每个节点包含若干子节点,对应可能的下一个字符。
  • 标记结束节点:某些节点被标记为“单词结尾”,表示从根到该节点的路径构成一个完整单词。

例如,插入"app""apple"后的 Trie 结构如下(简化图示):

(root) | a | p | p ← isEnd = true ("app") | l | e ← isEnd = true ("apple")

可以看到,两个单词共享了前缀"app",节省了存储空间,同时支持高效的前缀查询。

3.2 数据结构设计

我们需要定义一个Trie类,包含以下成员:

  • children: 长度为 26 的数组,children[i]表示当前节点的第 i 个子节点(对应'a' + i字符)。
  • isEnd: 布尔值,标记当前节点是否为某个单词的结尾。

为什么用数组而不是 Map?
因为题目限定字符仅为小写字母(共 26 个),使用固定大小数组访问更快(O(1)),且内存连续性更好。若字符集更大或不确定,可改用HashMap<Character, Trie>

3.3 操作逻辑

插入(insert)
  1. 从根节点开始。
  2. word中每个字符ch
    • 计算索引index = ch - 'a'
    • children[index] == null,则新建一个子节点
    • 移动到该子节点
  3. 最后将当前节点的isEnd设为true
查找完整单词(search)
  1. 调用辅助方法searchPrefix(word)获取对应节点
  2. 若节点非空isEnd == true,则返回true
判断前缀存在(startsWith)
  1. 调用searchPrefix(prefix)
  2. 若返回非空节点,则说明存在该前缀
辅助方法:searchPrefix
  • 从根开始,逐字符向下遍历
  • 若某字符对应的子节点为空,返回null
  • 否则返回最终到达的节点

四、完整答案(Java 实现)

classTrie{privateTrie[]children;privatebooleanisEnd;/** * 初始化 Trie 根节点 */publicTrie(){// 26 个小写字母children=newTrie[26];isEnd=false;}/** * 插入一个单词到 Trie 中 */publicvoidinsert(Stringword){Trienode=this;// 从根节点开始for(inti=0;i<word.length();i++){charch=word.charAt(i);intindex=ch-'a';// 转换为 0~25 的索引// 如果该字符对应的子节点不存在,创建新节点if(node.children[index]==null){node.children[index]=newTrie();}// 移动到子节点node=node.children[index];}// 标记当前节点为单词结尾node.isEnd=true;}/** * 查找单词是否存在于 Trie 中(必须是完整单词) */publicbooleansearch(Stringword){Trienode=searchPrefix(word);// 节点存在 且 是单词结尾returnnode!=null&&node.isEnd;}/** * 判断是否存在以 prefix 开头的单词 */publicbooleanstartsWith(Stringprefix){// 只需判断前缀路径是否存在returnsearchPrefix(prefix)!=null;}/** * 辅助方法:查找 prefix 对应的最后一个节点 * @return 若存在则返回节点,否则返回 null */privateTriesearchPrefix(Stringprefix){Trienode=this;for(inti=0;i<prefix.length();i++){charch=prefix.charAt(i);intindex=ch-'a';// 路径中断,前缀不存在if(node.children[index]==null){returnnull;}node=node.children[index];}returnnode;}}

✅ 该实现完全符合题目要求,可通过 LeetCode 全部测试用例。


五、代码分析

5.1 类结构

  • Trie类既是节点类型,也是整棵树的入口(根节点)。
  • 每个Trie实例代表树中的一个节点。
  • 使用this作为根节点进行操作,符合面向对象设计。

5.2 关键方法解析

insert
  • 时间复杂度:O(L),L 为 word 长度
  • 每次只处理一个字符,最多创建 L 个新节点
  • 最后设置isEnd = true是关键,区分“前缀”和“完整单词”
searchvsstartsWith
  • 两者都依赖searchPrefix
  • 区别在于:search要求节点是单词结尾startsWith只要求路径存在
searchPrefix
  • 封装了公共逻辑,避免代码重复
  • 返回节点而非布尔值,便于上层判断

5.3 边界情况处理

  • 空字符串?题目规定长度 ≥1,无需处理
  • 重复插入?多次插入同一单词,isEnd仍为true,无副作用
  • 查询不存在的前缀?searchPrefix返回null,上层安全处理

六、时间复杂度与空间复杂度分析

6.1 时间复杂度

操作时间复杂度说明
Trie()初始化O(1)仅分配 26 个引用(未创建子节点)
insert(word)O(L)L = word.length(),每个字符处理一次
search(word)O(L)同上
startsWith(prefix)O(L)L = prefix.length()

所有操作的时间复杂度仅与字符串长度相关,与已插入单词总数无关!这是 Trie 相比哈希表的最大优势。

6.2 空间复杂度

  • 最坏情况:所有插入的单词无公共前缀(如 “a”, “b”, “c”, …)

    • 每个单词需独立路径
    • 总节点数 ≈ 所有单词长度之和
    • 每个节点占用26 * 引用大小 + 1 * boolean
    • 空间复杂度:O(N × L × Σ),其中:
      • N:单词数量
      • L:平均单词长度
      • Σ:字符集大小(本题 Σ=26)
  • 最好情况:所有单词共享长前缀(如 “app”, “apple”, “application”)

    • 节点复用率高,空间节省显著

实际应用中,英文单词通常有大量公共前缀,Trie 的空间效率较高。


七、常见问题解答(FAQ)

Q1: 为什么不用 HashMap 存储子节点?

:本题字符集固定(26 小写字母),数组访问更快(直接索引 vs 哈希计算),且内存更紧凑。若字符集大(如 Unicode)或稀疏,可用Map<Character, Trie>节省空间。

Q2: 如何删除一个单词?

:LeetCode 本题不要求,但可扩展:

  • 先查找单词是否存在
  • 从尾部向上回溯,若某节点无子节点且非其他单词结尾,可删除
  • 需注意:不能破坏其他共享前缀的单词

Q3: Trie 能处理中文吗?

:可以,但需调整:

  • 字符集变大(常用汉字约 3500+)
  • 改用Map<Character, Trie>Trie[]动态扩容
  • 内存开销显著增加

Q4:isEnd的作用是什么?

:区分“路径存在”和“完整单词”。例如:

  • 插入 “app” 后,“a”、“ap” 路径存在,但不是单词
  • 只有 “app” 的末节点isEnd = true

八、优化思路

8.1 空间优化:使用 Map 替代数组

classTrie{privateMap<Character,Trie>children=newHashMap<>();privatebooleanisEnd=false;publicvoidinsert(Stringword){Trienode=this;for(charch:word.toCharArray()){node.children.putIfAbsent(ch,newTrie());node=node.children.get(ch);}node.isEnd=true;}// 其他方法类似...}

优点

  • 节省内存(尤其字符集大或稀疏时)
  • 支持任意字符

缺点

  • 哈希计算开销
  • 无法利用字符连续性

8.2 压缩 Trie(Radix Tree / Patricia Trie)

将单链路径压缩为一个节点,例如:

原始: a -> p -> p -> l -> e 压缩: "apple"

适用场景:长单词、低分支度(如 IP 路由表)

8.3 添加计数功能

在节点中增加int count,记录以该节点为前缀的单词数量,支持:

  • 统计前缀出现次数
  • 实现 Top-K 自动补全

九、数据结构与算法基础知识点回顾

9.1 什么是 Trie(前缀树)?

  • 定义:一种有序树,用于存储动态集合,其中键通常是字符串。
  • 别名:字典树、单词查找树
  • 核心思想空间换时间,通过共享前缀加速查询

9.2 Trie 的典型性质

性质说明
根节点不包含字符
路径从根到节点的路径 = 字符串前缀
分支每个节点的子节点代表不同字符
叶子不一定是单词结尾(因单词可为另一单词前缀)

9.3 与其他数据结构对比

数据结构插入查找前缀匹配空间
HashSetO(1)O(1)O(N×L)
TreeSetO(L log N)O(L log N)O(L + K)
TrieO(L)O(L)O(L)

注:TreeSet 的前缀匹配需中序遍历,K 为匹配结果数

9.4 Trie 的局限性

  • 内存消耗大:每个节点需存储 Σ 个指针
  • 缓存不友好:节点分散,缓存命中率低
  • 不适合短单词集:若单词少且无公共前缀,不如哈希表

十、面试官提问环节(模拟)

Q1: 你能手写一个 Trie 吗?重点讲讲searchstartsWith的区别。

:当然。两者都调用searchPrefix找到前缀末尾节点。区别在于:

  • search要求该节点isEnd == true,即是一个完整插入的单词
  • startsWith只要路径存在即可,不要求是完整单词

例如,插入 “apple” 后:

  • search("app")→ false(“app” 未被插入)
  • startsWith("app")→ true(“apple” 以 “app” 开头)

Q2: 如果字符集包含大写字母、数字、符号,如何修改?

:有几种方案:

  1. 扩大数组:如 ASCII 128 字符 →new Trie[128],索引ch
  2. 使用 MapMap<Character, Trie> children,通用性强
  3. 哈希映射:自定义字符到索引的映射函数

推荐方案 2,灵活且节省空间。

Q3: Trie 在搜索引擎中如何用于自动补全?

:步骤如下:

  1. 用户输入前缀(如 “jav”)
  2. 在 Trie 中查找该前缀对应节点
  3. 从该节点 DFS 遍历所有子树,收集isEnd == true的完整单词
  4. 按热度/频率排序后返回 Top-K 建议

可进一步优化:

  • 节点存储热门词计数
  • 使用优先队列限制结果数量

Q4: 如何统计以某前缀开头的单词数量?

:可在节点中增加int prefixCount字段:

  • insert时,路径上每个节点prefixCount++
  • 查询时直接返回该节点的prefixCount

例如插入 “app”, “apple”:

  • ‘a’ 节点 count=2
  • ‘p’ 节点 count=2
  • 第二个 ‘p’ 节点 count=2
  • ‘l’ 节点 count=1

十一、这道算法题在实际开发中的应用

11.1 搜索引擎自动补全(Autocomplete)

  • 用户输入时实时提示可能的搜索词
  • Trie 支持毫秒级前缀匹配
  • 结合用户历史、热门词提升体验

11.2 拼写检查(Spell Checker)

  • 构建正确单词的 Trie
  • 对输入单词,查找编辑距离 ≤1 的候选词(需结合其他算法)

11.3 IP 路由表(Longest Prefix Match)

  • 将 IP 地址视为二进制字符串
  • 使用压缩 Trie(Patricia Trie)高效匹配最长前缀路由

11.4 敏感词过滤

  • 将敏感词库构建成 Trie
  • 对文本逐字符匹配,发现前缀即触发过滤

11.5 生物信息学:DNA 序列匹配

  • DNA 序列由 A/T/C/G 组成(Σ=4)
  • Trie 用于快速查找基因片段

十二、相关题目推荐

题号题目难度关联点
211添加与搜索单词 - 数据结构设计中等Trie + 通配符 ‘.’
212单词搜索 II困难Trie + DFS 网格搜索
421数组中两个数的最大异或值中等二进制 Trie
648单词替换中等Trie 前缀匹配替换
677键值映射中等Trie + 计数
720词典中最长的单词简单Trie + 排序
745前缀和后缀搜索困难双 Trie 或后缀 Trie

建议学习路径:先掌握本题 → 211(通配符)→ 212(综合应用)→ 421(二进制 Trie)


十三、总结与延伸

13.1 核心收获

  • Trie 是处理字符串前缀问题的利器
  • 时间复杂度稳定 O(L),不受数据规模影响
  • 设计关键:节点结构(children + isEnd)、路径遍历逻辑
  • 权衡:空间换时间,适用于前缀密集场景

13.2 延伸思考

  • 持久化 Trie:如何将 Trie 存入数据库?
  • 并发安全:多线程插入/查询如何加锁?(读写锁 or 无锁 Trie)
  • 分布式 Trie:海量词库如何分片存储?
  • 与 AC 自动机结合:多模式串匹配(如敏感词检测)

13.3 学习建议

  1. 动手实现:手写 Trie 是面试高频题
  2. 理解变种:压缩 Trie、后缀 Trie、二进制 Trie
  3. 联系实际:思考你在项目中能否用 Trie 优化某功能
  4. 刷相关题:巩固 Trie 在不同场景的应用

结语:Trie 虽然不是最常用的数据结构,但在特定场景下(尤其是涉及前缀操作时)具有不可替代的优势。掌握它,不仅是为了通过一道 LeetCode 题,更是为了在真实工程中多一把“利器”。希望本文能助你深入理解 Trie 的精髓,从容应对面试与实战!

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

MOOTDX通达信数据接口终极指南:从零基础到实战精通

MOOTDX通达信数据接口终极指南&#xff1a;从零基础到实战精通 【免费下载链接】mootdx 通达信数据读取的一个简便使用封装 项目地址: https://gitcode.com/GitHub_Trending/mo/mootdx 想要快速上手通达信数据接口&#xff1f;MOOTDX作为通达信数据的Python封装库&#…

作者头像 李华
网站建设 2026/2/17 21:15:39

DepthCrafter:如何免费生成视频长深度序列?

DepthCrafter&#xff1a;如何免费生成视频长深度序列&#xff1f; 【免费下载链接】DepthCrafter DepthCrafter是一款开源工具&#xff0c;能为开放世界视频生成时间一致性强、细节丰富的长深度序列&#xff0c;无需相机姿态或光流等额外信息。助力视频深度估计任务&#xff0…

作者头像 李华
网站建设 2026/2/21 23:13:23

3步搞定Neovim LSP配置:新手必学的命令自定义技巧

3步搞定Neovim LSP配置&#xff1a;新手必学的命令自定义技巧 【免费下载链接】nvim-lspconfig Quickstart configs for Nvim LSP 项目地址: https://gitcode.com/GitHub_Trending/nv/nvim-lspconfig 还在为Neovim语言服务器启动失败而烦恼吗&#xff1f;每次打开代码文…

作者头像 李华
网站建设 2026/2/21 3:56:46

mkcert终极指南:5分钟搞定本地HTTPS,告别浏览器安全警告

mkcert终极指南&#xff1a;5分钟搞定本地HTTPS&#xff0c;告别浏览器安全警告 【免费下载链接】mkcert A simple zero-config tool to make locally trusted development certificates with any names youd like. 项目地址: https://gitcode.com/GitHub_Trending/mk/mkcert…

作者头像 李华
网站建设 2026/2/18 19:01:10

python之lession 1

一、命令行验证python 1.使用win R调出输入cmd窗口 2.使用cmd命令打开命令行窗口 3.使用python --version查看python的版本号&#xff0c;目的是为了查询python是否正确安装二、python使用 1.打印hello world 2.python脚本的后缀为.py文件三、python语言简介 1.python高层次解…

作者头像 李华
网站建设 2026/2/20 2:31:19

突破传统:如何用Vue3+Three.js构建沉浸式3D抽奖系统

突破传统&#xff1a;如何用Vue3Three.js构建沉浸式3D抽奖系统 【免费下载链接】log-lottery &#x1f388;&#x1f388;&#x1f388;&#x1f388;年会抽奖程序&#xff0c;threejsvue3 3D球体动态抽奖应用。 项目地址: https://gitcode.com/gh_mirrors/lo/log-lottery …

作者头像 李华