优选算法 100 题 —— 2 滑动窗口
前一篇的链接:优选算法100 题 ——1 双指针
2.滑动窗口
2.1 长度最小的子数组
题目链接: 209. 长度最小的子数组
解法一:暴力枚举
两个指针定一移一,遍历完数组。
在暴力解法的基础上进行优化,可以得出解法二:滑动窗口算法。
解法二:利用单调性,使用“同向双指针”
关于 滑动窗口 五个点
-
是什么? ——同向双指针
-
在哪用?——暴力解法中存在单调性时。
-
怎么用?—— 1. left = 0,right = 0 2. 进窗口 3. 判断 4. 出窗口 5. 更新结果。
-
正确性——利用单调性,规避了多余枚举
-
时间复杂度—— O(n)O(n)O(n)
class Solution {public int minSubArrayLen(int target, int[] nums) {int n = nums.length, len = Integer.MAX_VALUE, sum = 0;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;}
}
2.2 无重复字符的最长子串
题目链接: 3. 无重复字符的最长子串
仍是在暴力枚举基础上进行改进
class Solution {public int lengthOfLongestSubstring(String s) {int[] hash = new int[128];int n = s.length(), len = 0, left = 0, right = 0;while (right < n) {hash[s.charAt(right)]++;while(hash[s.charAt(right)] > 1) {hash[s.charAt(left++)]--;}len = Math.max(len, right - left + 1);right++;}return len;}
}
这里用 用 int 数组充当 hash表 判断是否有重复元素。
2.3 最大连续1的个数 Ⅲ
题目链接: 1004. 最大连续1的个数 Ⅲ
class Solution {public int longestOnes(int[] nums, int k) {int left = 0, right = 0, len = 0, count = 0 , n = nums.length;while (right < n) {if (nums[right] == 0) {count++;}while (count > k) {if (nums[left++] == 0) count--;}len = Math.max(len, right - left + 1);right++;}return len;}
}
不翻转,只看 指针圈出的元素中零的个数。当 0 的个数超过 k 时,移动定指针,使 0 的个数变到合理范围内。
2.4 将 x 减到 0 的最小操作数
题目链接:1658. 将 x 减到 0 的最小操作数
思想:正难则反
观察题目,发现满足条件的数可能在最左边,或者最右边,甚至两边都有。从看到题的第一印象,正着找这些数的个数,很复杂。但是反过来,会发现那些不满足的数,总是连续的一组,找到他们的长度,用总长度减去,即得所求。
那么这道题就变成了,找出最长子数组的长度,是该数组所有元素之和等于 原数组总和 - x
class Solution {public int minOperations(int[] nums, int x) {int sum = 0;for (int num : nums) {sum += num;}int target = sum - x, len = -1, n = nums.length; if (target < 0 || target > sum) return -1;sum = 0;for (int left = 0, right = 0; right < n; right++) {sum += nums[right];while (sum > target) {sum -= nums[left++];}if (sum == target) {len = Math.max(len, right - left + 1);};}return len == -1 ? -1 : n - len;}
}
2.5 水果成篮
题目链接:904. 水果成篮
总结题目要求:找到只含有两个不同元素的最长子数组的长度。
解法一:暴力枚举 + 哈希 哈希用来,记录种类
解法二:滑动窗口 + 哈希
直接用 hash 表
class Solution {public int totalFruit(int[] fruits) {Map<Integer,Integer> map = new HashMap<>();int n = fruits.length, left = 0, right = 0, len = 0;while (right < n) {map.put(fruits[right], map.getOrDefault(fruits[right],0) + 1);while (map.size() > 2) {map.put(fruits[left],map.get(fruits[left]) - 1);if (map.get(fruits[left]) == 0) {map.remove(fruits[left]);}left++;}len = Math.max(len, right - left + 1);right++;}return len;}
}
数组模拟 hash
class Solution {public int totalFruit(int[] fruits) {int n = fruits.length, left = 0, right = 0, len = 0, kinds = 0;int[] hash = new int[n + 1];while (right < n) {if (hash[fruits[right]] == 0) kinds++;hash[fruits[right]]++;while (kinds > 2) {hash[fruits[left]]--;if (hash[fruits[left]] == 0) {kinds--;}left++;}len = Math.max(len, right - left + 1);right++;}return len;}
}
2.6 找到字符串中所有字母异位词
题目链接:438. 找到字符串中所有字母异位词
即在 s 中找到包含 p 中所有元素的区域(可以顺序不同)
通过使用 hash 表可以快速判断是否有 异位词。
解法:滑动窗口 + hash
class Solution {public List<Integer> findAnagrams(String s, String p) {int np = p.length(), ns = s.length();int[] hash = new int[26], hash2 = new int[26] ;List<Integer> ret = new ArrayList<>();for (int i = 0; i < np; i++) {hash[p.charAt(i) - 'a']++;}for (int left = 0, right = 0; right < ns; right++) {char in = s.charAt(right);hash2[s.charAt(right) - 'a']++;if (right - left + 1 > np) {hash2[s.charAt(left++) - 'a']--;}if (check(hash,hash2)) {ret.add(left);}}return ret;}public boolean check (int[] hash, int[] hash2) {for (int i = 0; i < 26; i++) {if (hash2[i] != hash[i]) {return false;}}return true;}
}
由于这里 “窗口” 在滑动中的大小是定长的,为 p.length()。所以不用 while 一直移动,用 if 即可。
优化:引入一个 count 变量
使用 count 变量,来记录窗口中 “有效字符的个数” 。有效,即 该元素是否在 p 中,以及在 hash2 中该元素的个数是否符合 p 中的个数。
举个例子:s: abaaccddjlkjlk,p: baad。 此时 left 指在 s 的 1 (从 0 开始),right 指在 4 。那么一共有 3 个有效字符。即 ” b a a“
只要 count == p.length() 时,即为异位了字符串 p。
class Solution {public List<Integer> findAnagrams(String s, String p) {int np = p.length(), ns = s.length(), count = 0;int[] hash = new int[26], hash2 = new int[26] ;List<Integer> ret = new ArrayList<>();for (int i = 0; i < np; i++) {hash[p.charAt(i) - 'a']++;}for (int left = 0, right = 0; right < ns; right++) {int in = s.charAt(right) - 'a';if (hash2[in]++ < hash[in]) {count++;};if (right - left + 1 > np) {int out = s.charAt(left++) - 'a';if (hash2[out]-- <= hash[out]) {count--;}}if (count == np) {ret.add(left);}}return ret;}}
2.7 串联所有单词的子串
题目链接:30. 串联所有单词的子串
这道题同前一道题,原理相同,前一题是一个单词,这里是一个字符串。
如上图所示,为完全遍历。所以 left 要在不同的起始位置。从 0 到 word[0].length()
class Solution {public List<Integer> findSubstring(String s, String[] words) {List<Integer> list = new ArrayList<>();int footlen = words[0].length();int len = words.length, slen = s.length();Map<String, Integer> map = new HashMap<>();for (String str : words) {map.put(str,map.getOrDefault(str, 0) + 1);}for (int i = 0; i < footlen; i++) {Map<String, Integer> map2 = new HashMap<>(); int count = 0;int left = i, right = i;while (right + footlen <= slen) {String in = s.substring(right, right + footlen);map2.put(in, map2.getOrDefault(in,0) + 1);if (map2.get(in) <= map.getOrDefault(in, 0)) {count++;}if (right - left + 1 > len * footlen) {String out = s.substring(left, left + footlen); if (map2.get(out) <= map.getOrDefault(out, 0)) {count--;}map2.put(out, map2.get(out) - 1);left += footlen;}if (count == len) {list.add(left);}right += footlen;}}return list;}
}
2.8 最小覆盖字串
76. 最小覆盖子串
方法一: 暴力枚举。
从第一个字母开始,找到含子串的字符串。记录起始位置及长度。再从第二个字符开始,找到含子串的字符串,若长度小于已记录的则更新长度,起始位置。
方法二:滑动窗口。
为不失一般性,使用一条线代表 s 字符串。
当移动到【left,right】划定的字符串中含有子串后,若继续右移right,进入窗口,那么长度变大,与所求相悖。因此,向右移动 left,进行出窗口操作。若【left,right】划定范围仍满足要求则 更新 起始位置和长度。当 不满足要求时,移动 right 进窗口,至符合要求或到达 s 字符串尾。
优化:假定使用 map 记录,字符出现的次数。但是这样在判定是否含有子串时,要对 t 的 map 和 【left,right】的 map 一一取出再比较。这样很浪费时间。这里我们引入一个 count 变量 来记录 【left,right】中合法字符的种类(某一字符数量等于 t 中的数量则 count++) (不是记个数的原因:主要是 eg:t: abcd,数量为 4,【left,right】:aaaa,数量同样为 4,且 a 是 t 中字符。)
当 count == tmap.size() 时说明符合要求了。
class Solution {public String minWindow(String s, String t) {int slen = s.length(), tlen = t.length();if (slen < tlen) {return "";}Map<Character, Integer> tmap = new HashMap<>();for (char c : t.toCharArray()) {tmap.put(c, tmap.getOrDefault(c, 0) + 1);}Map<Character, Integer> smap = new HashMap<>();int left = 0, right = 0, begin = -1, count = 0, minLen = Integer.MAX_VALUE;while (right < slen) {char c = s.charAt(right);// 仅处理 t 中存在的字符smap.put(c, smap.getOrDefault(c, 0) + 1);// 当该字符的数量满足 t 中的要求时,count 加 1if (smap.get(c).equals(tmap.getOrDefault(c,0))) {count++;}// 当窗口包含 t 中所有字符时,尝试收缩左指针while (count == tmap.size() && left <= right) {// 更新最小窗口if (right - left + 1 < minLen) {begin = left;minLen = right - left + 1;}char leftChar = s.charAt(left);// 处理左指针指向的字符(若在 t 中)smap.put(leftChar, smap.get(leftChar) - 1);// 若数量不满足 t 的要求,count 减 1(退出收缩循环)if (smap.getOrDefault(leftChar,0) < tmap.getOrDefault(leftChar,0)) {count--;}left++; // 移动左指针}right++; // 移动右指针}return begin == -1 ? "" : s.substring(begin, begin + minLen);}
}
再次优化,据题目可知 s,t 中全部是英文字母,所以可以用 int【128】 的数组来代替 map。
class Solution {public String minWindow(String ss, String tt) {char[] s = ss.toCharArray();char[] t = tt.toCharArray();int[] hasht = new int[128];int category = 0;for (char ret : t) {if (hasht[ret]++ == 0)category++;}int begin = -1, min = Integer.MAX_VALUE;int[] hashs = new int[128];for (int right = 0, left = 0, count = 0; right < s.length; right++) {char in = s[right];if (++hashs[in] == hasht[in])count++;while (count == category) {if (right - left + 1 < min) {begin = left;min = right - left + 1;}char out = s[left++];if(hashs[out]-- == hasht[out]) count--;}}return begin == -1 ? "" : ss.substring(begin, begin+min);}
}