梦想开始的地方——滑动窗口模板
前言
之所以说这是梦想开始的地方,是因为这是我一个月前在 LeetCode 上刷的第一道算法题。
这是 LeetCode 上 2 月 19 日的每日一题,当时对算法的技巧一窍不通的我点开了他,思考了五分钟,发现似乎无法用暴力方法解决它。
当我点开解答时,惊呆了,怎么会有这么巧妙的方法,而且还给出了同类题目的解体模板,我简直是小刀划屁屁——开了眼了。
由于前几天一直忙于教研室的工作和博客网页的优化,所以没有刷题,现在忙得也差不多了,可以重新开始 LeetCode 之旅了,就想再感受一次当初的那份惊艳。
原题
给定一个由若干 0
和 1
组成的数组 A
,我们最多可以将 K
个值从 0
变成 1
。
返回仅包含 1
的最长(连续)子数组的长度。
示例 1:
1 | 输入:A = [1,1,1,0,0,0,1,1,1,1,0], K = 2 |
示例 2:
1 | 输入:A = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3 |
解题思路
题意转换。把「最多可以把 K 个 0 变成 1,求仅包含 1 的最长子数组的长度」转换为 「找出一个最长的子数组,该子数组内最多允许有 K 个 0 」。
这一步的题意转换让我想起了高中数学老师讲圆锥曲线题目的解法,难题往往难在对题目描述的转化上,只要把未知问题转化为已知问题,难题也就迎刃而解了。
经过上面的题意转换,我们可知本题是求最大连续子区间,可以使用滑动窗口方法。滑动窗口的限制条件是:窗口内最多有 K 个 0。
代码思路:
- 使用
left
和right
两个指针,分别指向滑动窗口的左右边界。 right
主动右移:right
指针每次移动一步。当A[right]
为 0,说明滑动窗口内增加了一个 0;left
被动右移:判断此时窗口内 0 的个数,如果超过了 K,则left
指针被迫右移,直至窗口内的 0 的个数小于等于 K 为止。- 滑动窗口长度的最大值就是所求。
示例
以 A= [1,1,1,0,0,0,1,1,1,1,0], K = 2
为例,下面的动图演示了滑动窗口的两个指针的移动情况。
代码
1 | class Solution { |
滑动窗口模板
《挑战程序设计竞赛》这本书中把滑动窗口叫做「虫取法」,我觉得非常生动形象。因为滑动窗口的两个指针移动的过程和虫子爬动的过程非常像:前脚不动,把后脚移动过来;后脚不动,把前脚向前移动。
我分享一个滑动窗口的模板,能解决大多数的滑动窗口问题:
1 | public int slidingWindow(int[] nums) { |
滑动窗口中用到了左右两个指针,它们移动的思路是:以右指针作为驱动,拖着左指针向前走。右指针每次只移动一步,而左指针在内部 while 循环中每次可能移动多步。右指针是主动前移,探索未知的新区域;左指针是被迫移动,负责寻找满足题意的区间。
模板的整体思想是:
- 定义两个指针 left 和 right 分别指向区间的开头和结尾,注意是闭区间;定义 sums 用来统计该区间内的各个字符出现次数;
- 第一重 while 循环是为了判断 right 指针的位置是否超出了数组边界;当 right 每次到了新位置,需要增加 right 指针的求和/计数;
- 第二重 while 循环是让 left 指针向右移动到 [left, right] 区间符合题意的位置;当 left 每次移动到了新位置,需要减少 left 指针的求和/计数;
- 在第二重 while 循环之后,成功找到了一个符合题意的 [left, right] 区间,题目要求最大的区间长度,因此更新 res 为 max(res, 当前区间的长度) 。
- right 指针每次向右移动一步,开始探索新的区间。
模板中的 sums 需要根据题目意思具体去修改,本题是求和题目因此把 sums 定义成整数用于求和;如果是计数题目,就需要改成字典用于计数。当左右指针发生变化的时候,都需要更新 sums 。
另外一个需要根据题目去修改的是内层 while 循环的判断条件,即: 区间 [left, right][left,right] 不符合题意 。对于本题而言,就是该区间内的 0 的个数超过了 2 。