其实滑动窗口是一类特殊的双指针类型题,两个指针同向移动,我们更关心两个指针内所包含的数据,这时就可以称为滑动窗口类型的题了,很多解法我们很自然的就能想到用滑动窗口去解决,比如,“在一个数组中,那段连续元素相加等于target”。这种一看就知道,应该用两个指针做滑动窗口,然后计算包含的值的结果即可,总不可能用循环嘛对不对。
滑动窗口其实理论基础就是双指针,指的是一类问题的求解方法,在数组上通过双指针同向移动而解决的一类问题。其实我们可以不必为它们专门命名一个名字,它们的解法其实是很自然的。不过使用滑动窗口解决的问题通常是暴力解法的优化,掌握这一类问题最好的办法就是练习,然后思考清楚为什么可以使用滑动窗口。
题目解析:用滑动窗口思想解题即可,用两个指针选择子串,然后用哈希表判断是否有重复。
代码如下:
/**
*
*/
class Solution {
public int lengthOfLongestSubstring(String s) {
int ans = 0;
char[] arr = s.toCharArray();
Map map = new HashMap<>();
for (int j = 0, i = 0; j < arr.length; j++) {
if (map.containsKey(arr[j])) {
i = Math.max(map.get(arr[j]), i);
}
ans = Math.max(ans, j - i + 1);
map.put(arr[j], j + 1);//下标 + 1 代表 i 要移动的下个位置
}
return ans;
}
}
题目解析:记words的长度为m,words中每个单词的长度为n,s的长度为ls。 首先需要将s划分为单词组,每个单词的大小均为n (首尾除外)。这样的划分方法有n种,即先删去前i (i=0≈n-1) 个字母后,将剩下的字母进行划分,如果末尾有不到n个字母也删去。对这n种划分得到的单词数组分别使用滑动窗口对words进行类似于字母异位词的搜寻。
代码如下:
/**
* 滑动窗口
*/
class Solution {
public List findSubstring(String s, String[] words) {
List res = new ArrayList();
int m = words.length, n = words[0].length(), ls = s.length();
for (int i = 0; i < n; i++) {
if (i + m * n > ls) {
break;
}
Map differ = new HashMap();
for (int j = 0; j < m; j++) {
String word = s.substring(i + j * n, i + (j + 1) * n);
differ.put(word, differ.getOrDefault(word, 0) + 1);
}
for (String word : words) {
differ.put(word, differ.getOrDefault(word, 0) - 1);
if (differ.get(word) == 0) {
differ.remove(word);
}
}
for (int start = i; start < ls - m * n + 1; start += n) {
if (start != i) {
String word = s.substring(start + (m - 1) * n, start + m * n);
differ.put(word, differ.getOrDefault(word, 0) + 1);
if (differ.get(word) == 0) {
differ.remove(word);
}
word = s.substring(start - n, start);
differ.put(word, differ.getOrDefault(word, 0) - 1);
if (differ.get(word) == 0) {
differ.remove(word);
}
word = s.substring(start - n, start);
}
if (differ.isEmpty()) {
res.add(start);
}
}
}
return res;
}
}
题目解析:使用右指针不断扩张,去寻找一个可行解,找到可行解之后,使用左指针进行收缩,优化可行解,同时更新结果,一次遍历即可得到最终解。
代码如下:
/**
* 滑动窗口
*/
class Solution {
public String minWindow(String s, String t) {
HashMap need = new HashMap<>(); // 用来保存所需要匹配的字符个数
HashMap window = new HashMap<>(); // 用来保存当前窗口内能满足匹配的字符个数
int left = 0, right = 0; // 左右窗口指针,逻辑上定义为左闭右开的区间,这样的话[0,0)时,区间内就没有元素,方便边界处理
int valid = 0; // 记录当前window有多少个字符满足need,当全部满足时,开始收缩左指针
int start = 0, end = 0; // 记录满足条件字符串的起始位置
int len = Integer.MAX_VALUE; // 记录满足条件的字符串的长度,用于更新最终结果
// 初始化need
for(int i = 0; i < t.length(); i++){
need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0) + 1); // 将相应的键值对存入need
}
// 滑动窗口开始工作
while(right < s.length()){ // 保证right遍历了整个字符串
// 先右窗口扩张,找到可行解
char in_ch = s.charAt(right);
right++;
if(need.containsKey(in_ch)){ // 如果遍历到的是我们需要的字符,则将其加入window中,保证need和window保存的是同种字符
window.put(in_ch, window.getOrDefault(in_ch, 0) + 1);
if(window.get(in_ch).equals(need.get(in_ch))){ // 第一次相等时更新valid, 注意Integer对象要用equals来进行比较,不能用 ==
valid++; // 表示有一个字符已经满足了
}
}
// 如果valid全部满足,则去收缩左窗口,优化可行解
while(valid == need.size()){
// 保存当前解
if(len > right - left){
len = right-left;
start = left;
end = right;
}
// 同时收缩左窗口,优化当前可行解
char out_ch = s.charAt(left);
left++;
if(need.containsKey(out_ch)){
if(window.get(out_ch).equals(need.get(out_ch))){ // 因为window内的某字符数量可能多于need中的,所以当相等的时候再--
valid--;
}
window.put(out_ch, window.get(out_ch)-1); // 同时window内该字符数量-1
}
}
}
return s.substring(start, end);
}
}
题目解析:用一个哈希表统计 s 所有长度为 10 的子串的出现次数,返回所有出现次数超过 10 的子串。
代码如下:
/**
* 滑动窗口 + 哈希表
*/
class Solution {
public List findRepeatedDnaSequences(String s) {
Set set = new HashSet();
List res = new ArrayList();
int len = s.length();
if(len <= 10) return res;
for(int i = 0; i < len - 9; i++){
String str =s.substring(i, i + 10);
if(!set.add(str) && !res.contains(str))res.add(str);
}
return res;
}
}
题目解析:滑动窗口经典题,用一个int值比较各窗口值大小即可。
代码如下:
/**
* 滑动窗口
*/
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int left = 0;
int sum = 0;
int result = Integer.MAX_VALUE;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= s) {
result = Math.min(result, right - left + 1);
sum -= nums[left++];
}
}
return result == Integer.MAX_VALUE ? 0 : result;
}
}
题目解析:对于序列中每一个元素x左侧的至多k个元素,如果这k个元素中存在一个元素落在区间[x- t, x +t]中,我们就找到了一对符合条件的元素。注意到对于两个相邻的元素,它们各自的左侧的k个元素中有k-1个是重合的。于是我们可以使用滑动窗口的思路,维护一一个大小为k的滑动窗口,每次遍历到元素x时,滑动窗口中包含元素x前面的最多k个元素,我们检查窗口中是否存在元素落在区间[x - t, x + t]中即可。
代码如下:
/**
* 滑动窗口 + 有序集合
*/
class Solution {
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
// 滑动窗口结合查找表,此时滑动窗口即为查找表本身(控制查找表的大小即可控制窗口大小)
TreeSet set = new TreeSet<>();
for (int i = 0; i < nums.length; i++) {
// 边添加边查找
// 查找表中是否有大于等于 nums[i] - t 且小于等于 nums[i] + t 的值
Long ceiling = set.ceiling((long) nums[i] - (long) t);
if (ceiling != null && ceiling <= ((long) nums[i] + (long) t)) {
return true;
}
// 添加后,控制查找表(窗口)大小,移除窗口最左边元素
set.add((long) nums[i]);
if (set.size() == k + 1) {
set.remove((long) nums[i - k]);
}
}
return false;
}
}
题目解析:利用双端队列手动实现单调队列,用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可。单调队列类似 (tail -->) 3 --> 2 --> 1 --> 0 (–> head) (右边为头结点,元素存的是下标)。
代码如下:
/**
* 单调队列
*/
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
ArrayDeque deque = new ArrayDeque<>();
int n = nums.length;
int[] res = new int[n - k + 1];
int idx = 0;
for (int i = 0; i < n; i++) {
// 根据题意,i为nums下标,是在[i - k + 1, i] 中选到最大值,只需要保证两点
// 1.队列头结点需要在[i - k + 1, i]范围内,不符合则要弹出
while (!deque.isEmpty() && deque.peek() < i - k + 1) {
deque.poll();
}
// 2.既然是单调,就要保证每次放进去的数字要比末尾的都大,否则也弹出
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offer(i);
// 因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了
if (i >= k - 1) {
res[idx++] = nums[deque.peek()];
}
}
return res;
}
}
刷 leetcode 500+ 题的一些感受
《算法系列》之排序