LeetCode 643. 子数组最大平均数 I|从暴力枚举到滑动窗口

目录)

LeetCode 643. 子数组最大平均数 I|从暴力枚举到滑动窗口

题目链接:643. 子数组最大平均数 I

弄了个网站, 可以看代码是怎么操作达到最终要的结果: 过程可视化

一、最开始的思路:枚举每一个长度为 k 的子数组

题目要求找到一个长度为 k 的连续子数组,使它的平均值最大。

我最开始的想法很直接:既然要求最大平均值,那就把所有长度为 k 的连续子数组都找出来,分别计算平均值,再取最大的那个。

于是就这样写出了第一版:

python 复制代码
class Solution:
    def findMaxAverage(self, nums: list[int], k: int) -> float:
        n = len(nums)
        max_mean = -float('inf')
        i = 0

        while i <= n-k:
            current = 0
            j = i

            while j < i+k:
                current += nums[j]
                j += 1

            max_mean = max(current / k, max_mean)
            i += 1

        return max_mean

这里两个 while 分别负责:

  • 外层:枚举每一个合法的子数组起点 i。
  • 内层:计算从 i 开始、长度为 k 的子数组总和。

例如:

text 复制代码
nums = [1, 12, -5, -6, 50, 3]
k = 4

第一个窗口:[1, 12, -5, -6]
第二个窗口:[12, -5, -6, 50]
第三个窗口:[-5, -6, 50, 3]

这个思路没有问题,但每次移动到下一个位置,都需要重新累加 k 个数字。

一共有 n-k+1 个窗口,每个窗口需要 O(k) 的求和时间。

因此时间复杂度是:O((n-k+1) × k),通常记为 O(nk)。

因此,当数组很长、k 也很大时,就容易超出时间限制。。。

那么,有没有办法避免重复计算?


二、发现问题:相邻窗口其实有大量重复内容

观察前两个窗口:

text 复制代码
第一个:[ 1, 12, -5, -6]
第二个:[12, -5, -6, 50]

它们中间的 12、-5、-6 完全一样。

但我之前的代码每次都会:

python 复制代码
current = 0

然后重新计算整个窗口的总和。

既然大部分数字没有变化,为什么不直接利用上一个窗口已经计算出来的结果?

例如:

text 复制代码
第一个窗口总和:1 + 12 - 5 - 6 = 2

向右移动一格以后:

text 复制代码
移出左边的 1
加入右边的 50

新窗口总和 = 2 - 1 + 50 = 51

这样就不需要重新计算中间的三个数字了。

所以关键的变化是:

不再每次重新计算窗口,而是维护一个可以随着窗口移动而更新的总和。

这就引出了 滑动窗口。


三、什么是滑动窗口?

简单来说:滑动窗口(Sliding Window)是一种处理 数组或字符串中连续区间的常见算法思想。

可以把窗口理解成一个框,框住当前需要处理的一段连续元素。

随着窗口向右移动,我们不必每次重新处理框里的所有内容,而是尽量利用之前已经计算过的信息。

(推荐去我的网站看看, 有过程的可视化, 会更好的理解滑动窗口的窗长什么样,怎么运动的~

例如固定长度为 3:

text 复制代码
nums = [1, 2, 3, 4, 5]

[1, 2, 3] 4  5
 1 [2, 3, 4] 5
 1  2 [3, 4, 5]

滑动窗口通常分为两种:

类型 特点 常见问题
定长滑动窗口 窗口长度固定,每次整体移动 长度为 k 的最大总和、平均值
不定长滑动窗口 窗口长度可以变化,根据条件扩张或收缩 满足条件的最短或最长子数组

这道 643 题属于定长滑动窗口。

1. 定长滑动窗口一般怎么做?

通常可以分成四步:

第一步:确定窗口长度。

例如题目要求长度为 k,那窗口始终包含 k 个元素。

第二步:初始化第一个窗口。

先计算前 k 个元素的总和:

python 复制代码
current = sum(nums[:k])

第三步:移动窗口,更新状态。

每次窗口右移一格:

text 复制代码
旧窗口:[a, b, c]
新窗口:[b, c, d]

移出 a
移入 d

所以:

text 复制代码
新窗口和 = 旧窗口和 - 移出元素 + 移入元素

第四步:更新答案。

每移动一次,就根据题目要求更新最大值、最小值或其他统计结果。

需要注意的是,滑动窗口不一定维护总和。

根据题目不同,也可能维护字符出现次数、不同元素的数量等状态。

它的核心是:

窗口移动时,只处理发生变化的部分,尽量复用原来的计算结果。

2. 怎么判断一道题适不适合滑动窗口?

可以先观察三个问题:

  • 题目是不是在研究数组或字符串中的 连续区间?
  • 是不是需要不断考察 相邻的 区间?
  • 当区间移动时,能不能通过 加入、移除元素 来高效更新需要的信息?

如果这些条件都满足,就值得考虑滑动窗口。

但不是所有连续区间题都能直接套用同一种滑动窗口写法,尤其是不定长窗口,还需要考虑窗口收缩是否具有正确性依据。


四、回到这道题:怎么修改原来的代码?

我原来使用 i 表示窗口的起始位置。

所以继续保留这个定义。

例如 k = 4:

text 复制代码
i = 0
窗口下标:[0, 1, 2, 3]

i = 1
窗口下标:[1, 2, 3, 4]

从第一个窗口移动到第二个窗口:

text 复制代码
移出的下标:0 = i - 1
移入的下标:4 = i + k - 1

因此,只需要更新:

python 复制代码
current = current - nums[i-1] + nums[k+i-1]

但第一个窗口比较特殊。

因为它前面没有旧窗口可以复用,所以必须先计算一次:

python 复制代码
if i == 0:
    current = sum(nums[:k])

之后的窗口才使用更新公式。

于是得到第二版代码:

python 复制代码
class Solution:
    def findMaxAverage(self, nums: list[int], k: int) -> float:
        n = len(nums)
        max_mean = -float('inf')
        i = 0

        while i <= n-k:
            if i == 0:
                current = sum(nums[:k])
            else:
                current = current - nums[i-1] + nums[k+i-1]

            max_mean = max(current / k, max_mean)
            i += 1

        return max_mean

这次不再需要内层 while。

因为每次窗口移动,只需要一次减法和一次加法,就能得到新的窗口总和。

为什么循环条件是 i <= n-k?

因为 i 表示窗口起点。

一个长度为 k 的窗口,最右边的下标是:

text 复制代码
i + k - 1

它不能超过数组最后一个下标 n-1。

因此:

text 复制代码
i + k - 1 <= n - 1
i <= n - k

五、还能进一步简化吗?

其实可以。

因为所有窗口的长度都是 k,而且 k 是固定的正数。

所以:总和越大,平均值就越大。

我们没有必要每次都除以 k,可以先找到最大的窗口总和,最后再计算平均值。

另外,第一个窗口也可以在循环外初始化,这样就不需要每次判断 i == 0。

python 复制代码
class Solution:
    def findMaxAverage(self, nums: list[int], k: int) -> float:
        n = len(nums)

        current = sum(nums[:k])
        max_sum = current

        for i in range(1, n-k+1):
            current = current - nums[i-1] + nums[i+k-1]
            max_sum = max(max_sum, current)

        return max_sum / k

这版和我自己写的第二版,本质上是同一种算法,只是把初始化和循环分开,让代码更加简洁。


六、复杂度分析

最开始的暴力枚举:

  • 时间复杂度:O((n-k+1)k),通常简写为 O(nk)。
  • 空间复杂度:O(1)。

优化后的滑动窗口:

  • 时间复杂度:O(n)。初始化需要 O(k),之后每次窗口移动只需要 O(1)。
  • 空间复杂度:O(1)。只需要维护当前窗口总和和最大值等变量。

七、最后复盘

这道题的思考过程其实很简单:

text 复制代码
题目要求长度为 k 的最大平均值
↓
先枚举所有长度为 k 的连续子数组
↓
每次重新计算窗口总和
↓
发现相邻窗口有大量重复元素
↓
既然大部分元素没变,能不能复用旧结果?
↓
窗口右移时,只移出一个、加入一个
↓
维护窗口总和
↓
从 O(nk) 优化到 O(n)

可以通过这道题去理解定长滑动窗口的作用:

滑动窗口不是简单地把双层循环改成单层循环,而是通过维护窗口状态,避免对相邻区间进行重复计算。

以后遇到类似题目,可以先问:

当这个连续区间向右移动时,究竟哪些元素发生了变化?之前的计算结果能不能直接利用?

如果能找到高效更新状态的方法,就有机会使用滑动窗口优化。

相关推荐
文丑颜不良啊39 分钟前
LeetCode1461~1480题目解析
java·leetcode
深蓝AI3 小时前
10 万星投给了记忆:claude-mem 把编码 Agent 的断片压进 SQLite,值不值得装
人工智能·程序员
limuyang27 小时前
我在 2核2G 云主机上跑 OpenViking:一次关于云主机、模型选型与长期成本的真实复盘
人工智能·开源·ai编程
第九序言T8 小时前
Python 数据处理进阶(十八)批量处理同结构文件:一个文件夹一次跑完
开发语言·python·自动化·pandas·数据处理
Shulex8 小时前
跨境电商 AI Agent 核心架构解析:从只读问答到自主执行的工程落地
人工智能·架构·智能客服·跨境电商
Java后端的Ai之路8 小时前
CAS并发控制-从CPU指令到企业级乐观锁
vue.js·人工智能·python·cas·乐观锁
qq_426003969 小时前
UI自动化高频次登录login限流解决方案
python·ui·自动化
情 九9 小时前
MCP 实战 06:把 HTTP 接口接成工具,让 AI 帮你查数据
人工智能·网络协议·http·mcp
Sakura_j_h_9 小时前
EasyEDA PCB 快速设计Skills
c++·python·github