目录

题目描述

643. 子数组最大平均数 I

题意分析

要什么:在数组中找出长度恰好为 k 的连续子数组,使其平均值最大,返回这个最大平均值(浮点数,允许 $10^{-5}$ 的误差)。
约束透露的信号:长度被钉死为 k,所有候选子数组的分母完全相同,于是「平均值最大」和「和最大」是同一个问题——先把除法丢掉,最后再补上。长度固定还意味着候选区间的左右端点是绑定的,右端点一确定左端点就唯一,这正是定长窗口能一遍扫完的根本原因。$n$ 到 $10^5$ 且元素可负,说明要线性算法且不能用「和越加越大」这类只对正数成立的直觉。
边界k 保证不超过 n,所以至少存在一个合法窗口,不必处理无解;元素可为负,最大和可能是负数,答案初值必须取第一个真实窗口的和而不是 0;元素绝对值上限 $10^4$ 且 k 到 $10^5$,窗口和最大约 $10^9$,在 int 边缘,用 64 位更稳妥。

解法:滑动窗口维护有效区间

核心思路

暴力做法是枚举每个起点,再往后累加 k 个元素求和,取最大值,时间 $O(nk)$,在 $n = k = 10^5$ 时是 $10^{10}$ 级,必然超时。
瓶颈很直白:相邻两个窗口 [i, i+k-1][i+1, i+k]k - 1 个元素完全重合,暴力却把这部分反复加了一遍又一遍。
观察到窗口右移一格时,集合的变化只有两处:吐出最左边的 nums[i-k]、吞入新的 nums[i]。既然变化是 $O(1)$ 的,和的更新就该是 $O(1)$ 的。
由此得到要维护的不变量每轮循环开始处理下标 i 时,sum 恰好等于窗口 [i-k, i-1] 的元素和,maxSum 是所有已经完整出现过的定长窗口和的最大值。整个算法就是让这条不变量在窗口右移时保持成立,最后把 maxSum 除以 k 还原成平均值。

解题步骤

  • 先把 nums[0..k-1] 累加成 sum,并令 maxSum = sum为什么答案初值取第一个窗口而不是 0 或极小值:元素可以是负数,最大窗口和完全可能为负,用 0 当初值会返回一个根本不存在的窗口;用第一个真实窗口初始化既安全又省掉一次特判。
  • i = k 遍历到 n - 1,每轮执行 sum += nums[i] - nums[i - k]为什么是 i - k:当前窗口是 [i-k+1, i],上一个窗口是 [i-k, i-1],被挤出去的正是下标 i - k;写成 i - k + 1i - k - 1 都会让窗口长度悄悄变成 k ± 1
  • 每次更新完 sum 后立刻比较并更新 maxSum为什么更新时机在加减之后:只有加减都做完,sum 才代表一个长度恰为 k 的合法窗口;提前比较会把长度为 k + 1 的中间态误当成候选。
  • 返回 maxSum * 1.0 / k为什么要显式转成浮点maxSumk 都是整数,直接相除会做整数除法,把 12.75 截成 12;先乘 1.0(或在 Go 里两边都转 float64)才能得到真实平均值。
  • nums = [1, 12, -5, -6, 50, 3]k = 4 走一遍。初始化阶段累加前四个元素得 sum = 1 + 12 - 5 - 6 = 2maxSum = 2,此刻窗口是 [1, 12, -5, -6]i = 4sum += nums[4] - nums[0] = 50 - 1sum 变 51,窗口滑到 [12, -5, -6, 50],51 大于 2,maxSum = 51i = 5sum += nums[5] - nums[1] = 3 - 12sum 变 42,窗口滑到 [-5, -6, 50, 3],42 不超过 51,maxSum 保持 51。循环结束返回 51 / 4 = 12.75。可以反向验证:窗口 [12, -5, -6, 50] 的和确实是 51,是三个候选窗口(2、51、42)里最大的。

代码实现

// 核心实现:滑动窗口维护有效区间,维护必要状态并避免重复处理。
class Solution {
    public double findMaxAverage(int[] nums, int k) {
        int n = nums.length;
        long sum = 0;
        for (int i = 0; i < k; i++) {
            sum += nums[i];
        }
        long maxSum = sum;

        for (int i = k; i < n; i++) {
            sum += nums[i] - nums[i - k];
            if (sum > maxSum) {
                maxSum = sum;
            }
        }

        return maxSum * 1.0 / k;
    }
}
// 核心实现:滑动窗口维护有效区间,维护必要状态并避免重复处理。
func findMaxAverage(nums []int, k int) float64 {
    sum := 0
    for i := 0; i < k; i++ {
        sum += nums[i]
    }
    maxSum := sum

    for i := k; i < len(nums); i++ {
        sum += nums[i] - nums[i-k]
        if sum > maxSum {
            maxSum = sum
        }
    }

    return float64(maxSum) / float64(k)
}

复杂度分析

  • 时间复杂度:$O(n)$。凭什么:初始化累加 k 次,主循环走 n - k 轮且每轮只做一次加、一次减、一次比较,两段相加恰好是 $O(n)$,每个元素只被加入和移出窗口各一次。
  • 空间复杂度:$O(1)$。凭什么:只用了 summaxSumi 几个标量,没有前缀和数组也没有额外容器,窗口本身用两个下标隐式表示。

关键点总结

  • 定长窗口的本质是增量维护:窗口右移时集合只变化常数个元素,因此任何「可加可减」的统计量(和、元音个数、字符频次)都能 $O(1)$ 更新,把 $O(nk)$ 降到 $O(n)$。
  • 「最大平均值 + 长度固定」要第一时间转化成「最大和」。分母是常数时先约掉,能同时避免精度问题和不必要的浮点运算;一旦长度可变(如 644 题),这个转化就失效,必须换成二分答案。
  • 极值初始化永远从真实存在的候选取,而不是从 0 或凭感觉的常数取。元素含负数时这一条会直接决定对错。
  • 定长窗口和变长窗口是两套模板:定长的右指针每步必移、左指针跟着走,没有 while 收缩;变长的才需要「不满足条件就收缩左边界」。面试里说错模板会被认为没分清题型。
  • 面试视角:这题写完只有五六行,真正的加分项是主动指出「整数除法陷阱」和「窗口和可能超 int」这两个细节,并说明为什么把除法留到最后一步做。

易错点总结

  • 错误写法:maxSum 初始化为 0;用例 nums = [-1, -2, -3]k = 2 → 所有窗口和都是负数,返回 0 对应的 0.0,正确答案是 -1.5
  • 错误写法:返回 maxSum / k;用例 nums = [1, 12, -5, -6, 50, 3]k = 4 → 整数除法把 51 / 4 算成 12,返回 12.0,正确答案 12.75。
  • 错误写法:滑窗时写成 sum += nums[i] - nums[i - k + 1];用例 nums = [1, 2, 3, 4]k = 2i = 2 时减掉的是 nums[1] = 2 之外的错误元素,窗口长度失控,返回值与任何真实窗口都对不上。
  • 错误写法:在减去左端元素之前就更新 maxSum;用例 nums = [1, 2, 100, 1]k = 2 → 中间态 sum 一度代表三个元素之和 103,被误记为最大值,返回 51.5,正确答案 51。
  • 错误写法:主循环从 i = k - 1i = 0 开始;用例 nums = [5, 1, 1]k = 2 → 从 i = 0 起会访问 nums[-2] 直接越界;从 i = k - 1 起会把第一个窗口重复计算一次并减错元素。
  • 错误写法:用 intsum;用例 k = 10^5 且所有元素为 $10^4$ → 窗口和达到 $10^9$,再叠加中间的加减极易越过 int 上限,得到负数结果。
  • 错误写法:每轮重新遍历窗口内的 k 个元素求和;用例 n = k = 10^5 → 结果正确但退化到 $O(nk)$,直接超时,等于没做优化。
  • 错误写法:套用变长窗口模板,写 while (窗口长度 > k) 收缩;用例 任意输入 → 逻辑虽可能正确,但每次进入 while 都要判断,代码变长且极易在只需固定长度时误收缩到 k - 1
  • 错误写法:用 double 累计窗口和以图省事;用例 长度 $10^5$ 的数组 → 浮点累加带来舍入误差,虽然本题误差容限宽松未必挂掉,但比较大小时可能因误差把两个真实相等的窗口判成不等,属于无谓的风险。

相似题目

题目 难度 考察点
1456. 定长子串中元音的最大数目 中等 窗口内统计量从「和」换成「满足条件的字符个数」,增量更新方式相同
1052. 爱生气的书店老板 中等 需要先拆出固定收益,再用定长窗口最大化「额外挽回」的部分
1423. 可获得的最大点数 中等 取两端的问题要反向转化成「最小化中间定长窗口的和」
1151. 最少交换次数来组合所有的 1 中等 窗口长度由数组中 1 的总数动态确定,需要先扫一遍才知道 k