学习完二叉树的大部分基本功后,接下来我们来学习一种非常重要且应用广泛的算法思想—— 滑动窗口 (Sliding Window)。我们将通过一道经典的入门题 LeetCode 643 : 子数组最大平均数 I 【难度:简单;通过率:44.1%】来开始。这道题是“定长滑动窗口”的完美示例
一、 题目描述
给你一个由 n
个元素组成的整数数组 nums
和一个整数 k
请你找出平均数最大且 长度为 k
的连续子数组,并输出该最大平均数。任何误差小于 10^-5
的答案都将被视为正确答案
示例:
输入: nums = [1,12,-5,-6,50,3], k = 4
输出: 12.75
解释: 最大平均数 (12 - 5 - 6 + 50) / 4 = 51 / 4 = 12.75
输入: nums = [5], k = 1
输出: 5.00000
二、 从暴力解法到滑动窗口
1. 暴力解法
最直观的想法是什么?就是找出所有长度为 k
的连续子数组,分别计算它们的和以及平均数,然后找到其中的最大值
// 暴力解法:为了对比,不推荐
public double findMaxAverage_bruteForce(int[] nums, int k) {
double maxAvg = -Double.MAX_VALUE;
// 遍历所有可能的子数组起点
for (int i = 0; i <= nums.length - k; i++) {
long sum = 0;
// 对每个长度为 k 的子数组求和
for (int j = i; j < i + k; j++) {
sum += nums[j];
}
maxAvg = Math.max(maxAvg, (double) sum / k);
}
return maxAvg;
}
效率分析:
- 外层循环
i
遍历了约N
次 - 内层循环
j
每次都执行k
次 - 时间复杂度:O(N * k)。当
k
接近N
时,复杂度接近 O(N²),效率很低
问题在哪?
我们做了大量的重复计算!在计算 [1, 12, -5, -6]
的和之后,为了计算下一个子数组 [12, -5, -6, 50]
的和,我们又重新加了一遍 12, -5, -6
2. 滑动窗口
滑动窗口思想的核心就是避免重复计算。我们可以想象有一个长度固定为 k
的“窗口”在数组上滑动
- 当窗口向右滑动一格时,我们不需要重新计算窗口内所有元素的和
- 我们只需要:
- 加上新进入窗口的元素
- 减去刚离开窗口的元素
这样,每次移动窗口,我们都只需要进行一次加法和一次减法,时间复杂度是 O(1)
三、 滑动窗口的实现步骤
- 初始化第一个窗口:计算数组前
k
个元素的和,得到第一个窗口的和windowSum
。同时,用windowSum
初始化maxSum
- 滑动窗口:从第
k
个元素开始遍历数组(索引从k
到n-1
)- 对于每个新元素
nums[i]
,它将进入窗口 - 同时,元素
nums[i-k]
将离开窗口 - 更新窗口的和:
windowSum = windowSum + nums[i] - nums[i-k]
- 每次更新后,都用新的
windowSum
与maxSum
比较,并保留较大者
- 对于每个新元素
- 计算结果:遍历结束后,
maxSum
就是所有长度为k
的子数组中的最大和。将其除以k
,即可得到最大平均数
四、 代码实现 (最佳实践)
class Solution {
public double findMaxAverage(int[] nums, int k) {
// 1. 初始化第一个窗口的和
long windowSum = 0;
for (int i = 0; i < k; i++) {
windowSum += nums[i];
}
// 用第一个窗口的和来初始化 maxSum
long maxSum = windowSum;
// 2. 滑动窗口
// 从第 k 个元素开始,作为新进入窗口的元素
for (int i = k; i < nums.length; i++) {
// 更新窗口的和:加上新元素,减去旧元素
// 新元素是 nums[i],旧元素是 nums[i-k]
windowSum = windowSum + nums[i] - nums[i-k];
// 更新 maxSum
maxSum = Math.max(maxSum, windowSum);
}
// 3. 计算并返回最大平均数
return (double) maxSum / k;
}
}
五、 关键点与复杂度分析
- 定长窗口:本题的窗口长度
k
是固定的,这是最简单的滑动窗口模型(定长滑动窗口,后面我们会继续进阶高难度的滑动窗口) - 双指针思想:滑动窗口的本质是“双指针”思想的一种应用。我们可以想象一个左指针
left
和一个右指针right
维护着这个窗口。当右指针向右移动时,左指针也以相同的速度向右移动,从而保持窗口大小不变 - 数据类型:在计算和时,使用
long
类型的windowSum
和maxSum
是一个好习惯,可以防止当数组元素和k
都很大时可能发生的整数溢出 - 时间复杂度:O(N) 我们只需要对数组进行一次完整的遍历
- 空间复杂度:O(1) 我们只使用了几个额外的变量来存储和,没有使用与输入规模相关的额外空间