多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

LeetCode 720 词典中最长的单词:哈希表模拟与字典树双解法实战解析(LogicStack-LeetCode 题解精读)

LeetCode 720 词典中最长的单词:哈希表模拟与字典树双解法实战解析(LogicStack-LeetCode 题解精读) 教程文档【免费下载链接】LogicStack-LeetCode公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码项目地址https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode点击查看免费下载本文是「宫水三叶的刷题日记」刷穿 LeetCode 系列第 720 题的深度精读核心围绕「词典中最长的单词」这道简单题完整拆解哈希表模拟与**字典树Trie**两套解法从合法单词的定义、字典序最小化处理到字典树的二维数组实现与 static 复用优化。读完本文你不仅能独立 AC 这道题还能掌握一套可复用的「前缀合法性校验」模板并理解它与 208. 实现 Trie (前缀树)中等.md) 等系列题目的内在关联。题目回顾什么才是「合法单词」题目给出一个字符串数组words组成的一本英语词典要求返回words中最长的一个单词该单词必须满足由words词典中其他单词逐步添加一个字母组成。也就是说对于候选单词w它的每一个前缀从第一个字符到完整单词本身都必须是词典中真实存在的单词。例如示例 1words [w,wo,wor,worl, world]输出world因为w、wo、wor、worl全部在词典中示例 2words [a, banana, app, appl, ap, apply, apple]输出apple因为apply和apple都满足条件但apple字典序更小。当存在多个长度相同且都满足条件的单词时返回字典序最小的一个若无答案则返回空字符串。关键约束来自 原题解1 words.length 10001 words[i].length 30所有字符串只包含小写字母数据范围很小最多 1000 个单词、每个单词最长 30 个字符这意味着暴力枚举在理论上可行但为了把问题吃透本系列给出了「模拟 哈希表」与「字典树」两条截然不同的技术路线。解法一哈希表模拟核心思路数据范围很小可以直接模拟。算法分为三步预处理将words数组中所有单词存入HashSet使后续对任意子串的「是否存在」判断近似达到 $O(1)$枚举候选遍历words判断每个单词是否为合法单词即其所有前缀都在集合中维护最优解用当前最长合法单词做剪枝跳过不可能更新答案的单词。这里有一个容易被忽视的细节题目没有声明words中没有重复元素因此遍历时最好基于去重后的Set集合进行避免对重复单词做无意义的重复校验。剪枝逻辑维护变量ans当前最优合法单词对每个候选s若s.length() ans.length()直接跳过长度不占优且长度相同也不可能更优若s.length() ans.length()且s.compareTo(ans) 0说明字典序更大跳过compareTo返回正数表示s的字典序更大只有长度更大、或长度相同但字典序更小的情况下才继续做完整的前缀合法性校验。完整代码class Solution { public String longestWord(String[] words) { String ans ; SetString set new HashSet(); for (String s : words) set.add(s); for (String s : set) { int n s.length(), m ans.length(); if (n m) continue; if (n m s.compareTo(ans) 0) continue; boolean ok true; for (int i 1; i n ok; i) { String sub s.substring(0, i); if (!set.contains(sub)) ok false; } if (ok) ans s; } return ans; } }复杂度分析时间复杂度预处理Set集合复杂度近似 $O(n)$判断某个单词s是否合法需要枚举其所有前缀长度从 1 到 m 共 m 个子串生成子串的代价逐级累加整体为 $O(m^2)$ 量级其中m为字符串长度。compareTo操作的复杂度为 $O(\min(N, M))$相比子串生成可忽略。综合来看整体复杂度为 $O(\sum_{i 0}^{n - 1} words[i].length^2)$空间复杂度Set存储所有单词为 $O(\sum_{i 0}^{n - 1} words[i].length)$。原题解特别指出不算剪枝效果该做法计算量不超过 $10^6$完全可以轻松通过。这也印证了「数据范围决定算法选型」——当规模小到一定程度时朴素但正确的解法就是好解法。解法二字典树Trie哈希表方案中「枚举某单词的所有子串并判断是否在words中出现」的操作可以改用字典树来实现从而把单次合法性校验的复杂度从 $O(m^2)$ 降到 $O(m)$。前置知识字典树是什么字典树Trie / 前缀树是一种用于快速查询「某个字符串 / 字符前缀」是否存在的数据结构其核心是用边代表字符用点记录「是否为单词结尾」以及「后续字符指向哪里」。仓库中 Index/字典树.md 汇总了本系列全部字典树题解208、211、212、421、648、676、677、720、745、1032、1268、1707 等而 208. 实现 Trie (前缀树)中等.md) 则给出了 Trie 的两种标准实现二维数组实现与TrieNode 节点实现本文沿用其中效率更高的二维数组实现。合法单词的充要条件将所有words[i]插入字典树并在每个单词的结尾字符对应的节点打上结束标记isEnd。此时一个单词s能成为合法单词的充要条件是s的每一个前缀节点都带有「结尾标记」isEnd[p] true。这与哈希表方案「每个前缀都存在于集合中」是等价的——只是把「存在性」从哈希查询换成了树上的标记查询。特别注意这里的校验包含单词自身因此单词的最后一个字符所在节点也必须被打上isEnd标记。二维数组实现细节在本题中字符集固定为 26 个小写字母因此采用static int[][] tr new int[N][M]的二维数组承载整棵树N 30010预估最大节点数1000 个单词 × 每个最长 30 个字符理论上限为 30000加上根节点取 30010 留有余量M 26字符集大小tr[p][u]表示从节点p沿字符ua u到达的下一个节点编号0表示该分支尚未开辟isEnd布尔数组记录某编号节点是否为某个单词的结尾idx自增计数器代表「已使用的节点行数」每开辟一个新节点就idx。add操作从根节点 0 出发逐字符查找分支若tr[p][u] 0则idx开辟新节点并记录最后走到单词末尾节点并置isEnd[p] true。query操作同样从根出发逐字符下钻每到一个节点就检查isEnd[p]一旦发现某个前缀不是已插入的完整单词即该节点没有结尾标记就立即返回false全部前缀通过则返回true。static 复用与清理一个重要的工程细节原题解特别强调了一个细节为了防止每个样例都new大数组使用static进行优化并在跑样例前进行相应的清理工作。原因在于OJ 每测试一个样例都可能创建一个新的Solution实例若把N × M的大数组作为实例成员每次都会重新分配大块内存样例多时容易触发 GC 拖慢评测。将数组声明为static后无论有多少测试样例底层数组只分配一次每次运行前只需将用过的行清零、重置isEnd与idx即可复用。这一技巧在 208 题解中等.md) 中也有完整论述属于本系列字典树题目的通用优化手段可以显著缩短评测耗时。完整代码class Solution { static int N 30010, M 26; static int[][] tr new int[N][M]; static boolean[] isEnd new boolean[N]; static int idx 0; void add(String s) { int p 0, n s.length(); for (int i 0; i n; i) { int u s.charAt(i) - a; if (tr[p][u] 0) tr[p][u] idx; p tr[p][u]; } isEnd[p] true; } boolean query(String s) { int p 0, n s.length(); for (int i 0; i n; i) { int u s.charAt(i) - a; p tr[p][u]; if (!isEnd[p]) return false; } return true; } public String longestWord(String[] words) { Arrays.fill(isEnd, false); for (int i 0; i idx; i) Arrays.fill(tr[i], 0); idx 0; String ans ; for (String s : words) add(s); for (String s : words) { int n s.length(), m ans.length(); if (n m) continue; if (n m s.compareTo(ans) 0) continue; if (query(s)) ans s; } return ans; } }复杂度分析时间复杂度将所有words[i]存入字典树的总代价为 $O(\sum_{i 0}^{n - 1} words[i].length)$查询单个单词是否合法的代价为 $O(m)$m为该单词长度整体复杂度为 $O(\sum_{i 0}^{n - 1} words[i].length)$——相比哈希表方案的平方级复杂度字典树把「逐前缀校验」优化到了与字符串总长度同阶空间复杂度$O(C)$其中C为实际使用到的节点总数静态数组占用的最大空间为N × M但实际分配的行数由idx决定。两种解法的对比与选型启示维度哈希表模拟字典树单次合法性校验$O(m^2)$枚举所有子串$O(m)$沿树下钻整体时间复杂度$O(\sum len^2)$$O(\sum len)$空间占用单词集合节点二维数组需预估上限实现复杂度低几行搞定中需理解树上标记逻辑适用场景数据范围小、追求简洁字符集固定、追求最优复杂度两者在本题数据范围1000 × 30下都能轻松通过。从工程角度仓库 Index/字典树.md 中的 208 题解也指出纯前缀存在性判断用哈希表足够但一旦涉及前缀路径上的累积状态如本题的「每个前缀都必须是完整单词」字典树的结构优势就会显现——它天然把前缀关系显式编码在树中query过程即是对这条路径上每个节点状态的顺序校验。值得补充的是compareTo剪枝逻辑在两套解法中完全一致它解决的是题目的第二重目标长度相同取字典序最小。这提示我们面对「多条件最优」的题目可以先把次要条件字典序作为剪枝阈值把主要条件长度作为主推进目标从而减少无效计算。拓展阅读本仓库是「宫水三叶的刷题日记」系列题解的完整合集围绕本题你可以继续深入Index/字典树.md本系列全部字典树题目的索引表208/211/212/421/648/676/677/720/745/1032/1268/1707 及剑指 Offer II 067按推荐指数排列适合系统练习前缀树实现 Trie (前缀树)中等.md)Trie 的「二维数组」与「TrieNode」两种标准实现、1e5行数估算方法以及 static 复用的完整论证添加与搜索单词 - 数据结构设计在 Trie 上处理通配符查询是 720 题「前缀校验」思路的自然延伸单词替换利用 Trie 查找最短前缀根与本题共享「前缀路径」的遍历模式。把 720 题的两套解法吃透你就同时掌握了「小数据范围的暴力美学」与「前缀树结构思维」两条技术路线后续再遇到前缀匹配、单词联想、敏感词过滤类题目时都能快速定位到对应的模板。赞分享教程文档【免费下载链接】LogicStack-LeetCode公众号「宫水三叶的刷题日记」刷穿 LeetCode 系列文章源码项目地址https://gitcode.com/gh_mirrors/lo/LogicStack-LeetCode点击查看免费下载相关推荐LogicStack-LeetCode 题解精讲212. 单词搜索 II回溯算法与 Trie 字典树双解法LogicStack LeetCode 题解精讲212. 单词搜索 II回溯算法与 Trie 字典树双解法 导读 本文基于开源刷题仓库 LogicStac教程文档LogicStack-LeetCode 刷穿 LeetCode基于字典树Trie的 648. 单词替换 题解与双实现剖析LogicStack LeetCode 刷穿 LeetCode基于字典树Trie的 648. 单词替换 题解与双实现剖析 导读 本文是「宫水三叶的刷题日记教程文档LogicStack-LeetCode 题解884. 两句话中的不常见单词哈希表 模拟LogicStack LeetCode 题解884. 两句话中的不常见单词哈希表 模拟 本文以 LogicStack LeetCode 仓库中 884教程文档上一篇智能网页视频下载助手4个真实场景下的高效解决方案下一篇Cppcheck variableScope 检查项详解缩小变量作用域以提升 C/C 代码可读性创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表