力扣打卡到第5天,我给自己安排了两道高频题:找到字符串中所有字母异位词,和为K的子数组。一道是定长滑动窗口搭配字符频次计数的典型题,另一道是前缀和搭配哈希表的入门题。这两题都属于“连续区间计数”这个大方向,但一个窗口长度固定、靠滚动更新状态,另一个窗口长度任意、靠前缀差把区间和问题转化成两个数相减的问题。热题100里它们出现频率都不低,面试中也经常作为“你能不能看出这题不能用滑窗”的试金石。
如果只是看懂题解抄一遍,收获其实很小。真正值钱的是搞清楚:为什么异位词能用固定窗口?为什么和为K的子数组偏偏不能用双指针滑窗?哈希表里到底该存次数还是位置?这篇文章我会把两道题的完整思考路径、Java实现、边界条件和常见的变式都过一遍,尽量让刷题的人能直接落地复用。
1. 这两道题为什么不建议分开刷
1.1 表面上一滑窗一前缀和,底层都在问“某个区间是否满足条件”
先看题目本身。找到字符串中所有字母异位词(LeetCode 438),给定一个字符串 s 和一个字符串 p,要求返回 s 中所有 p 的异位词的起始索引。所谓异位词,就是字符串长度相同、各字符出现次数相同的词,本质是一个排列问题。比如 s = "cbaebabacd",p = "abc",结果是 [0, 6],因为 s 中下标 0 开始的 "cba" 和下标 6 开始的 "bac",都和 "abc" 拥有相同的字符频次。
和为K的子数组(LeetCode 560),给定整数数组 nums 和整数 k,需要统计连续子数组中和为 k 的子数组个数。比如 nums = [1, 2, 3],k = 3,结果是 2,因为 [1, 2] 和 [3] 两个连续片段都满足和等于 3。
这两题放一起,很多人第一反应是“一道滑动窗口、一道前缀和”,解法完全不一样。但把抽象层次拉高一层,它们本质上属于同一类问题:怎么快速判断一个连续区间的某些统计值是符合要求的。438 关心的是窗口内的字符频次是否和目标字符串字符频次一致,560 关心的是子数组的元素和是否等于固定值 k。一个是频次状态,一个是数值状态,但都需要在遍历过程中高效维护、高效查询。
1.2 放在同一天刷,能顺便记住一套判断模型
很多人刷题是“一题一题孤立刷”,今天记住滑动窗口模板,明天写了前缀和模板,碰到变形题还是认不出来。我习惯把同类别的题安排在同一天,重点不是各写一遍,而是对比它们的判断条件。
438 之所以能用滑动窗口,关键条件是窗口长度固定为 len(p)。既然长度恒定,滑动时只需要把右边新字符加入窗口、把左边旧字符移出窗口,就能 O(1) 维护当前窗口内的频次状态。560 不行,因为子数组长度不固定,如果试图用左右指针调整窗口,你会发现没有一个清晰的“什么时候收缩窗口”的规则。
看清这个差异之后,以后再遇到“连续”和“计数”关键词的题,你会习惯性先问自己三个问题:
- 区间长度是否固定。
- 窗口对应的统计值是否具有单调性。
- 如果不具备单调性,能不能用前缀差值来去掉“区间长度可变”这个变量。
这三个问题都可以从今天这两道题里得到答案,所以我一直觉得这两题是同一个知识块的上下篇,拆开刷反而少了那层顿悟感。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 字母异位词:从字符频次匹配到定长滑动窗口
2.1 异位词的本质是“字符频次相等”,不是“字符串相等”
先明确一个容易犯错的点:异位词不是要求窗口内字符串和 p 字符串排序后相等,而是每个字符出现的次数都相同,并且总长度相同。比如 p = "abc",那 "cba"、"bac"、"acb" 这些排列都算,因为它们底层字符数量分布是 a:1, b:1, c:1。判断两个字符串是否是异构,最稳定的方式就是比较长度为 26 的频次数组,不是先排序再 equals,也不是拿 HashSet
如果 p 的字符分布是 a:2, b:1,某个窗口内也是 a:2, b:1,那就认为匹配。所以问题就变成:在 s 中不断截取固定长度 len(p) 的子串窗口,判断窗口的频次数组和 p 的频次数组是否完全相等。
最直接的暴力写法是枚举每个起点 i,取 s.substr(i, i+m) 统计频次比较,每次统计都要遍历 m 个字符,总代价是 O((n-m+1) * m),这还没考虑数组比较的 26 次循环。当 s 很长或 p 很长时,这个复杂度就会很尴尬。优化的突破口就在于:相邻窗口之间其实只有两个字符发生了变化,左端移出一个,右端加入一个,中间字符的频次根本不用重新统计。
2.2 双频次数组的 Java 实现
固定长度滑窗最直观的写法是用两个数组,一个记录 p 的频次 pcnt,一个记录当前窗口的频次 wcnt,每次滑动后调用 Ar
