1. 问题背景与核心思路
这道题目来自力扣第1004题,题目描述如下:给定一个由0和1组成的数组nums和一个整数k,我们可以将最多k个0翻转为1,返回数组中仅包含1的最长子数组的长度。
我第一次看到这个问题时,脑海中立即浮现出滑动窗口的概念。滑动窗口算法特别适合处理这种需要寻找满足特定条件的连续子数组的问题。它的核心思想是维护一个动态变化的窗口,通过调整窗口的边界来寻找最优解。
提示:滑动窗口算法的时间复杂度通常是O(n),这比暴力解法要高效得多。对于这类问题,滑动窗口往往是首选方案。
2. 算法详细解析
2.1 滑动窗口的基本原理
滑动窗口算法通过维护两个指针(左指针left和右指针right)来定义一个窗口。窗口内的元素就是我们当前考察的子数组。算法的基本流程是:
- 右指针不断向右移动,扩展窗口
- 当窗口内的条件不满足时(这里指0的数量超过k),左指针向右移动,收缩窗口
- 在每次满足条件时,记录当前窗口的大小
这种"扩张-收缩"的策略确保了我们可以高效地找到最优解,而无需检查所有可能的子数组。
2.2 具体实现步骤
让我们更详细地分解代码中的实现逻辑:
- 初始化左右指针left和right都指向数组起始位置
- 初始化计数器zeroNum记录当前窗口中的0的数量
- 初始化maxLength记录最大窗口长度
- 右指针right遍历整个数组:
- 如果当前元素是0,zeroNum加1
- 当zeroNum超过k时,移动左指针left直到zeroNum不大于k
- 计算当前窗口长度(right-left+1),并更新maxLength
- 返回maxLength
这个过程中,最关键的部分是理解如何维护窗口内的0的数量不超过k。当超过时,我们需要收缩窗口的左边界,直到条件重新满足。
3. 代码实现详解
java复制public int longestOnes(int[] nums, int k) {
int left = 0, right = 0; // 初始化左右指针
int maxLength = 0; // 记录最大长度
int zeroNum = 0; // 记录窗口内0的数量
while (right < nums.length) {
// 更新0的数量
if (nums[right] == 0) {
zeroNum++;
}
// 当0的数量超过k时,收缩窗口
while (zeroNum > k) {
if (nums[left] == 0) {
zeroNum--;
}
left++;
}
// 更新最大长度
maxLength = Math.max(maxLength, right - left + 1);
right++;
}
return maxLength;
}
3.1 代码关键点解析
-
指针移动逻辑:右指针right每次循环都会向右移动一位,而左指针left只有在窗口内0的数量超过k时才会移动。
-
zeroNum的维护:当右指针遇到0时增加zeroNum,当左指针遇到0时减少zeroNum。这确保了zeroNum始终准确反映当前窗口内的0的数量。
-
窗口长度计算:窗口长度计算公式是right-left+1,因为数组索引是从0开始的。
-
maxLength更新时机:每次右指针移动后都会计算当前窗口长度并更新maxLength,确保不会错过任何可能的解。
4. 复杂度分析
4.1 时间复杂度
虽然代码中有嵌套的while循环,但每个元素最多被左指针和右指针各访问一次。因此,总的时间复杂度是O(n),其中n是数组的长度。
4.2 空间复杂度
算法只使用了固定数量的额外空间(几个整型变量),因此空间复杂度是O(1)。
5. 实例详细推演
让我们用题目中的示例来详细推演算法的执行过程:
nums = [1,1,1,0,0,0,1,1,1,1,0], k = 2
初始化:left=0, right=0, maxLength=0, zeroNum=0
- right=0: nums[0]=1
- zeroNum=0
- maxLength=max(0,0-0+1)=1
- right=1: nums[1]=1
- zeroNum=0
- maxLength=max(1,1-0+1)=2
- right=2: nums[2]=1
- zeroNum=0
- maxLength=max(2,2-0+1)=3
- right=3: nums[3]=0
- zeroNum=1
- maxLength=max(3,3-0+1)=4
- right=4: nums[4]=0
- zeroNum=2
- maxLength=max(4,4-0+1)=5
- right=5: nums[5]=0
- zeroNum=3 > k=2
- 进入内层while循环:
- left=0: nums[0]=1 → left=1
- left=1: nums[1]=1 → left=2
- left=2: nums[2]=1 → left=3
- left=3: nums[3]=0 → zeroNum=2, left=4
- maxLength=max(5,5-4+1)=5
- right=6: nums[6]=1
- zeroNum=2
- maxLength=max(5,6-4+1)=5
- right=7: nums[7]=1
- zeroNum=2
- maxLength=max(5,7-4+1)=5
- right=8: nums[8]=1
- zeroNum=2
- maxLength=max(5,8-4+1)=5
- right=9: nums[9]=1
- zeroNum=2
- maxLength=max(5,9-4+1)=6
- right=10: nums[10]=0
- zeroNum=3 > k=2
- 进入内层while循环:
- left=4: nums[4]=0 → zeroNum=2, left=5
- maxLength=max(6,10-5+1)=6
最终返回maxLength=6
6. 常见问题与优化思考
6.1 为什么使用滑动窗口?
滑动窗口算法特别适合解决这类需要寻找满足特定条件的连续子数组的问题。相比于暴力解法(检查所有可能的子数组),滑动窗口将时间复杂度从O(n²)降低到了O(n)。
6.2 如何处理k=0的情况?
当k=0时,问题退化为寻找最长的全1子数组。我们的算法仍然适用,因为当遇到0时,窗口会立即收缩到0的右边。
6.3 是否可以优化内层while循环?
在某些情况下,我们可以记录0的位置,这样当需要收缩窗口时,可以直接跳到下一个0的位置,而不是逐个移动左指针。不过这种优化在大多数情况下带来的提升有限,且增加了代码复杂度。
6.4 边界条件考虑
需要特别注意以下几种边界情况:
- 数组全为1的情况
- 数组全为0且k=0的情况
- k大于等于数组中0的总数的情况
- 空数组的情况
我们的算法已经正确处理了这些边界条件。
7. 算法扩展与变种
7.1 类似题目
掌握了这个算法后,可以尝试解决以下类似题目:
- 无重复字符的最长子串
- 最小覆盖子串
- 长度最小的子数组
- 替换后的最长重复字符
7.2 变种思考
如果题目改为可以翻转最多k个1为0,寻找最长全0子数组,算法应该如何调整?
实际上,只需要将代码中判断0的条件改为判断1即可,算法框架完全不变。这体现了滑动窗口算法的通用性。
8. 实际应用场景
这种算法在实际中有很多应用,例如:
- 网络流量分析:寻找满足特定条件的连续数据包序列
- 基因组分析:寻找满足特定碱基组成的DNA片段
- 质量控制:在生产线数据中寻找连续合格的产品序列
- 用户行为分析:寻找用户连续活跃的天数序列
9. 个人实现心得
在实际编码实现时,有几点经验值得分享:
-
指针移动顺序:一定要先处理右指针的移动和条件判断,再计算窗口长度。顺序错误会导致逻辑错误。
-
边界条件测试:务必测试k=0、k大于等于总0数、全1数组等边界情况,确保代码鲁棒性。
-
变量命名:使用有意义的变量名(如zeroNum而不是简单的count)可以提高代码可读性。
-
调试技巧:可以在循环中加入打印语句,输出每次循环的指针位置和关键变量值,帮助理解算法执行过程。
-
性能考量:虽然时间复杂度已经是O(n),但在实际应用中,如果数组特别大,还可以考虑并行化处理或其他优化手段。