滑动窗口算法
滑动窗口算法
名称
滑动窗口算法
场景
当遇到找出序列中连续子序列满足某些条件的问题的时候,可以使用滑动窗口. 序列包括:数组,字符串,List,链表等.
当我们遇到了求满足目标的连续子序列的时候,第一直觉适用滑动子窗口
解决方案
滑动窗口模板:
//创建双指针
int left= start;
int right= start;
//右指针移动
while(right < end){
将rank=right位置的数据添加到窗口中.
while(!窗口不合规){
// 判断是否满足条件,更新结果.
left++;
}
if(窗口满足最终条件){
更新最终结果.
刷新窗口.
}
right++;
}
返回结果.
效果
满足某个要求的连续子串普通的实现至少是 O(n^2).但是适用滑动窗口能够将问题复杂度降低到 O(n).
实例
问题一 剑指 Offer II 008. 和大于等于 target 的最短子数组
给定一个含有n个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组[numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
给定一个含有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 = 4, nums = [1,4,4]
输出:1
示例 3:输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 105
进阶:
如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。
class Solution {
public int minSubArrayLen(int target, int[] nums) {
/**
* 7
* [2,3,1,2,4,3]
*
* 给定一个数组,正整数,找出最短的连续子数组,求出长度最小的连续子序列.
* 满足条件是 子序列的和 >=target
* 求出长度最短的那个
*/
if (nums.length == 0) {
return 0;
}
int left = 0, right = 0;
int minLength = Integer.MAX_VALUE;
int sum = 0;
while (right < nums.length) {
sum += nums[right];
//[left,right]
while (sum >= target) {
minLength = Math.min(minLength, (right - left + 1));
sum -= nums[left];
left++;
}
right++;
}
return minLength == Integer.MAX_VALUE ? 0 : minLength;
}
}
如果使用 nlogn复杂度的算法.这个时候适用累加和,然后用二分查找累加和.
问题二 剑指 Offer II 009. 乘积小于 K 的子数组
给定一个正整数数组nums和整数 k,请找出该数组内乘积小于k的连续的子数组的个数。
示例 1:
输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。
示例 2:
输入: nums = [1,2,3], k = 0
输出: 0
提示:
1 <= nums.length <= 3 * 104
1 <= nums[i] <= 1000
0 <= k <= 106
class Solution {
public int numSubarrayProductLessThanK(int[] nums, int k) {
/**
* 找出所有的连续子数组,其乘积 < target元素
*/
int left = 0, right = 0;
int result = 1;
int count = 0;
while (right < nums.length) {
result *= nums[right];
while (left <= right && result >= k) {
result /= nums[left];
left++;
}
//当前满足了 以nums[right]结尾的子序列的乘积 < k
//这里 count += right - left +1 的思路是 以当前right结尾的所有的子序列.
// 其中 左区间可以是 [right,left].所以是 额外添加的元素是 (right -left +1)
count += right - left + 1;
right++;
}
return count;
}
}
问题三 剑指 Offer II 014. 字符串中的变位词
给定两个字符串s1和s2,写一个函数来判断 s2 是否包含 s1的某个变位词。
换句话说,第一个字符串的排列之一是第二个字符串的 子串 。
示例 1:
输入: s1 = "ab" s2 = "eidbaooo"
输出: True
解释: s2 包含 s1 的排列之一 ("ba").
示例 2:
输入: s1= "ab" s2 = "eidboaoo"
输出: False
提示:
1 <= s1.length, s2.length <= 104
s1 和 s2 仅包含小写字母
class Solution {
public boolean checkInclusion(String s1, String s2) {
/**
* s2是否包括s1的排列
*/
int left = 0, right = 0;
int[] chCount = new int[26];
for (int i = 0; i < s1.length(); i++) {
char ch = s1.charAt(i);
chCount[ch - 'a']++;
}
while (right < s2.length()) {
int rightRank = s2.charAt(right) - 'a';
chCount[rightRank]--;
while (chCount[rightRank] < 0) {
int leftRank = s2.charAt(left) - 'a';
chCount[leftRank]++;
left++;
}
if (right - left + 1 == s1.length()) {
return true;
}
right++;
}
return false;
}
}
这个题首先不断的nums[right]添加窗口并统计计数.依次将nums[left]移出窗口直到窗口合规,然后校验最终结果是否符合条件.
这题是标准的模板实现.
问题四 剑指 Offer II 017. 含有所有字符的最短字符串
给定两个字符串 s 和t 。返回 s 中包含t的所有字符的最短子字符串。如果 s 中不存在符合条件的子字符串,则返回空字符串 "" 。
如果 s 中存在多个符合条件的子字符串,返回任意一个。
注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最短子字符串 "BANC" 包含了字符串 t 的所有字符 'A'、'B'、'C'
示例 2:
输入:s = "a", t = "a"
输出:"a"
示例 3:
输入:s = "a", t = "aa"
输出:""
解释:t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。
提示:
1 <= s.length, t.length <= 105
s 和 t 由英文字母组成
进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗?
class Solution {
public String minWindow(String s, String t) {
/**
* 这个题使用滑动窗口,首先适用右侧进行滑动,然后不断判断[left,right]是否满足了条件所有的字母个数都满足了条件.然后移动左侧.知道不满足为止.然后获取[left-1,right]为一个符合条件的字符串.
* 然后右侧继续移动,知道满足条件为止.
*/
Map<Character, Integer> targetChCountMap = new HashMap<>();
for (int i = 0; i < t.length(); i++) {
char ch = t.charAt(i);
targetChCountMap.put(t.charAt(i), targetChCountMap.getOrDefault(ch, 0) + 1);
}
Map<Character, Integer> chCountMap = new HashMap<>();
int left = 0, right = 0;
boolean isLegal = false;
String minLenStr = null;
while (right < s.length()) {
if (!isLegal) {
char rightCh = s.charAt(right);
chCountMap.put(rightCh, chCountMap.getOrDefault(rightCh, 0) + 1);
isLegal = isLegalStr(chCountMap, targetChCountMap);
//right指向了下一个位置
right++;
}
while (left <= right && isLegal) {
char leftCh = s.charAt(left);
chCountMap.put(leftCh, chCountMap.get(leftCh) - 1);
isLegal = isLegalStr(chCountMap, targetChCountMap);
if (!isLegal) {
String targetStr = s.substring(left, right);
if (minLenStr == null || minLenStr.length() > targetStr.length()) {
minLenStr = targetStr;
}
}
left++;
}
}
return minLenStr == null ? "" : minLenStr;
}
private boolean isLegalStr(Map<Character, Integer> chCountMap, Map<Character, Integer> targetChCountMap) {
for (Map.Entry<Character, Integer> entry : targetChCountMap.entrySet()) {
Integer chCount = chCountMap.get(entry.getKey());
if (chCount == null || chCount < entry.getValue()) {
return false;
}
}
return true;
}
}
求cover了包含 t字符串的子串.这个题是这样做的:
1.将nums[right]不断添加到窗口中.直到满足条件
2.然后左移窗口,直到不满足条件来压缩窗口长度,找出最小长度的窗口
3.计算最终结果.
4.循环第一步
问题四 剑指 Offer II 057. 值和下标之差都在给定的范围内
给你一个整数数组 nums 和两个整数k 和 t 。请你判断是否存在 两个不同下标 i 和 j,使得abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k 。
如果存在则返回 true,不存在返回 false。
示例1:
输入:nums = [1,2,3,1], k = 3, t = 0
输出:true
示例 2:
输入:nums = [1,0,1,1], k = 1, t = 2
输出:true
示例 3:
输入:nums = [1,5,9,1,5,9], k = 2, t = 3
输出:false
提示:
0 <= nums.length <= 2 * 104
-231 <= nums[i] <= 231 - 1
0 <= k <= 104
0 <= t <= 231 - 1
class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
/**
* 这个题是:给定一个长度为 k的滑动窗口. 找出 最后一个元素和之前的元素的差值在 [nums[i]-t,nums[i]+t].
*
*/
TreeSet<Long> window = new TreeSet<>();
for (int i = 0; i < nums.length; i++) {
//从window中找出一个 介于 [nums[i] - t , nums[i] +t]的元素.如果有就返回.
Long ceiling = window.ceiling((long) nums[i] - (long) t); //找出 第一个 ceiling >= nums[i] -t的元素
//判断这个元素是否 <= nums[i] + t
if (ceiling != null && ceiling <= (long) nums[i] + (long)t) {
//如果满足条件就返回false
return true;
}
window.add((long) nums[i]);
if (i >= k) {
window.remove((long)nums[i - k]);
}
}
return false;
}
}
这里的区间有点不同,窗口不仅仅使用的是 nums[left,right],同时使用一个TreeSet将窗口数据添加进来,便于处理