其实我先做了这一题,很简单:
643. 子数组最大平均数 I - 力扣(LeetCode)
1 题目
给你一个整数数组 arr 和两个整数 k 和 threshold 。
请你返回长度为 k 且平均值大于等于 threshold 的子数组数目。
示例 1:
输入:arr = [2,2,2,2,5,5,5,8], k = 3, threshold = 4
输出:3
解释:子数组 [2,5,5],[5,5,5] 和 [5,5,8] 的平均值分别为 4,5 和 6 。其他长度为 3 的子数组的平均值都小于 4 (threshold 的值)。
示例 2:
输入:arr = [11,13,17,23,29,31,7,5,2,3], k = 3, threshold = 5
输出:6
解释:前 6 个长度为 3 的子数组平均值都大于 5 。注意平均值不是整数。
提示:
1 <= arr.length <= 1051 <= arr[i] <= 1041 <= k <= arr.length0 <= threshold <= 104
2 代码实现
cpp
class Solution {
public:
int numOfSubarrays(vector<int>& arr, int k, int threshold) {
int n = arr.size();
int currentSum = 0 ;
int count = 0 ;
for (int i = 0 ; i < k ; i ++ ){
currentSum += arr[i];
}
if( (currentSum / k) >= threshold ){
count++;
}
for (int i = k ; i < n ; i++){
currentSum = currentSum - arr[i - k] + arr [i];
if( (currentSum / k) >= threshold ){
count++;
}
}
return count;
}
};
错误的代码:
cpp
class Solution {
public:
int numOfSubarrays(vector<int>& arr, int k, int threshold) {
int n = arr.size();
int currentSum = 0 ;
int count = 0 ;
for (int i = 0 ; i < k ; i ++ ){
currentSum += arr[i];
if( (currentSum / k) >= threshold ){
count++;
}
}
for (int i = k ; i < n ; i++){
currentSum = currentSum - arr[i - k] + arr [i];
if( (currentSum / k) >= threshold ){
count++;
}
}
return count;
}
};
纠正:
你的代码错误核心是 第一个循环的逻辑判断时机不对,导致在计算 "第一个完整窗口" 的和之前,就提前做了平均值判断 ------ 相当于统计了 "长度小于 k 的子数组",和题目要求的 "长度必须为 k 的连续子数组" 不符。
一、先看错误出在哪:第一个循环的问题
题目要求子数组 长度必须是 k ,但你的第一个循环(for (int i = 0; i < k; i++))里,每累加一个元素就判断一次 currentSum / k >= threshold,这是错误的:
- 当
i=0时:只累加了 1 个元素(长度 = 1),就用currentSum/3(假设 k=3)判断,显然不符合 "长度为 k" 的要求; - 当
i=1时:累加了 2 个元素(长度 = 2),还是用currentSum/3判断,依然不符合; - 只有当
i=k-1时(循环最后一次),才累加完 k 个元素(第一个完整窗口),此时判断才是正确的。
简单说:第一个循环里,你多判断了 k-1 次 "无效子数组"(长度不足 k),导致计数偏多,最终答案错误。
二、举个例子直观感受错误
比如用示例 1 测试你的代码:arr = [2,2,2,2,5,5,5,8], k=3, threshold=4第一个循环(i 从 0 到 2):
- i=0:currentSum=2 → 2/3=0 ≥4?否(count=0);
- i=1:currentSum=4 →4/3≈1 ≥4?否(count=0);
- i=2:currentSum=6 →6/3=2 ≥4?否(count=0);这一步还好,没多计数。但换个测试用例就会出错:比如
arr=[5,5,5], k=3, threshold=5: - i=0:currentSum=5 →5/3≈1 ≥5?否;
- i=1:currentSum=10 →10/3≈3 ≥5?否;
- i=2:currentSum=15 →15/3=5 ≥5?是(count=1);这一步结果正确,但如果是
arr=[6,6,6], k=3, threshold=2: - i=0:currentSum=6 →6/3=2 ≥2?是(count=1)→ 错误!此时子数组长度是 1,不是 3;
- i=1:currentSum=12 →12/3=4 ≥2?是(count=2)→ 错误!子数组长度是 2;
- i=2:currentSum=18 →18/3=6 ≥2?是(count=3)→ 正确;最终你的代码返回 3,但实际满足条件的只有 1 个(长度为 3 的子数组),明显错误。
三、还有一个隐藏风险:整数除法的精度问题
虽然你的代码里 currentSum / k 看似没问题,但其实是 "巧合"------ 因为题目要求 "平均值 ≥ threshold",等价于 currentSum ≥ k × threshold(这是更严谨的转化)。
比如 k=3, threshold=4 → 等价于 currentSum ≥12。如果用 currentSum / k ≥4,当 currentSum=11 时,11/3=3(整数除法截断),判断正确;但如果 threshold=4.3(假设题目是浮点数阈值),整数除法就会出错。不过这道题阈值是整数,所以这个问题没暴露,但第一个循环的判断时机错误是核心 bug。
四、修正后的代码(只改 1 处)
把第一个循环里的判断移到循环外面 ------ 等累加完 k 个元素(第一个完整窗口)后,再判断一次即可:
cpp
运行
cpp
class Solution {
public:
int numOfSubarrays(vector<int>& arr, int k, int threshold) {
int n = arr.size();
int currentSum = 0 ;
int count = 0 ;
// 第一步:只累加第一个窗口的和,不判断
for (int i = 0 ; i < k ; i ++ ){
currentSum += arr[i];
}
// 累加完 k 个元素后,再判断第一个窗口(长度=k)
if( (currentSum / k) >= threshold ){
count++;
}
// 第二步:滑动窗口,后续每个窗口都是长度=k,直接判断
for (int i = k ; i < n ; i++){
currentSum = currentSum - arr[i - k] + arr[i];
if( (currentSum / k) >= threshold ){
count++;
}
}
return count;
}
};
五、进一步优化(避免整数除法,更严谨)
如之前的思路,把 currentSum / k >= threshold 转化为 currentSum >= k * threshold,完全避免整数除法的潜在问题(虽然这道题用除法也能过,但养成习惯更好):
cpp
class Solution {
public:
int numOfSubarrays(vector<int>& arr, int k, int threshold) {
int n = arr.size();
int target = k * threshold; // 转化为整数判断,无精度问题
int currentSum = 0 ;
int count = 0 ;
for (int i = 0 ; i < k ; i ++ ){
currentSum += arr[i];
}
if(currentSum >= target){ // 直接用和判断,更高效
count++;
}
for (int i = k ; i < n ; i++){
currentSum = currentSum - arr[i - k] + arr[i];
if(currentSum >= target){
count++;
}
}
return count;
}
};
总结
你的核心错误是 第一个循环中 "提前判断"------ 把 "累加过程" 和 "完整窗口判断" 混在一起,导致统计了长度不足 k 的子数组。修正后,只需要在第一个窗口累加完成后判断一次,后续滑动窗口的判断都是正确的(因为滑动后窗口长度始终是 k)。
这道题和 643 题的滑动窗口逻辑一致,重点是要明确:窗口必须是 "完整长度 k" 后,才能做判断!!!!
总结框架
针对 LeetCode 中 "长度固定为 k 的连续子数组" 这类问题(比如 643 题 "最大平均数"、1343 题 "满足阈值的子数组数"),可以提炼出一个 通用滑动窗口框架------ 核心是 "先初始化窗口、再滑动更新窗口、全程处理窗口条件",所有这类题都能套这个模板解决,无需重复思考逻辑。
下面用清晰的步骤 + 代码框架 + 应用示例,帮你彻底掌握:
一、通用框架核心思想
固定窗口长度 k,避免重复计算子数组和:
- 初始化窗口 :计算前
k个元素的和(第一个完整窗口); - 处理第一个窗口:根据题目要求判断第一个窗口是否满足条件(比如求和、计数、找最大);
- 滑动更新窗口 :从第
k个元素开始,每次窗口右移时,减去左侧离开的元素,加上右侧进入的元素(O (1) 时间更新窗口和); - 处理后续窗口:每次更新后,再次判断当前窗口是否满足条件,更新结果;
- 返回最终结果:根据题目要求返回统计值、最大值等。
二、通用代码框架(直接复制修改)
cpp
class Solution {
public:
// 函数返回值根据题目调整(比如 int、double)
返回类型 题目函数名(vector<int>& arr, int k, 其他参数) {
int n = arr.size();
int currentSum = 0; // 核心:当前窗口的和(固定长度 k)
结果变量; // 比如 count(计数)、maxSum(找最大)等,根据题目初始化
// 步骤1:初始化第一个窗口(前 k 个元素的和)
for (int i = 0; i < k; ++i) {
currentSum += arr[i];
}
// 步骤2:处理第一个窗口(根据题目要求判断)
处理逻辑; // 比如:计数+1、更新maxSum等
// 步骤3:滑动窗口(遍历剩余元素,更新窗口并处理)
for (int i = k; i < n; ++i) {
// 窗口右移:去掉左侧离开的元素(arr[i - k]),加入右侧新元素(arr[i])
currentSum = currentSum - arr[i - k] + arr[i];
// 步骤4:处理当前窗口(和步骤2的处理逻辑一致)
处理逻辑; // 重复步骤2的判断,更新结果变量
}
// 步骤5:返回最终结果(可能需要转换类型,比如 sum/k 转 double)
return 最终结果;
}
};
三、框架应用示例(对应两道题,看如何套模板)
示例 1:LeetCode 643 题(子数组最大平均数 I)
- 题目要求:找长度为 k 的子数组的最大平均数(等价于找最大和,再除以 k)
- 套用框架:
- 结果变量:
int maxSum = currentSum(记录最大窗口和) - 处理逻辑:每次窗口和与
maxSum比较,更新最大值 - 最终结果:
static_cast<double>(maxSum) / k(转 double 避免整数除法)
- 结果变量:
cpp
class Solution {
public:
double findMaxAverage(vector<int>& nums, int k) {
int n = nums.size();
int currentSum = 0;
// 结果变量:初始化最大和为第一个窗口和
int maxSum = 0;
// 步骤1:初始化第一个窗口
for (int i = 0; i < k; ++i) {
currentSum += nums[i];
}
// 步骤2:处理第一个窗口
maxSum = currentSum;
// 步骤3:滑动窗口
for (int i = k; i < n; ++i) {
currentSum = currentSum - nums[i - k] + nums[i];
// 步骤4:处理当前窗口(更新最大和)
maxSum = max(maxSum, currentSum);
}
// 步骤5:返回结果(转 double 计算平均数)
return static_cast<double>(maxSum) / k;
}
};
示例 2:LeetCode 1343 题(满足阈值的子数组数)
- 题目要求:统计长度为 k 的子数组中,平均值 ≥ threshold 的个数(等价于和 ≥ k×threshold)
- 套用框架:
- 结果变量:
int count = 0(统计满足条件的个数) - 处理逻辑:判断窗口和是否 ≥ 目标值(k×threshold),满足则 count++
- 最终结果:直接返回 count
- 结果变量:
cpp
class Solution {
public:
int numOfSubarrays(vector<int>& arr, int k, int threshold) {
int n = arr.size();
int currentSum = 0;
int target = k * threshold; // 转化为整数判断,避免浮点数
// 结果变量:计数初始为 0
int count = 0;
// 步骤1:初始化第一个窗口
for (int i = 0; i < k; ++i) {
currentSum += arr[i];
}
// 步骤2:处理第一个窗口
if (currentSum >= target) count++;
// 步骤3:滑动窗口
for (int i = k; i < n; ++i) {
currentSum = currentSum - arr[i - k] + arr[i];
// 步骤4:处理当前窗口(满足条件则计数+1)
if (currentSum >= target) count++;
}
// 步骤5:返回结果
return count;
}
};
四、框架关键注意点(避免踩坑)
- 窗口初始化后再处理 :第一个窗口必须累加完
k个元素后,再做判断(避免像你之前的错误那样,提前判断长度不足 k 的子数组); - 窗口更新逻辑固定 :
currentSum = currentSum - arr[i - k] + arr[i]是核心,不管题目怎么变,滑动时都这么写(i - k是左侧离开的元素索引); - 结果处理灵活调整 :
- 找最大 / 最小值:结果变量初始化为第一个窗口的值,后续更新时比较替换;
- 计数:结果变量初始化为 0,满足条件则累加;
- 需浮点数:最后一步转换类型(比如 643 题的
static_cast<double>);
- 避免冗余计算:用 "窗口和" 替代每次遍历 k 个元素求和,时间复杂度从 O (nk) 降到 O (n),效率翻倍。
五、总结
这类 "固定长度 k 的子数组" 问题,本质都是 "窗口和的维护 + 条件判断",通用框架的核心价值是:
- 无需重复设计滑动逻辑,直接套模板;
- 重点只需要关注 "处理逻辑"(步骤 2 和步骤 4),根据题目要求调整即可;
- 避免踩 "提前判断""整数除法" 等常见坑。
后续遇到类似题目(比如 "长度为 k 的子数组最小和""长度为 k 的子数组中偶数的个数"),都可以直接用这个框架,只改 "结果变量" 和 "处理逻辑" 两部分。