离了滑动窗口还真不行?——很多人在算法面试里遇到“寻找和为K的子数组”这道题,第一反应就是套双指针。我第一次认真做这道题是在一次模拟面试里,原题说给一个整数数组 nums 和一个整数 k,要统计满足 nums[i..j](i <= j)的和恰好等于 k 的连续子数组个数。我脱口而出“滑动窗口”,结果被面试官一句话堵回来:“数组里有负数,你怎么滑?”这题核心不是窗口,而是“前缀和 + 哈希表”。今天我把整个解题过程、原理推导、常见翻车点一次性说清楚。
1. 先看懂题:为什么不能无脑套滑动窗口
1.1 题面拆解与输出口径
这题在算法题库里通常长这样:给定整数数组 nums 和一个整数 k,请你返回数组中和为 k 的连续子数组的个数。这里有几个容易被忽略的细节:
- “子数组”指的是连续的区间,不是随便从数组里挑几个数;
- 区间长度至少是 1,也就是说空子数组不算数;
nums里可能有正数、负数,还可能包含 0;- 返回值是满足条件的区间数量,不是返回区间本身。
举个例子,nums = [1, 2, 3, -2, 2],k = 3。把所有和为 3 的连续子数组列出来:
[1, 2],下标 0 到 1;[3],下标 2 到 2;[2, 3, -2],下标 1 到 3;[3, -2, 2],下标 2 到 4。
总共有 4 个,答案就是 4。
很多人会混淆“子数组”和“子序列”,子序列可以不连续,子数组不行。如果题目改成“和为 K 的子序列”,那就是另一类完全不同的题,很可能要走动态规划或背包思路。所以拿到题第一步,先把“连续区间”这个定义钉死。
1.2 为什么负数的出现让很多常见模板失效
滑动窗口通常用于求解“满足某种条件的连续子数组”问题,比如无重复字符的最长子串、长度最小的子数组。这个模板通用成立的前提是:窗口向右扩张时,窗口内的累加和单调不减;窗口左边界向右收缩时,窗口累加和单调不增。如果数组里全是非负数,这个前提成立,双指针就能用。
但是一旦数组里出现负数,窗口内累加和就不是单调的了。我举个最直白的例子:nums = [3, 1, -2, 4],k = 2。按滑动窗口的思维,右指针一路往前走,一旦窗口和超过 2 就收缩左边界,你会发现无论如何都很难覆盖到 [3, 1, -2] 这样的区间,因为它在窗口扩到 -2 时总和才从 4 降到 2,你根本没法预测“前面多加的负数会不会把总和拉回来”。负数让窗口失去了单调性,滑动窗口模板在负数数组面前基本是废的。
这也是面试官喜欢考这题的原因之一:它考察的不是“你背了多少模板”,而是你能不能跳出固定套路,在一个看似可以用双指针的场景里识别出真正的瓶颈,然后自己构造出一个能用哈希表优化的数学模型。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从三层循环到前缀和:解法演进的关键一步
2.1 最直接的三重枚举能不能过
面对区间和问题,最朴素的想法是枚举左右端点:外层循环固定左端点 i,内层循环枚举右端点 j,再写一层循环把 i 到 j 之间的元素全部加起来,判断是否等于 k。
用代码描述大概是这样:
cpp复制int count = 0;
for (int i = 0; i < nums.size(); ++i) {
for (int j = i; j < nums.size(); ++j) {
int sum = 0;
for (int p = i; p <= j; ++p) sum += nums[p];
if (sum == k) ++count;
}
}
三层循环的时间复杂度是 O(n^3)。n 稍微大一点,比如 10^4 级别,直接跑几亿到几万亿次运算,超时是必然的。别笑,我在很多人提交的代码里见过这种写法,多半是刚学数组时留下的思维惯性。
稍微优化一下,可以在枚举右端点 j 时,用一个累加变量 sum 承接当前区间的和,每次 j 往后扩一格就把新元素加进去,省掉最内层循环,变成 O(n^2):
cpp复制int count = 0;
for (int i = 0; i < nums.size(); ++i) {
int sum = 0;
for (int j = i; j < nums.size(); ++j) {
sum += nums[j];
if (sum == k) ++count;
}
}
这个版本可以用,但面试官往往不会满意。只要数组长度到 10^5 以上,O(n^2) 依然会超时。必须找出 O(n) 的办法。
2.2 用前缀和把区间和变成两个数相减
“前缀和”是解决连续区间和问题的一把通用钥匙。定义一个数组 pre,其中 pre[i] 表示 nums[0..i-1] 的前缀和,边界上让 pre[0] = 0,代表空前缀的和。比如 nums = [1, 2, 3, -2, 2],对应的前缀和数组是:
text复制pre[0] = 0
pre[1] = 1
pre[2] = 3
pre[3] = 6
pre[4] = 4
pre[5] = 6
这样从下标 i 到下标 j(假设都从 0 开始)的子数组和,就能直接写成:
text复制nums[i..j] 的和 = pre[j+1] - pre[i]
为什么?因为 pre[j+1] 是 nums[0..j] 的和,pre[i] 是 `nums[0.
