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

文章详情

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

AlgoNote 题解:LeetCode 0745 前缀和后缀搜索(WordFilter)—— 用字典树将「后缀 + 前缀」合并为一次匹配

AlgoNote 题解:LeetCode 0745 前缀和后缀搜索(WordFilter)—— 用字典树将「后缀 + 前缀」合并为一次匹配 教程文档知识库【免费下载链接】AlgoNote⛽️「算法通关手册」从零开始的「算法与数据结构」学习教程200 道「算法面试热门题目」1000 道「LeetCode 题目解析」持续更新中项目地址https://gitcode.com/gh_mirrors/le/AlgoNote点击查看免费下载导读本篇基于 AlgoNote「算法通关手册」中 0745 前缀和后缀搜索题解 展开讲解如何在 LeetCode 0745 中设计一个既能按前缀又能按后缀检索单词的WordFilter词典类。你将掌握一种经典的「Trie 组合键」技巧把单词的所有{后缀}#{单词}组合预插入字典树从而把两次字符串匹配合并成一次 O(L) 查询并学会在节点上聚合「最大下标」以满足返回最大索引的要求。读完本文你可以直接将该思路迁移到任何「多条件前缀/后缀匹配」的检索类设计题中。1. 题目核心既要前缀匹配又要后缀匹配LeetCode 0745「前缀和后缀搜索」Prefix and Suffix Search是一道困难级别的设计题标签为「设计、字典树、数组、哈希表、字符串」。题目要求实现一个特殊词典WordFilterWordFilter(string[] words)使用词典中的单词words初始化对象f(string pref, string suff)返回词典中「同时具有前缀pref和后缀suff」的单词下标如果存在多个满足要求的单词返回最大的下标如果不存在返回-1。题目给出的数据规模直接决定了算法选择1 words.length 10^41 words[i].length 71 pref.length, suff.length 7words[i]、pref、suff仅由小写英文字母组成最多对函数f执行10^4次调用。关键约束有三点值得注意单词长度不超过 7非常短但f的调用次数可以高达10^4。这意味着初始化阶段可以承受一定的预处理开销而查询阶段必须高效——这正是字典树方案成立的前提。1.1 示例演示输入 [WordFilter, f] [[[apple]], [a, e]] 输出 [null, 0] 解释 WordFilter wordFilter new WordFilter([apple]); wordFilter.f(a, e); // 返回 0 因为下标为 0 的单词前缀 prefix a 且 后缀 suffix e 。apple以a开头、以e结尾因此f(a, e)返回 0。1.2 直观的暴力方案及其缺陷最直观的做法是查询时遍历所有单词检查每个words[i]是否以pref开头words[i].startswith(pref)且以suff结尾words[i].endswith(suff)取满足条件且下标最大的i。单次查询的时间复杂度为 O(n × L)n 为单词数L 为单词长度。在words.length 10^4、f调用10^4次的极端场景下总代价达到 O(n × L × 10^4) ≈ 10^9 级别在 Python 中会明显超时。因此需要用预处理换取查询速度这就是字典树方案的核心动机。2. 前置知识从仓库的字典树实现看 Trie 的基本操作在进入本题解法前先回顾 AlgoNote 仓库中的字典树基础。字典树Trie又称前缀树是一种高效存储和查找字符串集合的树形结构从根节点出发按字母顺序逐层分支相同前缀的单词共享路径。它的基本性质是根节点不存字符其他每个节点只存一个字符从根到某节点的路径组成该节点对应的字符串每个节点的所有子节点字符互不相同。仓库的 字典树专题文档 系统讲解了字典树的创建、插入、查找与前缀查找与之配套的 可运行实现 采用哈希表存储子节点的方式key 为字符value 为子节点实例并实现了三个核心方法insert(word)遍历单词字符子节点不存在则新建最后用isEnd标记单词结尾search(word)逐字符下钻最后判断isEndstartsWith(prefix)只验证前缀路径是否存在不要求结尾标记。本题的解题代码正是对这一基础实现的重要变体把「单词结尾标记」替换为「路径上经过的最大下标」并引入#分隔符来同时编码后缀与前缀信息。理解了 string_trie.py 中的insert与startsWith模式再读下面的解法就会非常顺畅。3. 核心思路用{后缀}#{单词}组合键把两次匹配变成一次匹配3.1 为什么朴素地建两棵 Trie 不够好一种自然想法是建两棵字典树一棵存所有单词用于前缀匹配另一棵存所有单词的反转用于后缀匹配。但这样f需要分别查出「前缀匹配的候选集合」与「后缀匹配的候选集合」再取交集并找最大下标——候选集合的存储与求交都缺乏优雅且高效的做法。本题解法的精妙之处在于用分隔符#将「后缀 单词」拼成一条键。由于题目保证单词只含小写英文字母#绝不会与单词内容冲突可以安全地作为后缀部分与单词部分的天然分界。3.2 组合键的构造原理对于单词word遍历其所有可能的后缀包括空后缀构造键suffix # word插入字典树单词apple的所有后缀为apple、pple、ple、le、e以及空串对应插入的键为apple#apple、pple#apple、ple#apple、le#apple、e#apple、#apple。为什么这样可行假设某单词word满足「前缀为pref、后缀为suff」那么word以suff结尾等价于存在某个后缀suffix suff该后缀对应键suff # word必然以suff # pref为前缀因为word以pref开头。因此查询时只需构造键suff # pref并在字典树中查找该前缀路径即可。一次前缀匹配同时完成了「后缀匹配」与「前缀匹配」两件事。3.3 在每个节点聚合最大下标题目要求返回「最大下标」。由于构造键时是按照words数组下标递增的顺序插入的越晚插入的单词下标越大。因此每经过一个节点就将其weight更新为当前index最终路径终点节点的weight自然就是经过该路径即满足该后缀前缀组合的所有单词中的最大下标。3.4 实现步骤汇总对每个单词word及其下标index遍历所有后缀含空后缀构造键suffix # word从根节点逐字符插入该键每到达一个节点就把该节点的weight更新为index查询时构造键suff # pref从根节点逐字符下钻中途任一字符缺失则返回-1完整走到终点后返回终点节点的weight。4. 完整代码实现Pythonclass TrieNode: def __init__(self): self.children {} self.weight -1 # 存储最大索引 class WordFilter: def __init__(self, words: List[str]): self.root TrieNode() # 对于每个单词插入所有可能的 {suffix}#{word} 形式 for index, word in enumerate(words): word_len len(word) # 遍历所有后缀包括空后缀 for i in range(word_len 1): suffix word[i:] # 插入 suffix#word key suffix # word node self.root for char in key: if char not in node.children: node.children[char] TrieNode() node node.children[char] node.weight index # 更新最大索引 def f(self, pref: str, suff: str) - int: # 查找 suff#pref key suff # pref node self.root for char in key: if char not in node.children: return -1 node node.children[char] return node.weight # Your WordFilter object will be instantiated and called as such: # obj WordFilter(words) # param_1 obj.f(pref,suff)4.1 关键细节解读空后缀的处理range(word_len 1)保证i word_len时suffix 对应键#word。这使得「仅按前缀查询」的场景suff为空时本不会出现但空后缀组合为后续扩展留有余地以及「后缀为空、前缀非空」的组合都能被覆盖。本题约束suff.length 1但保留空后缀的写法并不影响正确性。#分隔符的选择因为字母集限定为小写字母#不会与任何字符冲突可以作为路径中的安全分隔符。若字符集扩大例如允许大写或数字需要相应替换分隔符或增加转义策略。与基础 Trie 的差异相比 string_trie.py 中用isEnd标记单词结尾本题的TrieNode用weight存储最大下标且不对单词结尾做特殊标记——因为查询目标是「路径经过的最大下标」而非「是否存在完整单词」。4.2 手动推演示例以words [apple]为例初始化时插入apple#apple、pple#apple、ple#apple、le#apple、e#apple、#apple六条键所有路径上的节点weight均为 0查询f(a, e)构造键e#a路径e - # - a均存在来自键e#apple的前缀e#a终点节点weight 0返回 0与题目示例一致。再验证不存在的情况若查询f(b, e)构造键e#b走到e - #后下一字符b不存在于子节点返回-1。5. 复杂度分析时间复杂度初始化O(n × L²)其中 n 是单词数量L 是单词的平均长度。每个单词最多插入 L 1 条键每条键的长度约为 L后缀 1分隔符 L单词总插入字符量约 O(n × L²)。得益于单词长度 ≤ 7 的约束这一开销完全可控。查询O(L)L 是前缀和后缀的长度之和即键suff # pref的长度。空间复杂度O(n × L²)即字典树占用的节点空间。同样因为 L ≤ 7空间增长被严格限制在可接受范围。对比暴力方案查询 O(n × L)字典树方案把最耗时的查询降到了 O(L)以初始化阶段的 O(n × L²) 预处理为代价——在「查询次数多、单词短」的题目约束下这是最优取舍。6. 解法延伸与工程化思考6.1 变体改为存储最小/第 k 大下标本题的weight聚合策略是「路径上最大下标」因为它要求返回最大下标。若题目改为返回最小下标只需让weight取「首次插入时不再覆盖」或「显式取 min」即可若需要统计满足条件的单词个数可以在节点上额外维护计数。聚合信息的具体含义由题目要求决定组合键的插入框架无需改动。6.2 空间优化的思路当单词数量极大或长度较长时O(n × L²) 的空间可能成为瓶颈。可以思考的方向包括仅对「较短的单词」构建组合键或对单词按长度分组建 Trie使用「两个字典树 候选下标集合求交」的替代方案但在下标交集计算上需要额外设计利用哈希表缓存查询结果memoization对于重复的(pref, suff)查询直接命中缓存避免重复下钻。这些都属于本题基础上的进一步工程优化核心的「组合键编码」思想不变。6.3 字典树知识在仓库中的定位本题在 AlgoNote 中归类为「字典树」题型与仓库的 字典树题目列表 下的其他题目如 0208 实现 Trie、0677 键值映射、0211 添加与搜索单词同属一类。基础数据结构可对照 字典树专题文档 与 string_trie.py 实现 学习本题的完整题解收录于 prefix-and-suffix-search.md并登记在 题解总列表 与 0700-0799 目录索引 中。7. 小结LeetCode 0745「前缀和后缀搜索」的核心启示在于用#分隔符将后缀与单词拼成组合键把「后缀匹配 前缀匹配」合并为字典树上的单次前缀查找查询复杂度降至 O(L)在插入过程中于每个节点聚合最大下标天然满足「返回最大下标」的要求避免了查询时的二次扫描本题是「设计 字典树」的经典组合答案成立的关键前提是单词长度小预处理可承受且查询次数多查询必须快这也解释了为何面对此类设计题要优先考虑预处理换查询速度的路线。掌握这一技巧后你可以把它直接迁移到搜索引擎自动补全、模糊检索、多条件字符串过滤等实际场景中。赞分享教程文档知识库【免费下载链接】AlgoNote⛽️「算法通关手册」从零开始的「算法与数据结构」学习教程200 道「算法面试热门题目」1000 道「LeetCode 题目解析」持续更新中项目地址https://gitcode.com/gh_mirrors/le/AlgoNote点击查看免费下载相关推荐Lecture_Notes树结构应用字典树与后缀树字符串匹配Lecture_Notes树结构应用字典树与后缀树字符串匹配 字符串匹配是计算机科学中的基础问题广泛应用于搜索引擎、代码编辑器、生物信息学等领域。传统的暴GitHub_Trending/leetcode1/leetcode字典树实现前缀树及其在搜索建议中的应用GitHub_Trending/leetcode1/leetcode字典树实现前缀树及其在搜索建议中的应用 你还在为字符串前缀搜索效率低下而烦恼吗一文掌握T示例工程教程AlgoNote 题解0538. 把二叉搜索树转换为累加树BST 反中序遍历 前缀和AlgoNote 题解0538. 把二叉搜索树转换为累加树BST 反中序遍历 前缀和 本篇是「算法通关手册」AlgoNote 中 LeetCode 0教程文档知识库上一篇ThinkPad风扇终极控制指南TPFanCtrl2让你的笔记本散热性能提升300%下一篇mermaid-ascii loop/opt块教程嵌套循环帧在时序图的画法创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表