爱喝冬瓜汤的萝卜

导航

滑动窗口算法

滑动窗口算法

名称

滑动窗口算法

场景

当遇到找出序列中连续子序列满足某些条件的问题的时候,可以使用滑动窗口. 序列包括:数组,字符串,List,链表等.
当我们遇到了求满足目标的连续子序列的时候,第一直觉适用滑动子窗口

解决方案

滑动窗口模板:

//创建双指针
int left= start;
int right= start;

//右指针移动
while(right < end){

 将rank=right位置的数据添加到窗口中.
 
 while(!窗口不合规){
  // 判断是否满足条件,更新结果.
    left++;
 }
 
 if(窗口满足最终条件){
   更新最终结果.
   刷新窗口.
 }
 right++;
  
}

返回结果.


效果

满足某个要求的连续子串普通的实现至少是 O(n^2).但是适用滑动窗口能够将问题复杂度降低到 O(n).

实例

问题一 剑指 Offer II 008. 和大于等于 target 的最短子数组

给定一个含有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复杂度的算法.这个时候适用累加和,然后用二分查找累加和.

问题二 剑指 Offer II 009. 乘积小于 K 的子数组

给定一个正整数数组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;
    }
}

问题三 剑指 Offer II 014. 字符串中的变位词

给定两个字符串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]移出窗口直到窗口合规,然后校验最终结果是否符合条件.
这题是标准的模板实现.

问题四 剑指 Offer II 017. 含有所有字符的最短字符串

给定两个字符串 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.循环第一步

问题四 剑指 Offer II 057. 值和下标之差都在给定的范围内

给你一个整数数组 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将窗口数据添加进来,便于处理

posted on 2022-06-25 19:51  爱喝冬瓜汤的萝卜  阅读(135)  评论(0编辑  收藏  举报