1. 题目拆解:在做一件什么事
力扣219题“存在重复元素2”看起来只是简单地在“存在重复元素”这题后面加了个数字,但内核完全不同。
先读题:给你一个整数数组nums和一个整数k,请判断数组中是否存在两个不同的下标i和j,满足nums[i] == nums[j]且|i - j| <= k。
简单说就是:在数组里找两个相同的数,并且它们的位置不能隔得太远,距离不能超过 k。
一个很直观的例子:
nums = [1, 2, 3, 1] k = 3数组里有三个1?其实只有数组开头和结尾各一个1,它们下标分别是 0 和 3,距离是 3,刚好满足k = 3,所以答案是true。
再看一个:
nums = [1, 2, 3, 1, 2, 3] k = 2第一个1在位置 0,第二个1在位置 3,距离是 3,大于 2,不满足条件;2在位置 1 和 4,距离 3,也不行;3在位置 2 和 5,距离 3,还是不行。所以返回false。
这道题看着不起眼,但它是哈希表 + 滑动窗口这个经典组合的入门级代表。很多人在只会暴力解的时候觉得这题很简单,但等到面试时被要求给出时间复杂度为O(n)的解法,才发现里面有些边界细节特别容易踩坑。这篇文章不打算只贴一个答案,而是把为什么这样解、每一步在做什么、换一种写法会错在哪,全部拆开讲清楚。
适合的读者包括:刚开始刷算法题准备面试的同学,已经刷过几十题想梳理“哈希表窗口类”技巧的人,还有想在复杂度分析上更进一步、搞明白“为什么空间是 O(k)”的老手。
2. 整体思路设计:为什么不能上来就排序或暴力
2.1 暴力解法的天花板
很多人拿到这题的第一反应是双重循环:
def containsNearbyDuplicate(nums, k): n = len(nums) for i in range(n): for j in range(i + 1, n): if nums[i] == nums[j] and abs(i - j) <= k: return True return False这个解法在思路上完全正确,没有任何逻辑问题,但时间复杂度是O(n^2)。当数组长度n到 10 万级别,k也很大时,双层循环需要检查接近n^2/2对组合,也就是几十亿次比较,在力扣上必定超时。
为什么暴力会超时?因为这里面做了大量无用功。举个例子,如果k = 100,下标i和下标j距离超过 100 的配对,根本不在我们关心的范围内,但内层循环会把它们全部检查一遍。限制条件|i - j| <= k其实已经告诉你:每个元素只需要和它右边最多 k 个邻居比较就够了。
2.2 窗口的本质:把目光限制在最近 k 个元素内
顺着上面的思路,有一个关键转变:对任意一个位置i而言,我们只需要关心这个位置之前的 k 个位置里,有没有和nums[i]相同的值。
因为只要在i左侧距离 k 以内出现过相同的数,就找到了答案;如果距离超过了 k,就算数值相同,也不在当前需要考虑的范围内。
这就像一个宽度为 k 的窗口,从左往右滑动,窗口里的元素是我们“当前还记得”的内容。窗口左边的元素滑出了视野,不再需要关心。
为什么窗口宽度是k而不是k + 1?这其实一个特别容易搞混的点:
- 两个下标
i和j的距离是|i - j|,比如i = 0,j = 3,中间隔着下标 1、2,但距离是 3。 - 当遍历到位置
j时,能和它形成配对的位置是[j - k, j - 1],一共 k 个位置。 - 如果当前窗口里正好有 k 个元素,那么检查的其实是“当前位置前 k 个元素是否包含与当前元素相等的值”。
结合具体的例子:
nums = [1, 2, 3, 1] k = 3遍历到下标 3,也就是元素1时,窗口里应该有下标 0、1、2 共 3 个元素,也就是[1, 2, 3],刚好是 k 个。如果窗口大小是 k,下标 0 在窗口中,就能发现重复;如果你不小心把窗口大小写成k + 1,在某些用例下也不一定错,但维护起来就会混乱,而且可能把不该算进来的元素包含进来。
再换一个序列验证:
nums = [1, 2, 3, 4, 1] k = 3遍历到最后一个1(下标 4)时,窗口内应该是下标 1、2、3,也就是[2, 3, 4],并不包含下标 0 的那个1,因为下标 0 和下标 4 的距离是 4 > 3,所以不应该算。如果你把窗口做成“保留 k+1 个元素”,下标 0 就还在窗口里,就会错误地返回true。
2.3 为什么不能先排序
有人可能会想:既然要找重复元素,不如先把数组排序,排序之后相同的元素都会相邻,这不就简单了吗?
这个思路在“判断有无重复元素”的题目里确实可行,但在本题中不行,因为排序会破坏下标信息。|i - j|的前提是原始下标之间的距离,一旦排序,下标全变了,距离没有任何意义。
牢记一件事:**当题目要求元素之间的相对位置关系时,排序法基本是自废武功。**这道题必须在保持原有顺序的情况下做判断。
3. 核心实现原理:哈希表为什么够用
3.1 用哈希表存放“值到最后一次出现的下标”
最常用的解法是用一个哈希表记录“值 -> 最近一次出现的下标”。遍历数组时,每个元素先在哈希表里查一下:
- 如果哈希表中存在相同的值,说明之前已经出现过,取出之前的下标
prev,判断i - prev <= k是否成立。 - 如果成立,直接返回
true。 - 如果存在相同值但距离超过 k,不要直接放弃,而是要把当前下标更新到哈希表中。
- 如果哈希表中不存在该值,就把当前值存进去,下标是当前
i。
这里有个很关键的点:为什么同一个值如果有过更早的出现位置,后来出现时,我们要用后出现的下标覆盖旧下标?原因很简单——后面的元素只可能和“更接近当前元素的那个旧位置”形成满足距离要求的配对,因为越靠后的旧位置距离当前元素越近,更有机会满足<= k的条件。
用一个具体例子说明:
nums = [1, 0, 1, 1] k = 1- 下标 0,值 1,哈希表为空,存入
{1: 0}。 - 下标 1,值 0,哈希表没有 0,存入
{1: 0, 0: 1}。 - 下标 2,值 1,哈希表里有 1,旧下标是 0,
2 - 0 = 2 > k = 1,不满足。此时注意:不能什么都不做,应该把哈希表里的 1 更新为下标 2,变成{1: 2, 0: 1}。 - 下标 3,值 1,哈希表里有 1,旧下标是 2,
3 - 2 = 1 <= k,返回true。
如果第三步不更新下标,到了第四步时,你会拿旧下标 0 去计算,距离 3,还是返回错误结果。所以“更新哈希表中相同值的下标”是必不可少的一步。
3.2 标准代码实现与逐行说明
用 Python 写最简洁的版本:
def containsNearbyDuplicate(nums, k): pos = {} for i, num in enumerate(nums): if num in pos and i - pos[num] <= k: return True pos[num] = i return False这段代码只有几行,但每一行的作用都很关键,逐行拆开说:
pos = {}:哈希表,键是数组里的数字,值是该数字最近一次出现的下标。for i, num in enumerate(nums):同时拿到下标和值。这里如果用普通的for i in range(len(nums))方式也能写,但enumerate更简洁,也更不容易下标越界。if num in pos and i - pos[num] <= k:先判断这个值以前有没有出现过,再判断距离是否满足条件。注意顺序很重要,Python 的and有短路机制,如果num in pos为假,后面的i - pos[num]根本不会执行,不会出现访问不存在的键的问题。pos[num] = i:无论之前的判断结果如何,都要把当前下标更新进去。这一步相当于是“窗口内容”的维护。- 整个循环结束后没有返回
true,说明所有元素都检查完了,窗口内没有任何满足条件的重复对,返回false。
对应的 Java 版本也很容易写,只是细节上略有区别:
public boolean containsNearbyDuplicate(int[] nums, int k) { Map<Integer, Integer> pos = new HashMap<>(); for (int i = 0; i < nums.length; i++) { if (pos.containsKey(nums[i]) && i - pos.get(nums[i]) <= k) { return true; } pos.put(nums[i], i); } return false; }3.3 另一种思路:用集合模拟滑动窗口
除了哈希表存下标,还可以用哈希集合维护一个实际的“窗口”。窗口内容就是当前元素之前的 k 个不同数值(集合只能判断“是否存在”,不能存下标,所以必须主动控制集合的规模)。
def containsNearbyDuplicate(nums, k): window = set() for i, num in enumerate(nums): if num in window: return True window.add(num) if len(window) > k: window.remove(nums[i - k]) return False这个写法更贴切地体现了“滑动窗口”的名字:当窗口大小超过 k 时,就从集合中移除最老的元素,也就是nums[i - k]。这样window里永远只保留“从当前元素往前数 k 个位置”中的不同值。如果当前元素已经在window里,说明有一个不超过 k 的距离内的重复值存在。
这个写法和哈希表版本在本质上是一样的,只是前者存下标、判断距离,后者用“及时移除过期元素”来保证集合里的元素一定满足距离条件。两者都需要处理同一个关键细节:过期的元素怎么移除、什么时候移除。
使用集合版本时,最容易犯的错误是只add不remove,导致窗口里有超期的元素,最终错误判断。下面这个测试用例就能暴露这种错误:
nums = [1, 2, 3, 1] k = 2如果只 add 不 remove,遍历到最后一个 1 时集合里有{1, 2, 3},发现 1 存在,返回true,但正确结果是false,因为两个 1 之间的距离是 3,大于 2。
3.4 两种实现的时间与空间复杂度对比
| 实现方式 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力双重循环 | O(n^2) | O(1) | 数据量极小,约 n <= 1000 |
| 哈希表存下标 | O(n) | O(m),m 是数组中不同值的个数 | 数据量大、取值分散 |
| 哈希集合维护窗口 | O(n) | O(k) | 数据量大、k 远小于 n |
哈希表存下标时,空间复杂度严格来说是O(m),m是数组中不同数字的个数。如果数组里没有重复,m = n,空间就是O(n)。集合版本虽然空间也是O(k),但如果 k 大于 n,集合里最多也就是 n 个元素,本质也不会超过O(n)。
有人会纠结这两种写法哪个好。实际做笔试和面试时我推荐哈希表存下标,理由有三个:
- 代码更短,出错的概率更低。
- 不需要额外费心维护集合的删除操作,减少出现“忘记删过期元素”这类低级错误。
- 空间也不会比集合版本差。
但集合版本的思路适合写在题解里展示你对“滑动窗口”概念的理解,如果面试官问“你是怎么保证窗口内元素一定在新元素的 k 范围内”,你能清楚地解释集合的加入和删除时机,也很加分。
4. 边界测试与易错点实战梳理
4.1 边界用例速查表
刷题时重要的不只是把样例过了,而是把各种极端情况想清楚。这道题的边界主要有以下几类。
| 用例 | k | 预期结果 | 说明 |
|---|---|---|---|
[] | 任意 | false | 空数组直接返回 false |
[1] | 任意 | false | 只有一个元素,不可能形成配对 |
[1,1] | 1 | true | 相邻重复元素 |
[1,1] | 0 | false | 下标必须不同,距离必须 <= 0 不可能 |
[1,2,1] | 1 | false | 两个 1 之间隔着 2,距离为 2 |
[1,2,1] | 2 | true | 距离等于 2 时满足条件 |
[1,2,3,1] | 3 | true | 距离刚好等于 k,边界值要算进去 |
[1,2,3,4,1] | 3 | false | 距离为 4,超过 k |
[1,0,1,1] | 1 | true | 第三次出现 1 时,应该用最近一次的下标 2 判断 |
[1,2,3,4,5] | 巨大值 | false | 没有重复值,k 再大也没用 |
特别注意k = 0的情况。i - pos[num] <= 0,因为i一定大于或等于pos[num],而数组中又不允许同一个下标和自己配对,所以只要k = 0就一定返回false。有的代码里用<= k,有的用i - pos[num] < k,如果写成< k,那么距离恰好等于 k 的情况就会漏判。正确的写法是<= k,因为题目要求|i - j| <= k。
4.2 最容易踩的坑:窗口大小写成 k + 1
前面提到过窗口大小问题,这里用一个完整案例再展示一下它是怎么影响答题结果的。某位开发者 A 在写集合版本时,觉得“窗口里应该保留包含当前元素在内的 k+1 个元素”,于是把删除条件写成了:
if len(window) > k + 1: window.remove(nums[i - k - 1])这样做的结果是,窗口确实容纳了k + 1个元素。借助一个例子看有没有问题:
nums = [1, 2, 3, 4, 5, 1] k = 3最后一个 1 的下标是 5,第一个 1 的下标是 0,距离是 5,显然不满足条件。但窗口如果保留 4 个元素,遍历到下标 2 时,窗口里有下标 0、1、2 的三个元素[1, 2, 3];遍历到下标 5 时,窗口里从下标 2 开始保留到下标 5?需要好好算一下才能发现,这会导致窗口最前面仍然保留了下标 1 甚至下标 0 的元素,造成误判。所以在集合版本里,窗口保留的元素数量就是 k,而不是 k + 1。
怎么理解“窗口里有 k 个元素”这件事?当前下标是i,window里应当是i - k到i - 1这些位置的值,正好 k 个。当前元素nums[i]还没有加入集合时,先检查它是否在集合中,如果不在,再加入。当集合长度超过 k 时,说明窗口右侧已经推进到下一个位置,最左边的位置i - k需要移除。
4.3 易错点:只更新不判断会忽略更近的重复
还有一种错误的写法是:只要发现哈希表里已经有这个值,就直接更新下标,不做距离判断,想着“反正以后还会遇到”。这种做法在有多轮重复值时会有麻烦。
nums = [1, 2, 1, 3, 1] k = 2正确结果应该是true,因为下标 0 的 1 和下标 2 的 1 距离为 2。有个错误版本一遇到num in pos就更新下标而不判断距离,等到下标 4 的 1 出现时,拿到的 pos[1] 是 2,判断4 - 2 = 2 <= k成立,也能返回 true。看起来“歪打正着”了对吧?那再看一个用例:
nums = [1, 0, 1, 3, 1] k = 1下标 0 的 1 和下标 2 的 1 距离是 2,不满足;下标 2 的 1 和下标 4 的 1 距离是 2,还是不满足。正确结果是 false。如果不在发现重复时判断距离、而只是更新下标,遍历到下标 2 时会直接更新 pos[1] = 2;遍历到下标 4 时用 pos[1] = 2 计算,距离 2 > 1,能正确返回 false。这个特例看起来没问题,但换成如下用例:
nums = [1, 0, 1, 1] k = 1如果下标 2 出现 1 时只更新不判断,pos[1] 变成 2;下标 3 出现 1 时,距离3 - 2 = 1满足条件,返回 true,看似也对。那问题到底出在哪?
核心隐患在于:如果旧位置与当前位置不满足距离约束,你不能确定下一个出现的位置就满足,但你也不能确定它不满足。要判断正确,必须在每次出现重复时都做距离检查,因为题目不是找“任意两个重复”,而是找“任意一对距离不超过 k 的重复”。如果忽略早期判断,在某些精心构造的用例中会出现漏判。举个例子:
nums = [1, 0, 1, 2, 1] k = 1下标 0 的 1 与下标 2 的 1 距离 2;下标 2 的 1 与下标 4 的 1 距离 2;下标 0 和下标 4 距离 4。没有一个满足,正确 false。如果“只更新下标、不做距离判断”,整个过程都不会触发 true,结果正确。
但如果这样:
nums = [1, 0, 1, 2, 1, 1] k = 1下标 2 的 1 与下标 4 的 1 距离 2;下标 4 的 1 与下标 5 的 1 距离 1,正确 true。如果“只更新不判断”,在下标 4 时 pos[1] 更新为 4,下标 5 时距离 1,也true。看起来还是碰巧能过。
但要构造一个反例也很容易:
nums = [1, 0, 1, 0, 1, 0, 1] k = 1正确结果:任意两个相邻的相同元素之间都隔着 0,不存在距离为 1 的同值相邻,所以 false。“只更新不判断”的写法:遍历到每个 1 时距离都是 2,不触发 true,最终 false,好像也正确。
为了让反例更清晰,需要让“最近一次”和“更早一次”的距离差异导致漏判:
nums = [1, 2, 1, 3, 1] k = 1正确 false(下标 0 和 2 距离 2,下标 2 和 4 距离 2,下标 0 和 4 距离 4,均 > 1)。只更新不判断时也 false,依旧正确。
这让我意识到,“只更新不判断”在多数普通用例下确实不容易出错,但它在逻辑上是错误的,因为更新下标会丢失旧下标信息,而旧下标可能才是唯一满足距离条件的配对。看这个用例:
nums = [1, 2, 1, 3, 1, 0, 1] k = 2下标 0 和 2 的 1 距离 2,满足,正确 true。只更新不判断:下标 2 更新 pos[1]=2;下标 4 的 1 距离 2 也 true,还是能发现。要构造“旧下标满足但新下标不满足”的情况:
nums = [1, 2, 1, 2, 3, 1] k = 2下标 0 和 2 的 1 距离 2 满足,正确 true。只更新不判断时下标 2 更新 pos[1]=2,下标 5 时距离 3 > 2,不会从这得到 true——但已经错失了下标 0 和 2 这一对!它最终返回 false,这就是一条真实存在的反例。
所以结论很明确:判断条件i - pos[num] <= k要在更新之前做,不能省。因为你有可能先遇到一个满足条件的旧配对,然后又遇到一个不满足条件的新配对,只关注最近一次出现会让你错失答案。
有人可能会想:“那我把所有出现过的下标都存下来,例如pos[num]存一个列表,然后遍历列表查有没有距离小于 k 的?”用二分查找也能做到 O(n log n),但没必要。仔细想想:如果一个值出现了多次,其实只需要判断“相邻出现之间”的距离即可。因为如果任意两次出现距离小于等于 k,它们之间如果还有更近的邻居也一样能命中。而相邻出现的距离是最小的,所以所有出现位置中只要存在相邻距离满足条件,就一定有一对满足条件;如果所有相邻距离都不满足,那么任意两处非相邻的出现只会更远。因此只需存最近一次出现的下标就够了。
4.4 边界条件:负数、大整数、超大 k
这道题对数字的实际大小不关心,负数和正数一样处理,哈希表天然支持整型键。数组长度可能很长,但 k 可能远大于数组长度,这种情况下其实整个数组都落在窗口内,只要存在任意重复元素就返回 true。代码里不需要额外特判,因为窗口永远只包含当前元素之前的 k 个元素;即使 k 非常大,窗口里的元素也不会超过当前下标 i 个,而如果之前出现过相同值,距离i - pos[num]也不可能超过 i,所以照样能正确判断。
但这提示了一个优化点:如果k >= n - 1,其实可以退化为“整个数组是否存在重复元素”的问题。不过这个优化对时间复杂度和代码简洁性都没有本质影响,写不写都行,写上去反而增加了分支,容易引入 bug。
4.5 数值大小与哈希碰撞的注意事项
这道题的约束范围内通常用系统内置哈希表就足够了。但如果你用的语言对哈希冲突特别敏感,或者输入的数值范围很大,自定义哈希函数也不是不行,只是没必要。实测在力扣上直接使用HashMap或 Python 的dict都能稳定通过,没有发现因为哈希碰撞导致超时的极端情况。真正影响性能的往往不是哈希表本身,而是你写了不必要的多次遍历或者在循环内部做了多余操作。
5. 从错误提交到正确解法:一次真实排查过程
这部分写一下我在实际做题时遇到的一个典型错误版本,以及怎么通过测试用例定位问题。有一位开发同学 B 提交了一个版本,他在集合解法里用了一个deque来模拟窗口,但存的是值而不是下标:
from collections import deque def containsNearbyDuplicate(nums, k): q = deque() for num in nums: if num in q: return True q.append(num) if len(q) > k: q.popleft() return False这个版本表面上也能运行,但只要出现重复值,deque里会有两个相同的值,删除的时候会删掉最左边的元素,删除的未必是真正“过期”的元素。例如:
nums = [1, 2, 1, 3] k = 2- 处理 1,队列
[1] - 处理 2,队列
[1, 2] - 处理 1,队列里有 1,直接返回 true。
这个用例下它是正确的,但让队列因为重复值而“压缩”了时间序列,无法把“最老元素”准确移走。再看这个用例:
nums = [1, 2, 3, 1, 2] k = 2- 处理 1:队列
[1] - 处理 2:队列
[1, 2] - 处理 3:队列长度超过 2,移除最左边的 1,队列
[2, 3] - 处理 1:队列无 1,加入,队列
[2, 3, 1] - 处理 2:队列有 2,返回 true。
但正确结果是什么?下标 1 的 2 和下标 4 的 2 距离是 3 > 2,下标 3 的 1 和下标 0 的 1 距离也是 3,没有任何配对满足条件,应该返回 false。问题就出在:队列里同时存在下标 1 和下标 4 的 2,但下标 1 的 2 其实已经过期了,而因为队列里出现重复值,删除时按位置删除会把旧的那个 2 保留得更久,或者把新加入的 2 错删了,导致过期元素没有被清理。所以模拟窗口时队列里必须存下标,而不是值。
正确的双端队列思路应该是:队列里存下标,每次先移除所有“距离超过 k”的队首元素,再判断队首对应的值和当前值是否相等:
from collections import deque def containsNearbyDuplicate(nums, k): dq = deque() for i, num in enumerate(nums): while dq and i - dq[0] > k: dq.popleft() if dq and nums[dq[0]] == num: return True dq.append(i) return False这个版本用“下标差值”来判断何时过期,而不是用队列长度,因此不会受到窗口中重复值数量的干扰。但要注意,这个队列里可能同时存在多个下标,队首是最老的下标,队尾是最新的下标;如果队首与当前元素相等且距离不超过 k,就找到答案。否则把当前下标加入队尾继续滑动。
这个写法的时间复杂度同样是O(n),但空间上队列最多保留 k 个下标,而且实现起来需要处理while循环,比哈希表版本稍微复杂一点。实际面试中更推荐哈希表方式,简洁就是减少出错的最好方式,队列方式主要是帮你深入理解窗口维护的原理。
还有一个容易忽略的细节:如果你用哈希表存下标,不需要显式删除过期的键。比如这个场景:
nums = [1, 2, 3, 4, 1] k = 2哈希表里1 -> 0一直保留到遍历到下标 4 时,发现4 - 0 = 4 > 2,不会返回 true,然后会把1的下标更新为 4。所以即使哈希表里保留着过期下标也不会导致误判,因为你总是用距离计算来判断。这也是为什么哈希表版本往往比集合版本更让人安心的原因:集合版本依赖主动删除过期元素,哈希表版本依赖距离判断,后者天然就是防误判的。
6. 相关题目对比与面试扩展
把力扣的“存在重复元素”家族放在一起看会特别有意思:
- 第217题“存在重复元素”:只要数组里有任意重复值就返回 true,不管距离。解法是排序后看相邻元素,或者直接哈希表,选择非常多。
- 第219题:在217的基础上增加了距离约束
|i - j| <= k,排序法直接失效,必须保持原序,这是滑动窗口的典型应用场景。 - 第220题“存在重复元素 3”:约束变成
nums[i]和nums[j]的差值不超过t,且下标距离不超过k。这题的经典解法是桶排序。为什么不能用219题的思路直接做?因为219只要求“值相等”,用哈希表查重即可;而220要求“值相近”,可能差 0 到 t,你不能用精确匹配,需要按值分桶,每个桶宽度设为t + 1,这样数值差距不超过 t 的元素必然落在同一个桶或相邻桶中。
面试时,面试官比较喜欢让求职者从217讲到219,再追问一句“如果 k 不是下标距离而是值距离,你会怎么做”。很多人的第一反应是“维护一个 k 大小的窗口,然后对窗口内每个元素比较差值”,这样时间复杂度是 O(nk),在 k 比较大时退化为 O(n^2)。而桶排序的思路就能把复杂度压回 O(n)。
理解219的哈希表思想对做220很有帮助,因为桶排序本质上就是在控制一个“按值分桶的滑动窗口”。窗口里的元素会进入不同的桶,当窗口滑动时,对应的桶也要清理和更新,这个套路跟219的集合版本如出一辙。
说实话,219这道题本身没有什么高深技巧,但它处在“简单查重”到“变体进阶”的过渡带上,很适合用来检查你对窗口和哈希表这两个基础工具是否真正理解。如果你能把219的多种写法、边界条件、复杂度优化全部讲清楚,面试中遇到容器类算法题时会更从容。
在实际开发中,这种“最近 k 个位置内是否存在重复”的场景也很多。比如日志分析里,如果同一个用户 ID 在 5 秒内出现两次,可能是并发冲突或重复请求,需要实时检测;再比如某个服务收到相同的内容指纹,如果在最近 10 分钟内重复出现,就要做幂等处理,不重复执行任务。这类问题抽象出来就是:在时间窗口内判断元素是否重复出现。力扣219就是在模拟这种“窗口+去重”的经典场景。
不过要提醒一句:实际开发中数据通常不是一次性全部给定,而是流式地到达,所以更常用的做法是维护一个“最近 k 个元素”的缓存结构,LRU 缓存思想跟这里的滑动窗口其实很像。如果你熟悉 Java 的LinkedHashMap或直接用一个固定长度的环形数组,都能实现类似需求。刷题时不要只觉得“这只是个面试题”,能想明白它背后的工程场景,收获会更大。
最后分享一个小技巧:写完代码后,先手动跑几个边界用例,再用随机小数据对拍暴力解,确保两个版本结果一致。我一般习惯写一个简单的测试函数,生成所有可能的小数组,对比O(n^2)和O(n)解法的输出。这样做的意义不在于证明代码一定能通过力扣,而在于让你对“边界条件为什么这么设计”形成肌肉记忆。你在这个题目上多花半小时做的验证,以后在220题、以及无数个窗口类题目里都会加倍回报给你。