滑动窗口过程:
进窗口->判断->更新结果->出窗口
长度最小的子数组
算法思路:
由于分析的对象是「⼀段连续的区间」,因此可以考虑用「滑动窗口」来解决。
窗口左端为nums[left],右端下一个元素为nums[right]. 将nums[right]划入窗口中(sum += nums[right] )
- 如果窗口内元素之和大于等于 target :记录出此时窗口内元素数量,并且将左端元素划出继续判断是否满足条件,如果满足更新结果(因为左端元素可能很小,划出去之后依旧满足条件)
- 如果窗口内元素之和不满足条件: right++ ,放下⼀个元素进入窗口。
为何滑动窗口可以解决问题,并且时间复杂度更低?
因为我们在求第⼀段区间的时候,已经算出很多元素的和了,这些和是可以在计算
下次区间和的时候用上, 将 nums[left]剔除, 就能判断下一个区间, 这样就能省掉大量重复的计算。
时间复杂度:虽然代码是两层循环,但是我们的 left 指针和 right 指针都是不回退的,两者最多都往后移动 n 次。因此时间复杂度是 O(N) 。
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length, sum = 0, len = Integer.MAX_VALUE;
for(int left = 0, right = 0;right < n; right++) {
sum += nums[right];
while(sum >= target) {
len = Math.min(len, right-left+1);
sum -= nums[left++];
}
}
return len == Integer.MAX_VALUE ? 0:len;
}
}
无重复字符的最长子串
算法思路:
研究的对象依旧是⼀段连续的区间,因此可以使用「滑动窗口」思想来优化。
让滑动窗口满足:窗口内所有元素都是不重复的。
右端元素进入窗口的时候,哈希表统计这个字符在窗口内的频次:
▪ 如果这个字符出现的频次超过 1 ,说明窗口内有重复元素,那么就从左侧划出元素,直到重复的元素的频次变为 1 ,然后再更新结果(ret)。
▪ 如果没有超过1,说明当前窗口没有重复元素,可以直接更新结果(ret)
class Solution {
public int lengthOfLongestSubstring(String s) {
char[] str = s.toCharArray();
int[] hash = new int[128];//数组模拟哈希表
int left = 0, right = 0, n = str.length;
int ret = 0;
while(right < n) {
hash[str[right]]++;
while(hash[str[right]] > 1) {
hash[str[left]]--;
left++;
}
ret = Math.max(ret, right-left+1);
right++;
}
return ret;
}
}
最大连续1的个数III
算法思路(滑动窗口):
不要去想怎么翻转,不要把问题想的很复杂,这道题的结果无非就是⼀段连续的 1 中间塞了 k个 0 嘛。
因此,我们可以把问题转化成:求数组中⼀段最长的连续区间,要求这段区间内 0 的个数不超过 k 个。既然是连续区间,可以考虑使用「滑动窗口」来解决问题。
算法流程:
- 用变量zero统计窗口内0的个数;初始化⼀些变量 left = 0 ,right = 0 , ret = 0 ;
- 当 right 小于数组长度的时候,⼀直下列循环:
i. 让右侧元素进入窗口,如果是0, zero++
ii. 检查 0 的个数是否超过k, 如果超过,依次让左侧元素滑出窗口,直到 0 的个数恢复正
常
iii. 程序到这里,说明窗口内元素是符合要求的,更新结果(ret);
iv. right++ ,处理下⼀个元素;
- 循环结束后, ret 存的就是最终结果。
class Solution {
public int longestOnes(int[] nums, int k) {
int ret = 0;
for(int left = 0, right = 0, zero = 0; right < nums.length; right++) {
if(nums[right] == 0) zero++;
while(zero > k) {
if(nums[left++] == 0) {
zero--;
}
}
ret = Math.max(ret, right - left + 1);
}
return ret;
}
}
还有一种:
每次循环right都会向后一位
- 如果K >= 0,left不会向后一位
- 如果K < 0,left也会向后一位
所以 right - left 不会变小
class Solution {
public int longestOnes(int[] nums, int k) {
int left = 0, right = 0;
while (right < nums.length) {
if (nums[right++] == 0) k--;
if (k < 0 && nums[left++] == 0) k++;
}
return right - left;
}
}
将x减到0的最小操作数
算法思路(滑动窗口):
题目要求的是数组「左端+右端」两段连续的、和为 x 的最短数组,我们可以转化成求数组内⼀段连续的、和为 sum(nums) - x 的最长数组。因为元素>=1。此时,就是熟悉的「滑动窗口」问题了。
算法流程:
- 转化问题: target = sum(nums) - x 。如果 target < 0 ,问题无解;
- 初始化左右指针 left = 0 , right = 0 ;初始化记录滑动窗口内元素之和的变量sum = 0
- 当 right 小于等于数组长度时,⼀直循环:
⑴将right对应的元素划入窗口,如果 tmp > target, 循环减去左侧元素直至tmp <= target
⑵判断tmp == target,
- 如果相等,更新ret,进入下次循环
- 如果不等,说明 tmp < target, 进入下次循环
- 循环结束后,如果 ret 的值有意义,则计算结果返回;否则,返回 -1 。
class Solution {
public int minOperations(int[] nums, int x) {
int sum = 0;
for(int a: nums) sum += a;
int target = sum - x;
//处理细节
if(target < 0) return -1;
int ret = -1;
for(int left = 0, right = 0, tmp = 0; right <nums.length; right++) {
tmp += nums[right];
while(tmp > target) {
tmp -= nums[left++];
}
if(tmp == target) {
ret = Math.max(ret, right - left + 1);
}
}
return ret == -1 ? ret: nums.length - ret;
}
}
水果成篮
算法思路(滑动窗口):
研究的对象是⼀段连续的区间,可以使用「滑动窗口」思想来解决问题。
让滑动窗口满足:窗口内水果的种类只有两种。
做法:右端水果进入窗口的时候,用哈希表统计这个水果的频次,判断哈希表的长度:
- 如果长度大于 2:说明窗口内水果种类超过了两种。那么就从左侧开始依次将水果划出,直到哈希表的大小小于等于 2,然后更新结果(ret);
- 如果没有超过 2,说明当前窗⼝内水果的种类不超过两种,直接更新结果 ret。
算法流程:
- 用Map来统计窗口内水果的种类和数量;
- 初始化变量:左右指针 left = 0,right = 0,记录结果的变量 ret = 0;
- 当 right 小于数组长度的时候,⼀直执行下列循环:
i. 将右侧水果放入哈希表中;
ii. 判断右侧水果进来后,哈希表的长度:如果超过 2:
- 将左侧元素滑出窗⼝,并且在哈希表中将该元素的频次-1;
- 如果这个元素的频次-1之后变成了 0,就把该元素从哈希表中删除;
- 重复上述两个过程,直到哈希表的长度不超过 2;
iii. 更新结果 ret;
iv. right++,让下⼀个元素进⼊窗口;
- 循环结束后,ret 存的就是最终结果
一、 使用容器
class Solution {
public int totalFruit(int[] fruits) {
Map<Integer, Integer> hash = new HashMap<Integer, Integer>();
int ret = 0;
for(int left = 0, right = 0; right < fruits.length; right++) {
hash.put(fruits[right], hash.getOrDefault(fruits[right], 0) + 1);
while(hash.size() > 2) {
hash.put(fruits[left], hash.get(fruits[left]) - 1);
if(hash.get(fruits[left]) == 0) {
hash.remove(fruits[left]);
}
left++;
}
ret = Math.max(ret, right - left +1);
}
return ret;
}
}
二、 用数组模拟哈希表
kinds统计窗口内水果的种类
class Solution {
public int totalFruit(int[] fruits) {
int n = fruits.length;
int[] hash = new int[n];//模拟哈希表
int ret = 0;
for(int left = 0, right = 0, kinds = 0; right < n; right++) {
if(hash[fruits[right]] == 0) kinds++;
hash[fruits[right]]++;
while( kinds > 2) {
hash[fruits[left]]--;
if(hash[fruits[left]] == 0) kinds--;
left++;
}
//更新结果
ret = Math.max(ret, right - left +1);
}
return ret;
}
}
找到字符串中所有字母异位词
算法思路(滑动窗口 + 哈希表):
因为字符串 p 的异位词的长度⼀定与字符串 p 的长度相同,所以我们可以在字符串 s 中构造⼀个长度与字符串 p 相同的滑动窗口,并在滑动中维护窗口中字母的数量。当窗口中每种字母的数量与字符串 p 相同时,说明当前窗口为字符串 p的异位词。
可以用两个大小为 26 的数组来模拟哈希表(全是小写字母),⼀个来保存 s 中的子串每个字符出现的个数,另⼀个来保存 p 中每⼀个字符出现的个数。这样就能判断两个串是否是异位词。
count统计符合要求的字符个数
class Solution {
public List<Integer> findAnagrams(String s, String p) {
List<Integer> ret = new ArrayList<>();
char[] pp = p.toCharArray();
char[] ss = s.toCharArray();
int[] hash1 = new int[26];
for(char ch: pp) hash1[ch-'a']++;
int[] hash2 = new int[26];
int m = pp.length;
for(int left = 0, right = 0, count = 0; right < ss.length; right++) {
char in = ss[right];
if(++hash2[in-'a'] <= hash1[in-'a']) count++;
if(right - left + 1 > m) {
char out = ss[left++];
if(hash2[out-'a'] <= hash1[out-'a']) {
count--;
}
hash2[out-'a']--;
}
//更新结果
if(count == m) ret.add(left);
}
return ret;
}
}
串联所有单词的串
如果我们把单词看成字母,问题就变成了找到「字符串中所有的字母异位词」。无非就是之前处理的对象是一个一个的字符,我们这里处理的对象是⼀个⼀个的单词。
class Solution {
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> ret = new ArrayList<>();
Map<String,Integer> hash1 = new HashMap<>();
for(String str : words) hash1.put(str, hash1.getOrDefault(str, 0) + 1);
int len = words[0].length(), m= words.length;
for(int i = 0; i < len; i++) {
Map<String,Integer> hash2 = new HashMap<>();
for(int left = i, right = i, count = 0; right + len <= s.length(); right += len) {
String in = s.substring(right, right+len);
hash2.put(in, hash2.getOrDefault(in, 0)+1);
if(hash2.get(in) <= hash1.getOrDefault(in, 0)) count++;
if(right - left + 1 > len * m) {
String out = s.substring(left, left +len);
if(hash2.get(out) <= hash1.getOrDefault(out, 0)) count--;
hash2.put(out, hash2.get(out) - 1);
left += len;
}
if(count == m) ret.add(left);
}
}
return ret;
}
}
最小覆盖子串
算法思路(滑动窗口+ 哈希表):
研究对象是连续的区间,因此可以使用滑动窗口的思想来解决。
我们可以使用两个哈希表,其中⼀个将目标串的信息统计起来,另⼀个哈希表动态维护窗口内字符串的信息。
使用count标记有效字符的种类
class Solution {
public String minWindow(String s, String t) {
char[] ss = s.toCharArray();
char[] tt = t.toCharArray();
int[] hash1 = new int[128];//统计t中元素的频次
int kinds = 0;//t中有多少种字符
for(char ch: tt) {
if(hash1[ch]++ == 0) {
kinds++;
}
}
int[] hash2 = new int[128];//统计窗口中字符的频次
int minlen = Integer.MAX_VALUE, begin = -1;
for(int left = 0, right = 0, count = 0; right < ss.length; right++) {
char in = ss[right];
if(++hash2[in] == hash1[in]) count++;
while(kinds == count) {
if(right - left + 1 < minlen) {
begin = left;
minlen = right - left + 1;
}
char out = ss[left++];
if(hash2[out]-- == hash1[out]) count--;
}
}
if(begin == -1) return new String();
else return s.substring(begin, begin + minlen);
}
}
小葱的01串
注意是给定一个长度为偶数的环形 01 字符串
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
char[] s = in.next().toCharArray();
int[] sum = new int[2];
for(int i = 0; i < n; i++) sum[s[i] - '0']++;
int left = 0, right = 0, ret = 0, half = n/2;
int[] count = new int[2];
while(right < n-1) { //细节问题
count[s[right] - '0']++;
if(right - left + 1 > half) count[s[left++] - '0']--;
if(right - left + 1 == half) {
if(count[0] * 2 == sum[0] && count[1] * 2 == sum[1]) ret += 2;
}
right++;
}
System.out.print(ret);
}
}
空调遥控
只需要找到范围小于等于2p的最大窗口即可
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt(), p = in.nextInt();
int[] arr = new int[n];
for(int i = 0; i < n; i++) {
arr[i] = in.nextInt();
}
Arrays.sort(arr);
int left = 0, right = 0, ret = 0;
p *= 2;
while(right < n) {
while(arr[right] - arr[left] > p) left++;
ret = Math.max(ret, right - left + 1);
right++;
}
System.out.print(ret);
}
}
小红的子串
import java.util.Scanner;
public class Main {
static int n, l, r;
static char[] s;
static long find(int x) {
if(x == 0) return 0;
//滑动窗口
int left = 0, right = 0;
int[] hash = new int[26];
int kinds = 0;//统计窗口内字符的种类
long ret = 0;
while (right < n) {
if (hash[s[right]-'a']++ == 0) kinds++;
while (kinds > x) {
if(hash[s[left]-'a']-- == 1) kinds--;
left++;
}
ret += right - left + 1;
right++;
}
return ret;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt(); l = sc.nextInt(); r = sc.nextInt();
s = sc.next().toCharArray();
System.out.println(find(r)-find(l-1));
}
}
本文介绍了如何使用滑动窗口策略解决一系列IT技术问题,如长度最小的子数组、无重复字符的最长子串、最大连续1的个数、将x减到0的操作数、水果成篮、字母异位词匹配、串联所有单词的串以及最小覆盖子串等,展示了滑动窗口算法在处理连续区间问题上的高效性。
2731

被折叠的 条评论
为什么被折叠?



