滑动窗口算法
当遇到找出序列中连续子序列满足某些条件的问题的时候,可以使用滑动窗口. 序列包括:数组,字符串,List,链表等.
当我们遇到了求满足目标的连续子序列的时候,第一直觉适用滑动子窗口
滑动窗口模板:
//创建双指针 int left= start; int right= start; //右指针移动 while(right < end){ 将rank=right位置的数据添加到窗口中. while(!窗口不合规){ // 判断是否满足条件,更新结果. left++; } if(窗口满足最终条件){ 更新最终结果. 刷新窗口. } right++; } 返回结果.
满足某个要求的连续子串普通的实现至少是 O(n^2).但是适用滑动窗口能够将问题复杂度降低到 O(n).
给定一个含有n个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组[numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
给定一个含有n个正整数的数组和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组[numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组[4,3]是该条件下的长度最小的子数组。
例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 105
进阶:
如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。
class Solution { public int minSubArrayLen(int target, int[] nums) { /** * 7 * [2,3,1,2,4,3] * * 给定一个数组,正整数,找出最短的连续子数组,求出长度最小的连续子序列. * 满足条件是 子序列的和 >=target * 求出长度最短的那个 */ if (nums.length == 0) { return 0; } int left = 0, right = 0; int minLength = Integer.MAX_VALUE; int sum = 0; while (right < nums.length) { sum += nums[right]; //[left,right] while (sum >= target) { minLength = Math.min(minLength, (right - left + 1)); sum -= nums[left]; left++; } right++; } return minLength == Integer.MAX_VALUE ? 0 : minLength; } }
如果使用 nlogn复杂度的算法.这个时候适用累加和,然后用二分查找累加和.
给定一个正整数数组nums和整数 k,请找出该数组内乘积小于k的连续的子数组的个数。 示例 1: 输入: nums = [10,5,2,6], k = 100 输出: 8 解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。 需要注意的是 [10,5,2] 并不是乘积小于100的子数组。 示例 2: 输入: nums = [1,2,3], k = 0 输出: 0 提示: 1 <= nums.length <= 3 * 104 1 <= nums[i] <= 1000 0 <= k <= 106
class Solution { public int numSubarrayProductLessThanK(int[] nums, int k) { /** * 找出所有的连续子数组,其乘积 < target元素 */ int left = 0, right = 0; int result = 1; int count = 0; while (right < nums.length) { result *= nums[right]; while (left <= right && result >= k) { result /= nums[left]; left++; } //当前满足了 以nums[right]结尾的子序列的乘积 < k //这里 count += right - left +1 的思路是 以当前right结尾的所有的子序列. // 其中 左区间可以是 [right,left].所以是 额外添加的元素是 (right -left +1) count += right - left + 1; right++; } return count; } }
给定两个字符串s1和s2,写一个函数来判断 s2 是否包含 s1的某个变位词。 换句话说,第一个字符串的排列之一是第二个字符串的 子串 。 示例 1: 输入: s1 = "ab" s2 = "eidbaooo" 输出: True 解释: s2 包含 s1 的排列之一 ("ba"). 示例 2: 输入: s1= "ab" s2 = "eidboaoo" 输出: False 提示: 1 <= s1.length, s2.length <= 104 s1 和 s2 仅包含小写字母
class Solution { public boolean checkInclusion(String s1, String s2) { /** * s2是否包括s1的排列 */ int left = 0, right = 0; int[] chCount = new int[26]; for (int i = 0; i < s1.length(); i++) { char ch = s1.charAt(i); chCount[ch - 'a']++; } while (right < s2.length()) { int rightRank = s2.charAt(right) - 'a'; chCount[rightRank]--; while (chCount[rightRank] < 0) { int leftRank = s2.charAt(left) - 'a'; chCount[leftRank]++; left++; } if (right - left + 1 == s1.length()) { return true; } right++; } return false; } }
这个题首先不断的nums[right]添加窗口并统计计数.依次将nums[left]移出窗口直到窗口合规,然后校验最终结果是否符合条件.
这题是标准的模板实现.
给定两个字符串 s 和t 。返回 s 中包含t的所有字符的最短子字符串。如果 s 中不存在符合条件的子字符串,则返回空字符串 "" 。 如果 s 中存在多个符合条件的子字符串,返回任意一个。 注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。 示例 1: 输入:s = "ADOBECODEBANC", t = "ABC" 输出:"BANC" 解释:最短子字符串 "BANC" 包含了字符串 t 的所有字符 'A'、'B'、'C' 示例 2: 输入:s = "a", t = "a" 输出:"a" 示例 3: 输入:s = "a", t = "aa" 输出:"" 解释:t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。 提示: 1 <= s.length, t.length <= 105 s 和 t 由英文字母组成 进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗?
class Solution { public String minWindow(String s, String t) { /** * 这个题使用滑动窗口,首先适用右侧进行滑动,然后不断判断[left,right]是否满足了条件所有的字母个数都满足了条件.然后移动左侧.知道不满足为止.然后获取[left-1,right]为一个符合条件的字符串. * 然后右侧继续移动,知道满足条件为止. */ Map<Character, Integer> targetChCountMap = new HashMap<>(); for (int i = 0; i < t.length(); i++) { char ch = t.charAt(i); targetChCountMap.put(t.charAt(i), targetChCountMap.getOrDefault(ch, 0) + 1); } Map<Character, Integer> chCountMap = new HashMap<>(); int left = 0, right = 0; boolean isLegal = false; String minLenStr = null; while (right < s.length()) { if (!isLegal) { char rightCh = s.charAt(right); chCountMap.put(rightCh, chCountMap.getOrDefault(rightCh, 0) + 1); isLegal = isLegalStr(chCountMap, targetChCountMap); //right指向了下一个位置 right++; } while (left <= right && isLegal) { char leftCh = s.charAt(left); chCountMap.put(leftCh, chCountMap.get(leftCh) - 1); isLegal = isLegalStr(chCountMap, targetChCountMap); if (!isLegal) { String targetStr = s.substring(left, right); if (minLenStr == null || minLenStr.length() > targetStr.length()) { minLenStr = targetStr; } } left++; } } return minLenStr == null ? "" : minLenStr; } private boolean isLegalStr(Map<Character, Integer> chCountMap, Map<Character, Integer> targetChCountMap) { for (Map.Entry<Character, Integer> entry : targetChCountMap.entrySet()) { Integer chCount = chCountMap.get(entry.getKey()); if (chCount == null || chCount < entry.getValue()) { return false; } } return true; } }
求cover了包含 t字符串的子串.这个题是这样做的:
1.将nums[right]不断添加到窗口中.直到满足条件
2.然后左移窗口,直到不满足条件来压缩窗口长度,找出最小长度的窗口
3.计算最终结果.
4.循环第一步
给你一个整数数组 nums 和两个整数k 和 t 。请你判断是否存在 两个不同下标 i 和 j,使得abs(nums[i] - nums[j]) <= t ,同时又满足 abs(i - j) <= k 。 如果存在则返回 true,不存在返回 false。 示例1: 输入:nums = [1,2,3,1], k = 3, t = 0 输出:true 示例 2: 输入:nums = [1,0,1,1], k = 1, t = 2 输出:true 示例 3: 输入:nums = [1,5,9,1,5,9], k = 2, t = 3 输出:false 提示: 0 <= nums.length <= 2 * 104 -231 <= nums[i] <= 231 - 1 0 <= k <= 104 0 <= t <= 231 - 1
class Solution { public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) { /** * 这个题是:给定一个长度为 k的滑动窗口. 找出 最后一个元素和之前的元素的差值在 [nums[i]-t,nums[i]+t]. * */ TreeSet<Long> window = new TreeSet<>(); for (int i = 0; i < nums.length; i++) { //从window中找出一个 介于 [nums[i] - t , nums[i] +t]的元素.如果有就返回. Long ceiling = window.ceiling((long) nums[i] - (long) t); //找出 第一个 ceiling >= nums[i] -t的元素 //判断这个元素是否 <= nums[i] + t if (ceiling != null && ceiling <= (long) nums[i] + (long)t) { //如果满足条件就返回false return true; } window.add((long) nums[i]); if (i >= k) { window.remove((long)nums[i - k]); } } return false; } }
这里的区间有点不同,窗口不仅仅使用的是 nums[left,right],同时使用一个TreeSet将窗口数据添加进来,便于处理