赞
踩
滑动窗口可以理解为双指针的一种,左窗口l
,右窗口r
。
一般,右窗口r
先行,达到阶段性分界点(题目要求条件或者限制),开始移动左窗口l
。我们要在移动的过程中统计题目的答案。
l
, 内循环移动r
统计最大值, 遇到分界条件后, 移动l
后立即继续持续移动r
统计最大值;for(int left = 0, right = 0; left < len; left++) {
while(right < len && right条件) {
...
right++;
}
...
}
r
, 内循环移动l
, 遇到分解条件后, 持续移动l
统计最小值;for(int left = 0, right = 0; right < len; right++) {
...
// 判断是否满足条件,满足条件后开始回缩left;
if(条件) {
while(left < len && left条件) {
...
left++;
}
}
}
求最小值同样也可以外循环移动l
,内循环移动r
。每一个left,都通过right遍历探索条件,满足条件后,再收缩left。
for(int left = 0, right = 0; left < len; left++) {
while(right < len && right条件) {
...
right++;
}
...
// 回缩left
while(条件) {
left++;
}
}
class Solution { public int minSubArrayLen(int target, int[] nums) { // 当前遍历的子数组总和 int sum = 0; // 最小子数组的长度 int min = Integer.MAX_VALUE; // 右窗口先行滑动 for(int l = 0, r = 0; r < nums.length; r++) { // 遍历的元素加入子数组 sum += nums[r]; // 达到题目条件,子数组总和sum>target,记录最小子数组长度,并开始移动左窗口,探索更小的子数组。 while(sum >= target) { min = Math.min(min, r - l + 1); // 左窗口移动,移除子数组中起始的元素。 sum -= nums[l++]; } } // 如果未有满足条件的子数组,min应该未改变过,返回题目要求0。 return min == Integer.MAX_VALUE? 0 : min; } }
class Solution { // 滑动窗口 // 这道题目求的是最大值 public int lengthOfLongestSubstring(String s) { // 辅助Set,判断当前遍历元素是否重复 Set<Character> set = new HashSet<>(); int max = 0; // 这里同样是先行移动右窗口,右窗口的移动是在内部循环,左窗口的移动在外循环 for(int l = 0, r = 0; l < s.length(); l++) { // 先行移动右窗口,直到遇到子数组中重复元素或越界。 while(r < s.length() && !set.contains(s.charAt(r))) { // 加入遍历集合 set.add(s.charAt(r)); // 计算最大值 max = Math.max(max, r - l + 1); r++; } // 右窗口遇到分界条件后,开始移动左窗口,但是因为探索的是最大值,我们将左窗口移动一次后,继续探索右窗口,而不是持续的移动左窗口缩小边界; set.remove(s.charAt(l)); } return max; } }
这里给了两种写法:
1.外循环left,内循环right
class Solution { /** * 滑动窗口 * 外循环left,内循环right。每到一个新left,开始向右遍历right,直到满足覆盖条件。再右移一次left,判断是否覆盖,若不覆盖继续开始向右循环right。 * 通过一个HashMap与目标字符串长度,共同记录判断是否满足覆盖子串条件 * @param s * @param t * @return */ public String minWindow(String s, String t) { Map<Character, Integer> tarMap = new HashMap<>(); for(int i = 0; i < t.length(); i++) { tarMap.put(t.charAt(i), tarMap.getOrDefault(t.charAt(i), 0) + 1); } int right = 0; // HashMap记录目标字符出现的次数,tarCount记录字符总数。当字符总数为0时,即为覆盖子串。 int tarCount = t.length(); String res = ""; // 外循环left for(int left = 0; left < s.length(); left++) { // 内循环right // 每一个left都判断当前是否覆盖子串,若不覆盖则向右遍历right,直到覆盖。 while(right < s.length() && tarCount > 0) { // 当前right指向字符是否为目标字符 if(tarMap.containsKey(s.charAt(right))) { // 若为目标字符,当前出现次数是否超过目标次数 if(tarMap.get(s.charAt(right)) > 0) { // 若没超过目标次数,则目标总数-1; tarCount -= 1; } // 目标字符出现次数 - 1; tarMap.put(s.charAt(right), tarMap.get(s.charAt(right)) - 1); } // right右移 right++; } // 若right已经出界且目标总数没有达到,则break退出。 if(right == s.length() && tarCount > 0) { break; } // 准备右移left if(tarMap.containsKey(s.charAt(left))) { // 若left为目标字符,且右移后目标字符出现次数达不到要求,则判断当前状态是否为最小覆盖范围 if(tarMap.get(s.charAt(left)) + 1 > 0) { if(res == "" || res.length() > right - left) { res = s.substring(left, right); } // 目标总数+1 tarCount++; } // 目标字符出现次数+1 tarMap.put(s.charAt(left), tarMap.get(s.charAt(left)) + 1); } } return res; } }
2.外循环right,内循环left
class Solution { public String minWindow(String s, String t) { Map<Character, Integer> tarMap = new HashMap<>(); for(int i = 0; i < t.length(); i++) { tarMap.put(t.charAt(i), tarMap.getOrDefault(t.charAt(i), 0) + 1); } // HashMap记录目标字符出现的次数,tarCount记录字符总数。当字符总数为0时,即为覆盖子串。 int tarCount = t.length(); String res = ""; // 外循环right for(int left = 0, right = 0; right < s.length(); right++) { if(tarCount > 0) { // 当前right指向字符是否为目标字符 if(tarMap.containsKey(s.charAt(right))) { // 若为目标字符,当前出现次数是否超过目标次数 if(tarMap.get(s.charAt(right)) > 0) { // 若没超过目标次数,则目标总数-1; tarCount -= 1; } // 目标字符出现次数 - 1; tarMap.put(s.charAt(right), tarMap.get(s.charAt(right)) - 1); } // right右移 } else { continue; } while(left <= right && tarCount == 0) { // 准备循环右移left if(tarMap.containsKey(s.charAt(left))) { // 若left为目标字符,且右移后目标字符出现次数达不到要求,则判断当前状态是否为最小覆盖范围 if(tarMap.get(s.charAt(left)) + 1 > 0) { if(res == "" || res.length() > right - left + 1) { res = s.substring(left, right + 1); } // 目标总数+1 tarCount++; } // 目标字符出现次数+1 tarMap.put(s.charAt(left), tarMap.get(s.charAt(left)) + 1); } left++; } } return res; } }
三数之和思想是双指针扩散,但在定义时由于题意是三元组,所以需要定义三个指针分别指向三个元素,一个指针进行遍历,每一次遍历中另外两个指针进行扩散。
双指针扩散思想
为了方便去重的判断,我们首先将元素按序排列。之后,使用三个指针left, middle, right
分别从前向后指向三个元素。left
为遍历的左边界,middle,right
为扩散指针,每轮middle = left + 1, right = numsl.length - 1
,三元组和为sum = nums[left] + nums[middle] + nums[right]
。若当前sum < 0,middle++
; 若当前sum > 0,right--
;两个指针按照sum的值进行循环扩散,直到指针相撞left == right
,且在这期间遇到sum == 0的情况,要记录nums[left], nums[middle], nums[right]
的值。
这里强调三点:
1.每一轮遍历,两个扩散指针都要将边界范围内的值全部覆盖到,直到指针相撞,这是因为要记录所有可能的情况。
2.扩散指针去重:当sum == 0时,要考虑去重,即nums[middle] == nums[middle + 1]
,nums[right] == nums[right - 1]
,将sum == 0的结果保持唯一性。
3.左指针遍历去重:上一个去重是在左边界确定的情况下保证另外两个元组的唯一性,那么左边界值等于上一轮左边界值得情况即nums[left] == nums[left - 1]
,左边界值如果出现过,就无需再进行扩散了,因为这样是重复的扩散。
public List<List<Integer>> threeSum(int[] nums) { // 遍历 + 双指针,left用来遍历和确立左边界,middle和right用来扩散 int left, middle, right; List<List<Integer>> res = new ArrayList<>(); // 先排序 Arrays.sort(nums); // 左边界遍历 for(left = 0; left < nums.length - 2; left++) { // 左边界去重 if(left > 0 && nums[left] == nums[left - 1]) { continue; } // 扩散 middle = left + 1; right = nums.length - 1; // 一定要扩散到两个指针碰撞,即边界内所有元素都判断到 while(middle < right) { int sum = nums[left] + nums[middle] + nums[right]; if(sum < 0) { middle++; } else if(sum > 0) { right--; } else { List<Integer> sub = new ArrayList<>(); sub.add(nums[left]); sub.add(nums[middle]); sub.add(nums[right]); res.add(sub); // sum == 0, 要进行扩散指针的去重 while(middle < right && nums[middle + 1] == nums[middle]) { middle++; } while(middle < right && nums[right - 1] == nums[right]) { right--; } // 重要:记录三元组后,要继续移动双指针,为了覆盖所有的边界元素! // 经过扩散指针的去重操作后,扩散指针指向的元素不存在还有其他匹配的元素了,所以只需继续向内移动指针就好(只移动一个指针也可以,目的就是为了覆盖边界内全部元素). middle++; right--; } } } return res; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。