滑动窗口:
所谓滑动窗口,就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
滑动窗口也可以理解为双指针法的一种,只不过这种解法更像是一个窗口的移动。
实现滑动窗口,主要确定如下三点:
- 确定窗口内是什么:一般是确定满足某个条件的最短或最长的区间
- 确定移动窗口的结束位置:结束位置先向后扩展到某个位置(此时受到某种约束不能继续扩展)确定移动窗口的起始位置:在结束位置确定后,起始位置朝着结束位置收缩(以找寻最优解(求最小区间时)并让结束位置能够继续后移(求最小/最大区间))
一、水果成篮
题目链接:904.水果成篮
/** * <pre> * <p>滑动窗口,使用两个指针表示左右边界</p> * </pre> * * @author <a href="https://github.com/kil1ua">Ken-Chy129</a> * @date 2023/1/4 10:50 */ public class 水果成篮904 { public int totalFruit(int[] fruits) { // lst1表示第一个篮子装的水果种类,num1表示装的数量,kind表示已经装了的种类数 // j指针移动结束时则结束计数并将本轮数量与历史最大数量比较得出最优解 int lst1 = -1, num1 = 0, lst2 = -1, num2 = 0, kind = 0, ans = 0; for (int i=0, j=0; i<fruits.length; i++) { // 如果已收集种类数不超过2,则j移动 while (kind <= 2 && j < fruits.length) { if (fruits[j] != lst1 && fruits[j] != lst2) { if (kind == 2) { // 如果已经拿了两种水果,下一棵树是第三种水果则结束循环 break; } if (num1 == 0) { lst1 = fruits[j++]; num1++; } else { lst2 = fruits[j++]; num2++; } kind++; } else if (fruits[j] == lst1) { j++; num1++; } else { j++; num2++; } ans = Math.max(j-i, ans); } // 循环结束时j指向下一个种类 if (fruits[i] == lst1) { num1--; if (num1 == 0) { // 第一个盘子已经没装了,这时装的水果种类数减一,意味着j可以继续右移 kind--; } } else if (fruits[i] == lst2) { num2--; if (num2 == 0) { kind--; } } } return ans; } // 使用Map进行处理,简化代码(逻辑相同) // 上法中的lst和num其实就是下面map中的key和value,kind就是map.size() // 因为map中封装了很多方法所以写出来的代码更简洁 public int way2(int[] fruits) { Map<Integer, Integer> cnt = new HashMap<>(); int ans = 0; for (int i=0, j=0; i<fruits.length; i++) { cnt.put(fruits[i], (cnt.getOrDefault(fruits[i], 0)) + 1); while (cnt.size() > 2) { cnt.put(fruits[j], cnt.get(fruits[j]) - 1); if (cnt.get(fruits[j]) == 0) { cnt.remove(fruits[j]); } j++; } ans = Math.max(ans, i-j+1); } return ans; } }
二、最小覆盖子串
题目链接:76.最小覆盖子串
/** * <pre> * <p></p> * </pre> * * @author <a href="https://github.com/kil1ua">Ken-Chy129</a> * @date 2023/1/4 13:45 */ public class 最小覆盖子串76 { private Map<Character, Integer> cnt = new HashMap<>(); private Map<Character, Integer> ori = new HashMap<>(); public String minWindow(String s, String t) { int len1 = s.length(); int len2 = t.length(); // 设置需求表,即每个字符需要出现多少次 for (int i=0; i<len2; i++) { ori.put(t.charAt(i), ori.getOrDefault(t.charAt(i), 0) + 1); } int pos1 = 0, pos2 = 0; // 左、右指针 int ansL = -1, ansR = -1, len = Integer.MAX_VALUE; while (pos2 < len1) { // 如果当前字符是t表中的字符,则需要增加cnt哈希表中该字符的数量 if (ori.containsKey(s.charAt(pos2))) { cnt.put(s.charAt(pos2), cnt.getOrDefault(s.charAt(pos2), 0) + 1); } // 如果当前窗口符合要求(通过判断cnt哈希表中的数量是否满足需求表中规定的数量) // 如果满足则尝试收缩左边界以找到最小的满足的情况 while (check() && pos1 <= pos2) { if (pos2 - pos1 + 1 < len) { // 如果新的窗口更小,则更新结果 len = pos2 - pos1 + 1; ansL = pos1; ansR = pos2; } if (ori.containsKey(s.charAt(pos1))) { // 如果值是t中的值则需要将map中的数量减少,以便判断是否还符合需求 cnt.put(s.charAt(pos1), cnt.getOrDefault(s.charAt(pos1), 0) - 1); } pos1++; // 窗口左边界不断收缩 } // 窗口右边界不断扩展 pos2++; } return ansL == -1 ? "" : s.substring(ansL, ansR+1); } public boolean check() { for (Map.Entry<Character, Integer> entry : ori.entrySet()) { if (cnt.getOrDefault(entry.getKey(), 0) < entry.getValue()) { return false; } } return true; } }
三、长度最小的子数组
题目链接:209长度最小的子数组
/** * <pre> * <p>滑动窗口求最小窗口O(n),也可用前缀和+二分O(nlogn)</p> * </pre> * * @author <a href="https://github.com/kil1ua">Ken-Chy129</a> * @date 2023/1/4 14:59 */ public class 长度最小的子数组209 { public int minSubArrayLen(int target, int[] nums) { int sum = 0, ans = Integer.MAX_VALUE; for (int i=0, j=0; j<nums.length; j++) { sum += nums[j]; while (sum >= target && i <= j) { ans = Math.min(ans, j - i + 1); sum -= nums[i++]; } } return ans == Integer.MAX_VALUE ? 0 : ans; } // 求出前缀和sums,区间[i, j]的和=sums[j]-sums[i] // 要使得区间>target,则遍历i,每次使用二分找到最小的j使得sum[i]+target<sum[j] public int minSubArrayLen2(int target, int[] nums) { int n = nums.length; if (n == 0) { return 0; } int ans = Integer.MAX_VALUE; int[] sums = new int[n + 1]; // 为了方便计算,令 size = n + 1 // sums[0] = 0 意味着前 0 个元素的前缀和为 0 // sums[1] = A[0] 前 1 个元素的前缀和为 A[0] // 以此类推 for (int i = 1; i <= n; i++) { sums[i] = sums[i - 1] + nums[i - 1]; } for (int i = 1; i <= n; i++) { int sum = target + sums[i - 1]; int bound = Arrays.binarySearch(sums, sum); if (bound < 0) { bound = -bound - 1; } if (bound <= n) { ans = Math.min(ans, bound - (i - 1)); } } return ans == Integer.MAX_VALUE ? 0 : ans; } }
四、螺旋矩阵
题目链接:54.螺旋矩阵
/** * <pre> * <p>每一个循环处理一圈的数据,之后圈不停向内缩小</p> * </pre> * * @author <a href="https://github.com/kil1ua">Ken-Chy129</a> * @date 2023/1/4 15:48 */ public class 螺旋矩阵54 { public List<Integer> spiralOrder(int[][] matrix) { List<Integer> res = new ArrayList<>(); int left = 0, right = matrix[0].length - 1, top = 0, bottom = matrix.length - 1; while (left <= right && top <= bottom) { for (int column=left; column<=right; column++) { res.add(matrix[top][column]); } for (int row=top+1; row<=bottom; row++) { res.add(matrix[row][right]); } if (left<right && top<bottom) { // 避免剩下一行多列或一列多行时额外输出(因为这两种情况在上面已经输出了) for (int column=right-1; column>left; column--) { res.add(matrix[bottom][column]); } for (int row=bottom; row>top; row--) { res.add(matrix[row][left]); } } left++; right--; top++; bottom--; } return res; } }
五、螺旋矩阵II
题目链接:59.螺旋矩阵II
/** * <pre> * <p>做法与逻辑矩阵相同</p> * </pre> * * @author <a href="https://github.com/kil1ua">Ken-Chy129</a> * @date 2023/1/4 16:22 */ public class 螺旋矩阵II59 { public int[][] generateMatrix(int n) { int left = 0, right = n-1, top = 0, bottom = n-1, value = 1; int[][] res = new int[n][n]; while (left <= right && top <= bottom) { for (int col=left; col<=right; col++) { res[top][col] = value++; } for (int row=top+1; row<=bottom; row++) { res[row][right] = value++; } if (left<=right && top<=bottom) { for (int col=right-1; col>=left; col--) { res[bottom][col] = value++; } for (int row=bottom-1; row>top; row--) { res[row][left] = value++; } } left++; right--; top++; bottom--; } return res; } }