- 科学计算
【免费下载链接】codeforces-go
算法竞赛模板库 by 灵茶山艾府 💭💡🎈
导读
本文以 leetcode/biweekly/191/b/README.md 题解为主体,深入剖析力扣双周赛 191 第 2 题「Count Values With Equally Spaced Occurrences II」(等间隔出现次数 II)的经典解法:先用哈希表统计每个值出现的所有下标,再逐一判断这些下标是否构成等差数列。读完本文,你将掌握「位置分组 + 等间隔判定」这一简洁的 O(n) 算法,并看到它在当前仓库中对应的 Go 实现、单测用例与自动化测试框架是如何组织起来的。
一、题意梳理:什么样的值「等间隔出现」
题目给一个整数数组nums,要求统计其中「等间隔出现」的元素的个数。一个值x被视为「等间隔出现」,需要满足:
x在数组中出现至少 3 次;x每次出现的位置下标构成一个等差数列,即相邻两次出现的位置差为一个固定的非零常数。
例如nums = [1,8,1,5,1,5,8,5]:
1出现在下标0, 2, 4,相邻间隔均为2,符合;5出现在下标3, 5, 7,相邻间隔均为2,符合;8只出现在下标1, 6,不足 3 次,不计入。
因此答案为2。
需要注意,本题(II 版本)只要求「至少 3 次」,与同一场双周赛的 Q1(I 版本)恰好要求「刚好出现 3 次」不同,下文会专门对比二者在实现上的差异。
二、核心思路:哈希表分组 + 等差判定
题解给出两步式框架:
- 统计位置:遍历数组,把每个值
x的所有出现下标依次记录在pos[x]中; - 判断等间隔:遍历每个
pos[x],若长度不足 3 直接跳过;否则用pos[1] - pos[0]求出相邻下标差d,再检查该组内所有相邻下标差是否都等于d。若成立,答案加一。
该思路的正确性基于一个简单事实:下标是天然有序的。由于我们在遍历数组时按下标递增的顺序append,pos[x]内部必然按从小到大排列,因此只需比较相邻差是否恒定,即可判断一组位置是否构成等差数列,无需额外排序。
由于每个元素只会被加入一个分组、每个分组只被遍历一次,整体代价与数组长度线性相关。题解给出的复杂度为:
- 时间复杂度:O(n),其中 n 是
nums的长度; - 空间复杂度:O(n),用于存储每个值的出现位置列表。
三、多语言实现:Python / Java / C++ / Go
原题解文档提供了四种语言的完整实现,这里全部保留并补充关键注释,方便对照学习。
Python 3
class Solution: def countSpecialIntegers(self, nums: list[int]) -> int: pos = defaultdict(list) for i, x in enumerate(nums): pos[x].append(i) ans = 0 for p in pos.values(): if len(p) < 3: continue d = p[1] - p[0] if all(y - x == d for x, y in pairwise(p)): ans += 1 return anspairwise(p)来自itertools,逐个生成相邻元素对(p[0], p[1]), (p[1], p[2]), ...,配合all(...)实现等间隔判定,写法最为紧凑。
Java
class Solution { public int countSpecialIntegers(int[] nums) { Map<Integer, List<Integer>> pos = new HashMap<>(); for (int i = 0; i < nums.length; i++) { pos.computeIfAbsent(nums[i], _ -> new ArrayList<>()).add(i); } int ans = 0; for (List<Integer> p : pos.values()) { if (p.size() < 3) { continue; } boolean ok = true; int d = p.get(1) - p.get(0); for (int i = 2; i < p.size(); i++) { if (p.get(i) - p.get(i - 1) != d) { ok = false; break; } } if (ok) { ans++; } } return ans; } }computeIfAbsent(nums[i], _ -> new ArrayList<>()).add(i)是 Java 中「按值分组建索引」的标准写法:键不存在时先创建列表再插入下标。
C++
class Solution { public: int countSpecialIntegers(vector<int>& nums) { unordered_map<int, vector<int>> pos; for (int i = 0; i < nums.size(); i++) { pos[nums[i]].push_back(i); } int ans = 0; for (auto& [_, p] : pos) { if (p.size() < 3) { continue; } bool ok = true; int d = p[1] - p[0]; for (int i = 2; i < p.size(); i++) { if (p[i] - p[i - 1] != d) { ok = false; break; } } ans += ok; } return ans; } };C++17 的结构化绑定auto& [_, p]直接解包出分组列表;ans += ok利用bool到int的隐式转换,省去显式分支。
Go
func countSpecialIntegers(nums []int) (ans int) { pos := map[int][]int{} for i, x := range nums { pos[x] = append(pos[x], i) } next: for _, p := range pos { if len(p) < 3 { continue } d := p[1] - p[0] for i := 2; i < len(p); i++ { if p[i]-p[i-1] != d { continue next } } ans++ } return }Go 版本有两处值得学习的惯用法:
- 具名返回值:
(ans int)声明了具名返回变量,函数体内ans++后直接return即可返回结果; - 带标签的
continue next:当内层循环发现间隔不相等时,直接跳出整组判定并跳到外层循环处理下一个分组,避免引入额外的布尔标志位。
四、仓库源码级验证:Go 实现、测试数据与自动化测试
该题解在仓库中并非孤立存在,b.go 是完整的可运行实现,与之配套的 b.txt 和 b_test.go 共同构成了可自动验证的最小闭环。
4.1 实现文件
b.go 与题解文档中的 Go 代码完全一致:用map[int][]int{}分组记录下标,再对每个分组做等间隔判定,逻辑与文档一一对应。
4.2 测试用例文件
b.txt 以「输入 + 期望输出」交替的格式存放了 3 组用例:
[1,8,1,5,1,5,8,5] 2 [8,8,8,8] 1 [8,6,6,8,8] 0可以手工推演验证:
| 用例 | 位置分组 | 判定过程 | 结果 |
|---|---|---|---|
[1,8,1,5,1,5,8,5] | 1→[0,2,4],8→[1,6],5→[3,5,7] | 1 间隔 2 成立;8 不足 3 次;5 间隔 2 成立 | 2 |
[8,8,8,8] | 8→[0,1,2,3] | 间隔恒为 1,成立 | 1 |
[8,6,6,8,8] | 8→[0,3,4],6→[1,2] | 8 的间隔为 3、1 不相等;6 不足 3 次 | 0 |
其中第三组用例特意构造了「出现次数足够但间隔不等」的反例,用来拦截「只判断出现次数、不判断等间隔」的错误实现。
4.3 自动化测试入口
b_test.go 的测试函数只有寥寥数行,核心是调用了测试工具库的testutil.RunLeetCodeFuncWithFile:
func Test_b(t *testing.T) { if err := testutil.RunLeetCodeFuncWithFile(t, countSpecialIntegers, "b.txt", 0); err != nil { t.Fatal(err) } }从 leetcode/testutil/leetcode.go 的实现可以看到该框架的工作方式:
os.ReadFile(filePath)读取b.txt的原始内容,经trimSpaceAndEmptyLine清洗为逐行字符串;- 通过反射
reflect.TypeOf(f)读取被测函数的入参/出参个数,据此推算每组用例占用的行数(fNumIn + fNumOut); - 将每组合并为一个 example,交给
RunLeetCodeFuncWithExamples执行:解析输入、调用被测函数、比对期望输出并给出通过/失败结论。
这意味着只要把新的「输入 + 期望输出」追加进b.txt,无需改动任何 Go 代码,就能自动扩展回归测试——这正是这套题解仓库把「题解文档、实现代码、测试数据、测试框架」四者打通的体现。
五、延伸对比:Q1(恰好 3 次)与 Q2(至少 3 次)
同场双周赛的 Q1「Count Values With Equally Spaced Occurrences I」与本篇 Q2 使用完全相同的算法骨架,唯一的区别在于判定条件。仓库中的 a.go 是 Q1 的 Go 实现:
func countSpecialIntegers(nums []int) (ans int) { pos := map[int][]int{} for i, x := range nums { pos[x] = append(pos[x], i) } for _, p := range pos { if len(p) == 3 && p[1]-p[0] == p[2]-p[1] { ans++ } } return }两版代码的对照一目了然:
- Q1(a.go):
len(p) == 3,只统计「恰好出现 3 次且等间隔」的值;因为长度固定为 3,只需比较p[1]-p[0]与p[2]-p[1]这一对差值; - Q2(b.go):
len(p) >= 3(通过跳过len(p) < 3实现),统计「至少出现 3 次且相邻间隔全部相等」的值;长度不定,需用循环逐一校验所有相邻差。
Q1 的测试数据 a.txt 与 Q2 完全相同,但期望输出不同——比如对[8,8,8,8],Q1 因 8 出现了 4 次(不满足恰好 3 次)而输出 0,Q2 则因 8 的下标[0,1,2,3]等间隔而输出 1。这组「同数据、异答案」的用例,恰好精准地刻画了两个版本的语义差别,也提醒我们在竞赛或面试中务必先确认「至少」还是「恰好」的表述。
六、边界情况与易错点总结
围绕该算法,有几点值得在实战中留意:
- 出现次数不足 3 次:直接跳过。位置列表长度小于 3 时,任何等差判定都无意义;
- 下标差恒定性:等间隔要求是所有相邻差都相等。即使
p[1]-p[0]与p[2]-p[1]相等,只要后续某一段差不同,该值依然不能计入; - 下标天然有序:遍历时按
i递增顺序append,pos[x]内部无需排序即可直接做相邻差比较; - 元素值域:
pos的键是数组元素本身(可能为负数或大整数),用哈希表(而非按值开数组)分组,才能保证空间复杂度与出现元素种类相关而非与值域相关; - 复杂度不随分组数量退化:即使所有元素互不相同,每个分组长度也为 1,内层循环立即跳过,整体仍是 O(n)。
结语
「等间隔出现次数 II」的解法虽然短小,却完整展示了哈希位置分组这一基础而重要的建模技巧:把一个「判断分布规律」的问题,转化为「对每个值收集下标、再检查等差数列」的线性扫描问题。配合本仓库 b.go、b_test.go、b.txt 以及 leetcode/testutil/leetcode.go 的自动化测试框架,你可以直接本地运行go test复现全部验证过程,并将这套「位置分组 + 等差判定」的思路迁移到诸如「字符等间隔出现」「周期性模式检测」等同类问题中。
- 科学计算
【免费下载链接】codeforces-go
算法竞赛模板库 by 灵茶山艾府 💭💡🎈
相关推荐
codeforces-go 题解剖析:力扣双周赛 176 Q2「前缀连通组」哈希表计数解法
codeforces go 题解剖析:力扣双周赛 176 Q2「前缀连通组」哈希表计数解法 导读 本题是力扣双周赛 176 的第二题(Number of Pre
科学计算交替异或划分计数:前缀异或 + 双哈希表 DP 精讲(力扣双周赛 174 Q3 · codeforces-go 题解精读)
交替异或划分计数:前缀异或 + 双哈希表 DP 精讲(力扣双周赛 174 Q3 · codeforces go 题解精读) 本篇以 codeforces go
科学计算codeforces-go 题解精讲:力扣双周赛 166 Q1「多数频数字符组」的频数分组技巧与 Go 实现
codeforces go 题解精讲:力扣双周赛 166 Q1「多数频数字符组」的频数分组技巧与 Go 实现 本篇基于 codeforces go 仓库中 le
科学计算
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考