1. 题目解析与核心思路
LeetCode 1004题"最大连续1的个数 III"是一个典型的滑动窗口应用场景。题目给定一个由0和1组成的数组nums,和一个整数k。我们可以将最多k个0翻转为1,要求找出翻转后最长的连续1的子数组长度。
这个问题的核心在于理解滑动窗口的适用条件。当遇到数组中的0时,我们需要记录当前窗口内0的数量。如果0的数量超过k,就需要调整窗口的左边界,始终保持窗口内0的数量不超过k。这种动态调整窗口大小的策略,正是滑动窗口算法的精髓所在。
关键观察点:窗口内0的数量与k的关系决定了窗口的扩张与收缩。当0的数量≤k时,窗口可以继续向右扩张;当0的数量>k时,必须收缩左边界。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 滑动窗口算法详解
2.1 算法基本框架
滑动窗口算法通常包含以下几个关键步骤:
- 初始化左右指针(left和right)和必要的计数器
- 右指针不断向右移动,扩展窗口
- 在每次右移后,检查是否满足收缩条件
- 如果满足收缩条件,移动左指针直到不再满足收缩条件
- 在每次调整窗口后,更新结果
对于本题,我们可以这样实现:
java复制class Solution {
public int longestOnes(int[] nums, int k) {
int left = 0, zeroCount = 0, maxLen = 0;
for (int right = 0; right < nums.length; right++) {
if (nums[right] == 0) {
zeroCount++;
}
while (zeroCount > k) {
if (nums[left] == 0) {
zeroCount--;
}
left++;
}
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
}
2.2 时间复杂度分析
这个算法的时间复杂度是O(n),其中n是数组的长度。虽然内部有一个while循环,但每个元素最多被左指针和右指针各访问一次,所以总体时间复杂度仍然是线性的。
空间复杂度是O(1),因为我们只使用了固定数量的额外空间(几个整型变量)。
3. 代码实现细节解析
3.1 变量初始化
java复制int left = 0, zeroCount = 0, maxLen = 0;
left:窗口左边界,初始为0zeroCount:当前窗口内0的计数,初始为0maxLen:记录最大窗口长度,初始为0
3.2 窗口扩展逻辑
java复制for (int right = 0; right < nums.length; right++) {
if (nums[right] == 0) {
zeroCount++;
}
// ...
}
右指针right从0开始遍历数组,遇到0时增加zeroCount。这个循环负责窗口的向右扩展。
3.3 窗口收缩条件
java复制while (zeroCount > k) {
if (nums[left] == 0) {
zeroCount--;
}
left++;
}
当窗口内0的数量超过k时,需要收缩左边界。左指针left向右移动,直到窗口内0的数量不超过k。注意只有当左指针经过0时才减少zeroCount。
3.4 结果更新
java复制maxLen = Math.max(maxLen, right - left + 1);
在每次调整窗口后,计算当前窗口长度(right - left + 1)并更新最大长度。
4. 算法优化与变种
4.1 早期终止优化
在某些情况下,可以提前终止循环。例如,当剩余未处理的元素数量加上当前最大长度不超过数组长度时,可以提前退出循环。
4.2 类似问题变种
这个算法框架可以应用于许多类似问题,例如:
- 最长不重复字符子串
- 包含最多两个不同字符的最长子串
- 和至少为K的最短子数组
这些问题的共同特点是都需要在一个序列中寻找满足特定条件的最长子序列或最短子序列。
5. 常见错误与调试技巧
5.1 边界条件处理
常见的边界条件包括:
- 空数组输入
- k=0的情况
- 全1数组
- 全0数组
确保你的代码能够正确处理这些特殊情况。
5.2 调试技巧
在调试滑动窗口问题时,可以:
- 打印窗口的左右边界和关键变量值
- 手动模拟小规模输入的执行过程
- 检查窗口收缩条件的逻辑是否正确
例如,可以添加调试输出:
java复制System.out.println("left=" + left + ", right=" + right +
", zeroCount=" + zeroCount + ", maxLen=" + maxLen);
6. 实际应用场景
滑动窗口算法在实际中有广泛的应用,例如:
- 网络流量控制中的滑动窗口协议
- 数据分析中的移动平均计算
- 文本编辑器的查找替换功能
- 视频流处理中的缓冲区管理
理解这个算法不仅有助于解决LeetCode问题,也能为实际工程问题提供解决思路。
7. 性能对比测试
为了验证滑动窗口算法的效率,我们可以对比暴力解法:
java复制// 暴力解法 O(n^2)
public int longestOnesBruteForce(int[] nums, int k) {
int maxLen = 0;
for (int i = 0; i < nums.length; i++) {
int zeros = 0;
for (int j = i; j < nums.length; j++) {
if (nums[j] == 0) zeros++;
if (zeros > k) break;
maxLen = Math.max(maxLen, j - i + 1);
}
}
return maxLen;
}
测试表明,对于大数组(如长度10^5),滑动窗口解法可以在几毫秒内完成,而暴力解法可能需要数秒甚至更长时间。
8. 扩展思考
8.1 如果允许将1翻转为0呢?
如果题目改为可以翻转最多k个1或0,求最长的连续0或1的子数组长度,该如何修改算法?
8.2 二维数组扩展
如果问题扩展到二维矩阵,寻找最大的全1子矩阵,且允许翻转最多k个0,该如何设计算法?
8.3 动态k值问题
如果k不是固定值,而是随着位置变化的函数k(i),该如何调整算法?
这些扩展问题可以帮助深化对滑动窗口算法的理解,并提升解决复杂问题的能力。
在实际编码面试中,我经常遇到候选人能够写出滑动窗口的基本框架,但在边界条件处理和问题变种上容易出错。建议在掌握基础算法后,多练习各种变种问题,培养灵活应用的能力。
