LeetCode 242. Valid Anagram 题解:Go 实现字母异位词判断的计数法原理与实战
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
导读
本文围绕 LeetCode 第 242 题「有效的字母异位词(Valid Anagram)」展开,以本仓库(LeetCode-Go)中该题的官方题解文档与 Go 源码为骨架,完整讲解题目约束、计数打表法的核心思路、两种 Go 实现(定长数组计数与哈希表计数)、边界条件与测试用例,并顺带剖析 Follow up 中 Unicode 场景的适配方案。读完本文,你将掌握字母异位词判定的标准套路,并能举一反三应用到字母异位词分组等进阶题型中。
题目原文与核心考点
题目定义如下:给定两个字符串s和t,编写一个函数判断t是否为s的字母异位词(anagram)。原题给出的两个示例为:
Input: s = "anagram", t = "nagaram" Output: trueInput: s = "rat", t = "car" Output: false第一组示例中,"nagaram"由"anagram"的字母重新排列而成(a、n、a、g、r、a、m 六个字母逐一对应),因此返回true;第二组示例中"rat"与"car"的字符构成不一致,返回false。
题目还附带两个重要说明:
- Note:可以假设输入字符串仅包含小写字母(lowercase alphabets),这是选择定长数组解法的前提条件;
- Follow up:如果输入包含 Unicode 字符,应该如何调整解法?
题目大意(中文释义)
题解文档给出的中文概括为:给出 2 个字符串s和t,如果t中的字母在s中都存在,输出true,否则输出false。更严谨地说,字母异位词要求两个字符串长度相同、字符构成完全相同、仅排列顺序不同——这一点从仓库测试用例{"", "1"}、{"a", "ab"}均返回false可以得到印证。
解题思路:计数打表法
题解文档给出的核心思路是计数打表(counting with lookup table),流程分三步:
- 先建立一个容量为 26 的数组,下标依次对应 26 个小写字母(
0对应a,25对应z); - 扫描字符串
s,每遇到一个字母,就在对应下标处加 1; - 再扫描字符串
t,每遇到一个字母,就在对应下标处减 1。
如果t是s的字母异位词,那么t中每个字母的出现次数必然与s完全一致,经过"先加后减"的抵消后,表中所有值都应归零;反之,只要表中存在非 0 值,就说明两个字符串的字符计数不一致,输出false。
该方法的时间复杂度为 O(n)(只需线性扫描两遍字符串,且每遍操作均为 O(1) 的数组下标访问),空间复杂度为 O(1)(固定 26 个整数的数组,与输入规模无关)。
源码实现一:定长数组计数(小写字母场景)
仓库解法一实现了上述打表思路,完整代码如下(见 242. Valid Anagram.go):
// 解法一 func isAnagram(s string, t string) bool { alphabet := make([]int, 26) sBytes := []byte(s) tBytes := []byte(t) if len(sBytes) != len(tBytes) { return false } for i := 0; i < len(sBytes); i++ { alphabet[sBytes[i]-'a']++ } for i := 0; i < len(tBytes); i++ { alphabet[tBytes[i]-'a']-- } for i := 0; i < 26; i++ { if alphabet[i] != 0 { return false } } return true }对这段代码的逐步拆解:
alphabet := make([]int, 26):创建定长计数数组,零值即初始计数 0;- 长度预检
len(sBytes) != len(tBytes):长度不同的两个字符串不可能互为字母异位词,直接短路返回false,省去后续全部扫描; sBytes[i]-'a':利用 ASCII 码连续性,把字符'a'~'z'映射到数组下标0~25。这也是题目要求"仅含小写字母"的原因——若输入混入大写字母或数字,该偏移计算会越界或错位;- 先对
s全体++,再对t全体--,最终遍历 26 个槽位,存在非 0 即返回false。
源码实现二:哈希表计数(通用字符场景)
仓库解法二改用map[rune]int替代定长数组,完整代码如下(见 242. Valid Anagram.go):
// 解法二 func isAnagram1(s string, t string) bool { hash := map[rune]int{} for _, value := range s { hash[value]++ } for _, value := range t { hash[value]-- } for _, value := range hash { if value != 0 { return false } } return true }与解法一相比,这里有两个关键差异:
- 按 rune 迭代而非按 byte 迭代:
range在字符串上迭代时解出的是 Unicode 码点(rune),天然按字符边界切分,不会把多字节 UTF-8 字符拆碎; - 用哈希表动态扩展槽位:字符集不再被限制为 26 个小写字母,任意 Unicode 字符都能作为 key 计数。
这两点使得解法二天然适配题目的 Follow up——当输入包含 Unicode 字符(如中文、emoji、带声调字母)时,只需把解法二中的range改按[]rune(s)迭代,即可正确完成计数。这也印证了仓库中解法一、解法二并存的设计意图:定长数组解法面向"仅小写字母"的标准场景追求极致的 O(1) 空间,哈希表解法面向通用字符场景保证正确性。
测试用例与边界验证
仓库测试文件 242. Valid Anagram_test.go 使用表驱动(table-driven)风格组织用例,覆盖了如下关键场景:
输入s | 输入t | 期望输出 | 覆盖的边界 |
|---|---|---|---|
"" | "" | true | 两个空字符串互为异位词 |
"" | "1" | false | 长度不等 + 非字母字符 |
"anagram" | "nagaram" | true | 题目示例一(正例) |
"rat" | "car" | false | 题目示例二(反例) |
"a" | "ab" | false | 长度不等 |
"ab" | "a" | false | 长度不等(长度预检兜底) |
"aa" | "bb" | false | 长度相等但字符构成不同 |
其中{"aa", "bb"}用例尤其值得注意:它验证了长度相等并不足以判定异位词——两个字符串必须逐字符计数完全一致才行,这正是计数表最终校验"全 0"的意义所在。运行go test执行该测试文件,会依次打印每个用例的输入与输出结果,全部断言通过(仓库目标为 100% 测试覆盖率,该题解法一、解法二均被同一测试集覆盖)。
思路延伸:异位词判定在仓库进阶题中的应用
字母异位词判定的计数思路,在本仓库的进阶题目中有直接延伸。例如 49. Group Anagrams.go 将字符串按 rune 排序后作为哈希 key,把互为异位词的字符串归入同一分组——排序后的字符串本质上是字符计数的另一种等价表达(两个字符串异位词当且仅当它们排序后相等)。该题实现同样通过[]rune(str)处理多字节字符,与解法二的 Unicode 兼容策略一脉相承,可作为读完本题后的下一道练手题。
小结
- 核心结论:字母异位词判定等价于"字符计数向量相等",用一次加、一次减的抵消操作即可在 O(n) 时间内完成;
- 适用前提:定长 26 数组解法依赖"仅含小写字母"的题目约束;突破该约束时切换为
map[rune]int计数即可,仓库解法二已给出可直接运行的实现; - 边界意识:先做长度预检、再校验计数表全 0,两个条件缺一不可,仓库测试用例已完整覆盖这两类反例。
【免费下载链接】LeetCode-Go✅ Solutions to LeetCode by Go, 100% test coverage, runtime beats 100% | LeetCode 题解项目地址: https://gitcode.com/GitHub_Trending/le/LeetCode-Go
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考