news 2026/9/12 21:01:28

LeetCode-Go 题解:966. Vowel Spellchecker 元音拼写检查器的三表哈希实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode-Go 题解:966. Vowel Spellchecker 元音拼写检查器的三表哈希实现

LeetCode-Go 题解:966. Vowel Spellchecker 元音拼写检查器的三表哈希实现

【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go

本篇技术指南以 leetcode/0966.Vowel-Spellchecker/README.md 为核心骨架,完整剖析 LeetCode 第 966 题《Vowel Spellchecker》(元音拼写检查器)的题意、四层匹配优先级规则,并结合 966. Vowel Spellchecker.go 的源码逐行讲解"三张哈希表 + 元音掩码"的解题方案。读完本文,你将掌握如何用 Go 以 O((N+Q)×L) 的时间复杂度实现一个可处理大小写错误与元音错误的分层拼写检查器,并理解其与配套单测 966. Vowel Spellchecker_test.go 之间的验证关系。

题目背景与问题定义

LeetCode 966 要求实现一个拼写检查器:给定一个单词列表wordlist和若干查询单词queries,对每个查询query,从wordlist中找出"正确"的单词并返回;若找不到任何匹配,则返回空字符串""

题目约束(继承自原文档):

  • 1 <= wordlist.length <= 5000
  • 1 <= queries.length <= 5000
  • 1 <= wordlist[i].length <= 7
  • 1 <= queries[i].length <= 7
  • wordlistqueries中的所有字符串仅由英文字母组成

由于单词长度上限仅为 7,单次匹配成本极低,问题真正的难点在于正确组织多层匹配规则,而不是暴力扫描

拼写检查处理的两类错误

对于给定的查询单词query,拼写检查器只处理两类拼写错误:

1. 大小写错误(Capitalization)

如果querywordlist中的某个单词不区分大小写地相等,则返回wordlist中该单词的原始大小写形式。原文档给出了三个例子:

  • wordlist = ["yellow"]query = "YellOw"correct = "yellow"
  • wordlist = ["Yellow"]query = "yellow"correct = "Yellow"
  • wordlist = ["yellow"]query = "yellow"correct = "yellow"

2. 元音错误(Vowel Errors)

如果将query中的元音字母('a''e''i''o''u'各自替换为任意元音后,能与wordlist中的某个单词(不区分大小写)匹配,则返回该单词的原始形式。注意这里的"替换"是逐一替换且元音之间互相等价,而不是插入或删除元音,因此元音数量必须一致。原文档的例子:

  • wordlist = ["YellOw"]query = "yollow"correct = "YellOw"eo互换位置不改变掩码)
  • wordlist = ["YellOw"]query = "yeellow"correct = ""(元音数量不同,掩码不匹配)
  • wordlist = ["YellOw"]query = "yllw"correct = ""(缺失元音,掩码不匹配)

四条匹配优先级规则

除上述两类错误外,题目还明确规定了匹配的优先级(原文档原文要点):

  1. querywordlist中某个单词区分大小写地完全一致时,直接返回该单词本身;
  2. 否则,若query仅存在大小写差异,返回wordlist第一个这样的匹配项;
  3. 否则,若query存在元音错误,返回wordlist第一个这样的匹配项;
  4. 若以上均不满足,返回空字符串""

优先级决定了匹配顺序必须是"精确匹配 → 大小写归一匹配 → 元音掩码匹配"的逐级降级过程,一旦命中即返回,不再尝试更低级别。

官方示例推演

原文档给出的示例为:

Input: wordlist = ["KiTe","kite","hare","Hare"] queries = ["kite","Kite","KiTe","Hare","HARE","Hear","hear","keti","keet","keto"] Output: ["kite","KiTe","KiTe","Hare","hare","","","KiTe","","KiTe"]

逐条推演:

query命中级别返回原因
kite精确匹配kitewordlist[1]完全一致
Kite大小写归一KiTe小写化后为kite,返回wordlist中第一个小写为kite的词,即KiTe
KiTe精确匹配KiTewordlist[0]完全一致
Hare精确匹配Harewordlist[2]完全一致
HARE大小写归一hare小写化后为hare,返回第一个匹配项hare
Hear元音掩码""掩码化后为h**r,而hare/Hare掩码为h*r*,不匹配
hear元音掩码""同上,掩码h**rh*r*不一致
keti元音掩码KiTe掩码化后为k*t*,与KiTe的掩码k*t*一致
keet元音掩码""掩码为k**t,元音数量与k*t*不一致
keto元音掩码KiTe掩码k*t*KiTe一致(oi同为元音)

这个推演过程同时验证了"元音错误"的边界:Hear之所以失败,是因为ea是两个元音,而hare只有一个元音,掩码长度对不上。

解题思路:三张哈希表的层次化匹配

原文档的解题思路明确指出"很明显需要用map来解题",并归纳为三种情况:

  1. 查询字符串完全匹配:用map[string]bool记录wordlist中的原始单词,key直接命中即返回;
  2. 查询字符串仅大小写不同:用map[string]string将单词的小写形式映射回原单词的正确大小写形式;
  3. 查询字符串有元音错误:用map[string]string将单词忽略元音的小写形式映射回原单词的正确形式。

这里的关键设计是**"只保留第一个匹配":题目要求返回wordlist中第一个匹配项,因此在构建wordsCapwordsVowel两张映射表时,只有当某个归一化键首次出现**时才写入,之后重复出现不再覆盖,从而天然保证"第一个匹配优先"。

其源码实现在 966. Vowel Spellchecker.go 中,核心数据结构如下:

wordsPerfect, wordsCap, wordsVowel := map[string]bool{}, map[string]string{}, map[string]string{}
  • wordsPerfect:精确匹配表(区分大小写);
  • wordsCap:大小写归一表(键为小写单词);
  • wordsVowel:元音掩码表(键为去掉元音特征的小写单词)。

Go 实现逐步解析

第一步:预处理wordlist,构建三张表

for _, word := range wordlist { wordsPerfect[word] = true wordLow := strings.ToLower(word) if _, ok := wordsCap[wordLow]; !ok { wordsCap[wordLow] = word } wordLowVowel := devowel(wordLow) if _, ok := wordsVowel[wordLowVowel]; !ok { wordsVowel[wordLowVowel] = word } }
  • wordsPerfect[word] = true:原样收录,用于第一级精确匹配;
  • strings.ToLower(word)得到小写形式,作为第二级匹配的键;
  • devowel(wordLow)得到元音掩码形式,作为第三级匹配的键;
  • 两处if _, ok := ...; !ok判断保证只保留第一次出现的单词,对应题目"返回第一个匹配项"的规则。这是整个实现中容易被忽略却至关重要的细节。

第二步:逐条处理查询,按优先级降级

res, index := make([]string, len(queries)), 0 for _, query := range queries { if _, ok := wordsPerfect[query]; ok { res[index] = query index++ continue } queryL := strings.ToLower(query) if v, ok := wordsCap[queryL]; ok { res[index] = v index++ continue } queryLV := devowel(queryL) if v, ok := wordsVowel[queryLV]; ok { res[index] = v index++ continue } res[index] = "" index++ } return res

匹配流程严格对应四条优先级规则:

  1. wordsPerfect[query]命中 → 返回原query(精确匹配);
  2. 小写化后查wordsCap命中 → 返回wordlist中的原始大小写(大小写归一);
  3. 小写化再元音掩码后查wordsVowel命中 → 返回原始单词(元音错误修正);
  4. 全部未命中 → 写入空字符串。

由于预先分配了make([]string, len(queries))并用index递增写入,避免了反复append造成的扩容开销,从源码结构看这是一种面向性能的写法。

devowel:元音掩码的实现细节

func devowel(word string) string { runes := []rune(word) for k, c := range runes { if c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' { runes[k] = '*' } } return string(runes) }

devowel将字符串先转换为[]rune再逐字符判断是否为小写元音a/e/i/o/u,命中则替换为占位符'*'。这样,所有元音无论实际是什么字母,都被归一到同一个占位符,从而把"元音之间互相等价"的题意精确建模为字符串相等比较

需要说明的是:wordlistqueries均只含英文字母,且本实现先经过strings.ToLower归一,因此devowel中只需判断小写元音即可;采用[]rune而非直接对string做下标替换,从代码结构上看是为了按字符而非按字节处理,使掩码逻辑更稳健、更易扩展到非纯 ASCII 场景。

例如:

  • KiTe→ 小写kite→ 掩码k*t*
  • keti→ 小写keti→ 掩码k*t*→ 与前者相等,故query = "keti"能命中KiTe

复杂度分析

wordlist长度为 N,queries长度为 Q,单词最大长度为 L(本题约束 L ≤ 7):

  • 预处理阶段:遍历 N 个单词,每个单词做一次小写转换与一次掩码转换,时间复杂度 O(N×L),空间上三张表合计存储 O(N×L) 个字符;
  • 查询阶段:每个查询执行一次精确查表、一次小写转换查表、一次掩码转换查表,时间复杂度 O(Q×L);
  • 总体复杂度:O((N+Q)×L) 时间,O(N×L) 空间,完全满足 5000×7 量级的数据规模,且每个查询的匹配都是哈希表 O(1) 级别的常数操作,无需对wordlist做任何二次扫描。

测试与验证

仓库为本题提供了完整单测 966. Vowel Spellchecker_test.go,其中Test_Problem966将官方示例作为唯一用例执行验证:

qs := []question966{ { para966{[]string{"KiTe", "kite", "hare", "Hare"}, []string{"kite", "Kite", "KiTe", "Hare", "HARE", "Hear", "hear", "keti", "keet", "keto"}}, ans966{[]string{"kite", "KiTe", "KiTe", "Hare", "hare", "", "", "KiTe", "", "KiTe"}}, }, }

该用例同时覆盖了精确匹配、大小写归一、元音错误修正与空结果四种路径,是验证题目四条优先级规则是否落实的直接依据。

本地运行该用例的命令为:

go test -v -run Test_Problem966 ./leetcode/0966.Vowel-Spellchecker/

若需对整个题解仓库生成覆盖率报告,可复用 gotest.sh 中的方式(该脚本以atomic模式对全部leetcode包一次性产出合法覆盖率文件):

go test -covermode=atomic -coverprofile=coverage.txt ./leetcode/...

注意运行前提:本仓库go.mod声明的 Go 版本为go 1.19,执行上述命令需要本地安装与之兼容的 Go 工具链。

小结

LeetCode 966 是一道"规则驱动型"的字符串处理题,其核心并不在算法技巧,而在于如何把四条优先级规则映射为可判定的键。本题的 Go 解法给出了一个简洁且可迁移的范式:

  • map[string]bool承载精确匹配;
  • 用"小写键 → 原词"的映射承载大小写归一;
  • 用"元音掩码键 → 原词"的映射承载元音错误修正;
  • 构建时"只保留首个匹配",查询时"逐级降级、命中即返"。

结合 966. Vowel Spellchecker.go 的源码与 966. Vowel Spellchecker_test.go 的单测,这一"三表哈希 + 掩码归一"的思路不仅适用于本题,也可推广到任何"多级模糊匹配且需要保留首个命中"的检索场景,例如搜索建议、同音词纠错、词典近似查询等。

【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

【计算机毕设实战】SpringBoot 校园报修管理系统

摘要传统高校宿舍报修依靠电话、微信、纸质登记&#xff0c;报修流程混乱、维修进度无法跟踪、维修数据难以统计。为解决高校后勤报修痛点&#xff0c;本文采用SpringBoot 后端 Vue 前端 MySQL 数据库前后端分离技术栈&#xff0c;开发一套校园报修管理系统。系统划分普通学生…

作者头像 李华
网站建设 2026/9/12 20:56:28

React单向数据流原理与双向绑定实现方案

1. React数据变更机制解析&#xff1a;单向数据流如何实现实时响应作为React开发者&#xff0c;我们经常被问到这个问题&#xff1a;"为什么React没有像Vue那样的双向绑定&#xff0c;却能实现数据实时变更&#xff1f;"这其实涉及到React最核心的设计哲学。我最初从…

作者头像 李华
网站建设 2026/9/12 20:56:09

微信小程序+SSM+MySQL毕业设计落地实践指南

简介&#xff1a;这是一套面向计算机专业本科生的毕业设计实战资源&#xff0c;聚焦设备故障报修业务场景&#xff0c;完整呈现微信小程序前端SSM后端MySQL数据库的全栈开发方案。资源覆盖用户、维修员、管理员三角色协同流程&#xff0c;支持报修提交、经验分享、维修报告生成…

作者头像 李华
网站建设 2026/9/12 20:52:40

Label Studio:从部署到导出的多模态标注完整路径

Label Studio&#xff1a;从部署到导出的多模态标注完整路径 【免费下载链接】label-studio Label Studio is a multi-type data labeling and annotation tool with standardized output format 项目地址: https://gitcode.com/GitHub_Trending/la/label-studio 数据标…

作者头像 李华