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)

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

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

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

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

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

相关推荐
天远API1 小时前
零信任架构实战:基于天远风控经营异常预警构建自动化供应商店铺巡检网关
人工智能·架构·自动化·dubbo
小李不困还能学1 小时前
PyCharm远程开发连接失败排查与解决
ide·python·pycharm
pride.li1 小时前
Python 安装
linux·python·ubuntu
田里的水稻1 小时前
EP_ROS框架---相机视频流交互二
人工智能·深度学习·数码相机·机器学习·迁移学习
AI行业应用研究1 小时前
会务信息载体的演进:纸质、PDF 与结构化数据源的分发成本与版本一致性
大数据·人工智能·安全·小程序·架构·pdf
罗西的思考1 小时前
[Agent Memory / 强化学习] MemPO源码学习笔记 — (6)— 奖励机制
人工智能·笔记·学习
全栈练习生1 小时前
模型上下文协议(MCP)
python·ai
Ai小禾2 小时前
【Obsidian对话数据怎么导入到另外一个Obsidian账号里去? 】用“AI导出鸭”把AI对话变成可迁移的知识资产
人工智能
极光通讯2 小时前
服务器内存来料检验(IQC)实操:批次追溯、外观判定与上机验证
java·服务器·算法