LeetCode 500 Keyboard Row 题解:Go 实现"同一键盘行单词筛选"与源码剖析
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
导读
本题要求从给定单词列表中筛选出仅由美式键盘同一行字母组成的单词,是字符串处理与字符集合判定的入门经典题。本文以 LeetCode-Go 仓库中 500. Keyboard Row 的官方题解文档为骨架,结合仓库内真实可运行的 Go 源码实现 与 单元测试,逐行讲解算法原理、边界处理与复杂度分析,并给出可复制的运行与验证命令。读完后你将掌握"多行键盘映射 + 字符串匹配"一类问题的标准解法,以及如何在本地运行该仓库的测试用例进行验证。
一、题目理解:什么算"同一键盘行"
1.1 原题描述
Given a List of words, return the words that can be typed using letters ofalphabeton only one row's of American keyboard.
翻译过来即:给定一个单词列表,只返回可以使用在键盘同一行的字母打印出来的单词。美式键盘共分三行:
- 第一行(字母区):
qwertyuiop - 第二行:
asdfghjkl - 第三行:
zxcvbnm
1.2 示例
Input: ["Hello", "Alaska", "Dad", "Peace"] Output: ["Alaska", "Dad"]分析:
"Alaska":全部字母a l s k a均位于第二行asdfghjkl,符合条件;"Dad":字母d、a、d均位于第二行,符合条件;"Hello":字母h在第二行,而e l o在第一行,跨了两行,不符合;"Peace":字母p在第一行,e a c也都在第一行——注意!Peace中p e a c e全部属于第一行qwertyuiop,理论上应该符合。但示例输出中没有它,原因是题目中Hello与Peace都含有字母e,且原题截图键盘中字母p与e的位置……这里以示例输出为准:示例给出Output: ["Alaska", "Dad"],实际按三行标准划分,"Peace"的五个字母p(第一行) e(第一行) a(第二行) c(第三行) e(第一行)——字母a属于第二行、c属于第三行,因此"Peace"实际跨了三行,不符合条件。上述两段分析仅用于理解判定规则,最终结论以 LeetCode 官方用例为准。
1.3 题目注意点
原题给出两条重要约束,直接决定了实现策略:
- 同一字符可以重复使用:即每个单词中字母是否重复出现不影响判定,无需去重;
- 输入字符串只包含字母:输入必然由英文字母构成,因此每个字符必然属于三行中的某一行,不存在"不属于任何行"的字符。
这两条约束是仓库源码中关键剪枝逻辑(见下文oneRow翻转技巧)成立的数学前提。
二、解题思路:逐单词匹配三行键盘
原文档给出的解题思路非常简洁:
给出一个字符串数组,要求依次判断数组中的每个字符串是否都位于键盘上的同一个行,如果是就输出。
将其展开为可执行算法,核心分三步:
- 定义三行键盘字符集:
{"qwertyuiop", "asdfghjkl", "zxcvbnm"}; - 逐单词判定:对每个单词,统计它命中了几个行。若恰好命中 1 行则输出,命中 ≥2 行则跳过;
- 大小写归一:键盘字符集为小写,单词需统一转小写后再匹配(如
"Hello"→"hello")。
这是一个时间复杂度与输入总字符数成正比、空间复杂度 O(1)(不含输出数组)的线性解法。
三、仓库源码逐行剖析:findWords500 的实现细节
仓库 500. Keyboard Row.go 中的实现非常精巧,值得逐行拆解:
package leetcode import "strings" func findWords500(words []string) []string { rows := []string{"qwertyuiop", "asdfghjkl", "zxcvbnm"} output := make([]string, 0) for _, s := range words { if len(s) == 0 { continue } lowerS := strings.ToLower(s) oneRow := false for _, r := range rows { if strings.ContainsAny(lowerS, r) { oneRow = !oneRow if !oneRow { break } } } if oneRow { output = append(output, s) } } return output }3.1 关键设计一:strings.ContainsAny判断"命中行"
strings.ContainsAny(lowerS, r)用于判断字符串lowerS中是否包含字符集r中的任意一个字符。例如:
ContainsAny("alaska", "asdfghjkl")→true,说明alaska命中第二行;ContainsAny("alaska", "qwertyuiop")→false,未命中第一行。
由于题目保证输入只含字母,一个非空单词必然至少命中一行(其每个字母都落在三行之一),这是下面翻转技巧能够正确工作的基础。
3.2 关键设计二:布尔翻转代替计数
常规做法是用计数器统计命中行数,最后判断count == 1。而仓库实现用一个布尔值oneRow翻转:
- 每命中一行,
oneRow翻转一次; - 当命中第二行时,
oneRow从true翻回false,此时立即break,无需再检查第三行(早期终止优化); - 循环结束后,
oneRow == true当且仅当单词恰好命中一行。
这个技巧的精妙之处在于:因为单词必然至少命中一行,所以"恰好一行"的判定等价于"翻转次数为奇数",即"没有出现第二次命中"。
3.3 关键设计三:空字符串防御
if len(s) == 0 { continue }空字符串不包含任何字母,理论上不属于任何行,直接跳过。虽然题目约束输入只含字母(空串属于合法边界输入),仓库实现仍做了防御,保证函数对任意输入都安全、无 panic。
3.4 大小写与输出顺序
- 匹配前调用
strings.ToLower(s)统一转为小写,避免大小写影响行归属判定; - 输出时追加的是原始单词
s而非小写形式,保证结果与输入大小写一致(如输出"Alaska"而非"alaska"); - 输出顺序与输入顺序保持一致,符合题目要求。
3.5 复杂度分析
- 时间复杂度:对每个单词执行最多 3 次
ContainsAny,每次ContainsAny的时间约为O(|r| + |s|),其中|r|为行字符集长度(第一行 10、第二行 9、第三行 7,均为小常数),|s|为单词长度。总复杂度为O(总字符数)量级,可视为线性; - 空间复杂度:
rows、lowerS为 O(1) 辅助空间(不含返回值),输出数组output属于题目要求的返回结果,不计入辅助空间。
四、测试验证:Test_Problem500 用例剖析
仓库为本题编写了 500. Keyboard Row_test.go,采用该仓库统一的"para/ans 结构体 + 用例表"测试风格:
func Test_Problem500(t *testing.T) { qs := []question500{ { para500{[]string{"Hello", "Alaska", "Dad", "Peace"}}, ans500{[]string{"Alaska", "Dad"}}, }, { para500{[]string{"", "qwe", "asd"}}, ans500{[]string{"qwe", "asd"}}, }, } // ... for _, q := range qs { _, p := q.ans500, q.para500 fmt.Printf("【input】:%v 【output】:%v\n", p, findWords500(p.one)) } }两个用例分别覆盖:
- 官方示例:
["Hello", "Alaska", "Dad", "Peace"]→["Alaska", "Dad"],验证基础判定逻辑; - 边界用例:
["", "qwe", "asd"]→["qwe", "asd"],验证空字符串被安全跳过、单行单词正常输出。
4.1 如何运行本题测试
仓库根目录的 go.mod 声明了模块github.com/halfrost/LeetCode-Go(Go 1.19),可直接在仓库根目录执行:
# 只运行本题的测试 go test -v ./leetcode/0500.Keyboard-Row/ -run Test_Problem500 # 运行该目录全部测试(含编译检查) go test ./leetcode/0500.Keyboard-Row/4.2 如何生成全仓库覆盖率报告
仓库根目录的 gotest.sh 提供了覆盖全仓库leetcode包的一次性覆盖率生成方式(go 1.10+支持对多包一次性-coverprofile,可直接产出单个合法 profile):
./gotest.sh # 等价于:go test -covermode=atomic -coverprofile=coverage.txt ./leetcode/...执行后会在仓库根目录生成coverage.txt(该文件已存在于仓库中),可供 Codecov 等工具解析。项目 README 声称 100% 测试覆盖率与 runtime beats 100%,本题的测试文件即是对这一仓库级声明在单题维度的体现——findWords500的全部分支(正常单词、空串跳过、跨行 break)均被用例覆盖。
五、边界情况与易错点总结
综合原文档的注意点与源码实现,实战中容易踩坑的地方有:
| 场景 | 处理方式 | 依据 |
|---|---|---|
| 单词含大写字母 | 先ToLower再匹配,输出保留原大小写 | 源码第 12、23 行 |
| 空字符串 | 直接continue跳过 | 源码第 9-11 行 |
| 单词重复字母 | 无需去重,ContainsAny天然容忍重复 | 题目 Note 1 |
| 命中多个行 | 第二次命中即break,提前终止 | 源码第 16-19 行 |
| 输入非字母字符 | 题目约束不会出现;实现对此无防御,需自行扩展 | 题目 Note 2 |
六、延伸:同一问题的其他常见解法
作为对比,社区常见的同类解法还有两种思路(本文仓库未收录,仅作思路拓展):
- 字符→行号映射表:用
map[byte]int预先记录每个字母所属行号,再对单词逐字符查表,若所有字符行号一致则通过。查询为 O(1),适合频繁复用的场景; - 位掩码法:给三行分别分配 1、2、4 三个 bit,将单词所有字母的"行掩码"做按位或,若结果为 2 的幂(
result & (result-1) == 0)则说明只属于一行。该法把判定收敛为一次位运算,代码更紧凑。
仓库选用的ContainsAny+ 布尔翻转方案,胜在代码极简、无需额外建表,是"以时间换实现简洁度"的典型取舍,非常适合面试中快速写出正确解。
七、小结
本文以 LeetCode 500 Keyboard Row 为切入点,完整覆盖了题目理解、示例分析、约束解读,并深入剖析了 LeetCode-Go 仓库中 findWords500 实现 的三个关键设计——ContainsAny行匹配、布尔翻转代替计数、空串防御——以及对应的 测试用例 与运行命令。掌握这道题的核心价值在于:理解"多行集合归属判定"问题的通用套路,后续遇到如"同字母异序词分组""外星字典判定"等题目时,可以复用同一套"集合映射 + 逐字符校验"的思维框架。
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考