前言:
滑动窗口算法专门优化一种连续问题场景,如找出字符串或者数组中满足xx条件的最长(或最短)的连续子串(或子数组)。
滑动窗口的解题思路如下:
需要用到双指针进行求解,两个指针构造一个窗口,窗口的移动是重点!
右指针每次往前移动一格,每次移动会有一个新的元素进入窗口,这时条件可能就会发生变化,再根据当前条件来决定左指针是否移动,以及移动多少格。总的来说,右指针每次必然要移动一格,目的是要探索“可能性”;左指针要随着移动,目的是要确定这次探索的“正确性”。滑动窗口的时间复杂度是O(n),因为2个指针分别遍历一次数组。
滑动窗口一般有3种类型:
1、固定窗口大小;
2、窗口大小不固定,求解最大能满足条件的窗口;
3、窗口大小不固定,求解最小能满足条件的窗口;
但是不管是哪种类型,基本的思路都是一样的,不一样的仅仅是代码的细节。
固定窗口大小的模板比较简单:
public int slidingWindowTemplate(String[] a, ...) {
// 输入参数有效性判断
if (...) {
...
}
// 申请一个散列,用于记录窗口中具体元素的个数情况
// 这里用数组的形式呈现,也可以考虑其他数据结构,如Set,Map等
int[] hash = new int[...];
// 预处理(可省), 一般情况是改变 hash
...
// l 表示左指针,r 表示右指针,如果是固定窗口,也可以只使用一个变量i来控制窗口大小
// count 记录当前的条件,具体根据题目要求来定义
// result 用来存放结果
int l = 0, r = 0, count = ..., result = ...;
for (int i = 0; i < ...; i++) {
// 先找到窗口的后边界
}
for (int i = 右边界; i < ...; i++) {
// 再依次滑动窗口,判断是否满足条件
if (条件满足) {
// 更新结果
results = ...
}
}
return results;
}
不固定窗口大小,代码模板如下:
public int slidingWindowTemplate(String[] a, ...) {
// 输入参数有效性判断
if (...) {
...
}
// 申请一个散列,用于记录窗口中具体元素的个数情况
// 这里用数组的形式呈现,也可以考虑其他数据结构,如Set,Map等
int[] hash = new int[...];
// 预处理(可省), 一般情况是改变 hash
...
// l 表示左指针,r 表示右指针
// count 记录当前的条件,具体根据题目要求来定义
// result 用来存放结果
int l = 0, r = 0, count = ..., result = ...;
while (r < A.length) {
// 更新新元素在散列中的数量
hash[A[r]]--;
// 根据窗口的变更结果来改变条件值
if (hash[A[r]] == ...) {
count++;
}
// 如果当前条件不满足,移动左指针直至条件满足为止
while (count > K || ...) {
...
if (...) {
count--;
}
hash[A[l]]++;
l++;
}
// 更新结果
results = ...
}
return results;
}
正文
1、LeetCode 438. 找到字符串中所有字母的异位词
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
这是一道固定窗口大小的滑动窗口题,窗口的大小就是字符串p的长度。我们先构造好窗口,再把窗口一点一点往右滑动,滑动的过程中判断条件是否成立。代码如下:
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int sl = s.length();
int pl = p.length();
List<Integer> ans = new ArrayList<>();
if (sl < pl) {
return ans;
}
// 由于题目中说明了字符串只包含小写字母,所以可以使用数组进行字母计数
int[] sArr = new int[26];
int[] pArr = new int[26];
// 先构造出初始化的窗口
for (int i = 0; i < pl; i++) {
sArr[s.charAt(i) - 'a']++;
pArr[p.charAt(i) - 'a']++;
}
if (Arrays.equals(sArr, pArr)) {
ans.add(0);
}
// 再一点一点滑动窗口,注意判断满足的条件
for (int i = pl; i < sl; i++) {
sArr[s.charAt(i) - 'a']++;
sArr[s.charAt(i - pl) - 'a']--;
if (Arrays.equals(sArr, pArr)) {
ans.add(i - pl + 1);
}
}
return ans;
}
}
2、LeetCode 3. 无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
这是一道窗口大小变化的滑动窗口题,先移动右指针,每次将窗口里增加一个元素,增加后,再判断是否满足条件,不满足的话,移动左指针,直到满足条件。
class Solution {
public int lengthOfLongestSubstring(String s) {
int left = 0;
int right = 0;
int ans = 0;
// 题目要求无重复的元素,可以使用Set结构进行存储
Set<Character> set = new HashSet<>();
// 移动右指针
while (right < s.length()) {
char c = s.charAt(right);
right++;
// 如果不再满足条件,移动左指针,直到条件再次满足
// 滑动窗口题目最重要的点就是要找到满足窗口的条件
while (set.contains(c)) {
set.remove(s.charAt(left));
left++;
}
set.add(c);
// 更新结果
ans = Math.max(ans, set.size());
}
return ans;
}
}
3、LeetCode 1004. 最大连续1的个数 III
给定一个由若干 0 和 1 组成的数组 A,我们最多可以将 K 个值从 0 变成 1 。返回仅包含 1 的最长(连续)子数组的长度。
示例 1:
输入:A = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出:6
解释:
[1,1,1,0,0,1,1,1,1,1,1]
粗体数字从 0 翻转到 1,最长的子数组长度为 6。
如果这道题没有说可以从0变成1,那么将非常简单,如果加了这个条件,难点在于不知道要将哪些0变成1,如果使用枚举,复杂度将非常大。这道题同样可以使用滑动窗口,限定条件就是窗口内的0的个数不能多于K个。
class Solution {
public int longestOnes(int[] nums, int k) {
int left = 0;
int right = 0;
int ans = 0;
int total = k;
// 移动右指针
while (right < nums.length) {
// 移动过程中统计0的个数
if (nums[right] == 0) {
total--;
}
right++;
// 如果不再满足条件,即窗口中0的个数多于K个了,就要移动左指针,
while (total < 0) {
if (nums[left] == 0) {
total++;
}
left++;
}
// 更新结果
ans = Math.max(ans, right - left);
}
return ans;
}
}
4、LeetCode 209. 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
这道题跟上面2题不同,这题是求满足条件的最短的子数组,其实套路是一样的,右指针先移动,然后计算窗口内元素的和,然后判断和是否大于target,在条件满足的情况下,尽可能的移动右指针,这样才能找到最小的间隔,代码如下:
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int left = 0;
int right = 0;
int ret = Integer.MAX_VALUE;
int tmpSum = 0;
// 右移指针
while (right < nums.length) {
// 累加窗口内元素之和
tmpSum += nums[right];
right++;
// 在满足条件的情况下,尽可能收缩窗口
while (tmpSum >= target) {
ret = Math.min(ret, right - left);
tmpSum -= nums[left];
left++;
}
}
return ret == Integer.MAX_VALUE ? 0 : ret;
}
}