【专题二】滑动窗口

57 阅读6分钟

前言

利用滑动窗口之前,该代码都是可以用暴力枚举进行实现的

left,right 双指针是具有单调性的 - 就是 left,right 走过的元素不会走第二遍

基本步骤

image.png

209. 长度最小的子数组

题目描述

image.png

解题思路

  1. 暴力枚举,枚举出所有的子数组,然后找到符合条件的最小长度子数组,时间复杂度 O(N*N)
  2. 滑动窗口:题目说了都是正整数,那么就是具有单调性的,如:a[0]+a[1] >= target ==> a[0]+a[1]+a[2] 一定大于target的,这时候我们就不用着急算a[2]了,我们可以先看a[1] 是否 >= target 时间复杂度:O(n)

代码实现

// 暴力
public int minSubArrayLen1(int target, int[] nums) {
    int ret = Integer.MAX_VALUE;
    int n = nums.length;
    for (int i = 0; i < n; i++) {
        int sum = 0;
        for (int j = i; j < n; j++) {
            sum += nums[j];
            if(sum >= target) {
                ret = Math.min(ret,j-i+1);
                break;
            }
        }
    }

    return ret == Integer.MAX_VALUE ? 0 : ret;
}
// 滑动窗口:[left,right] 维护一个区间,如果该区间内的元素 >= target,那么right如果往前退的话,[left,right-1] 该区间right不管怎么动都 < target了
public int minSubArrayLen(int target, int[] nums) {
    int ret = Integer.MAX_VALUE;
    int n = nums.length;
    int sum = 0;
    for (int right = 0,left = 0; right < n; right++) {
        sum += nums[right];
        while (sum >= target) {
            ret = Math.min(ret,right - left + 1);
            sum -= nums[left++];
        }
    }

    return ret == Integer.MAX_VALUE ? 0 : ret;
}

3. 无重复字符的最长子串

题目描述

image.png

解题思路

滑动窗口:借助一个整型数组,来标记当前遍历过的字符出现次数,如果发现当前次数 > 1,就表示区间存在重复元素了,就需要去重了【内部遍历 - 一直到当前元素次数出现 <= 1 为止】,然后就可以统计区间长度了

代码实现

public int lengthOfLongestSubstring(String ss) {
    char[] s = ss.toCharArray();
    int[] hash = new int[127];
    int n = ss.length();
    int ret = 0;
    for (int right = 0,left = 0; right < n; right++) {
        hash[s[right]]++;
        while (hash[s[right]] > 1) {
            hash[s[left++]]--;
        }
        ret = Math.max(ret,right-left+1);
    }

    return ret;
}

1004. 最大连续1的个数 III

题目描述

image.png

解题思路

滑动窗口:先用一个变量存储 0 再遍历区间出现的个数,当区间0的个数 > k 的时候,就需要去除区间第一个出现的0,然后计算区间长度了【遍历】

代码实现

public int longestOnes(int[] nums, int k) {
    int n = nums.length;
    int ret = 0;
    for (int right = 0,left = 0, zero = 0; right < n; right++) {
        if(nums[right] == 0) {
            zero++;
        }
        while (zero > k) {
            if(nums[left] == 0) {
                zero--;
            }
            left++;
        }

        ret = Math.max(ret,right-left+1);
    }

    return ret;
}

1658. 将 x 减到 0 的最小操作数

题目描述

image.png

解题思路

滑动窗口:类似于最上面的第一题,题目最终的意思就是:x - 左边n个连续数 - 右边n个连续数 = 0;那么我们可以利用滑动窗口求出:中间未减去的数字的个数len,然后最终返回 nums.length - len

代码实现

public int minOperations(int[] nums, int x) {
    int len = -1;
    int n = nums.length;

    int sum = 0;
    for(int num : nums) sum += num;
    sum -= x;
    if(sum < 0) return -1;

    int tmp = 0;
    for (int right = 0,left = 0; right < n; right++) {
        tmp += nums[right];
        while (tmp > sum) {
            tmp -= nums[left++];
        }

        if(tmp == sum) {
            len = Math.max(len,right - left + 1);
        }
    }

    return len == -1 ? -1 : n - len;
}

904. 水果成篮

题目描述

image.png

解题思路

滑动窗口:题目最终的意思(只存在两个数的最长子数组) - 借助map来进行实现

代码实现

// 存储种类,个数
HashMap<Integer,Integer> hash = new HashMap<>();
int ret = 0;
for (int right = 0, left = 0; right < fruits.length; right++) {
    // 进窗口
    int in = fruits[right];
    hash.put(in,hash.getOrDefault(in,0) + 1);

    // 判断
    while (hash.size() > 2) {
        // 出窗口
        int out = fruits[left++];
        hash.put(out,hash.get(out) - 1);
        if(hash.get(out) == 0) {
            hash.remove(out);
        }
    }

    ret = Math.max(ret,right-left+1);
}

return ret;

438. 找到字符串中所有字母异位词

题目描述

image.png

解题思路

如何判断异位词:1. 两边排序,然后逐个而比较;2. 我们直接比较字符的个数(我们用这个)

  1. 我们先统计 p 中每个字符的个数
  2. 滑动窗口:遍历 s 的区间大于 p 的时候,区间头部字符的个数就--
  3. 如果判断出是异位词,就存储区间开头下标

代码实现

public List<Integer> findAnagrams(String s, String p) {
    List<Integer> ret = new ArrayList<>();
    // 1. 先统计p的每个字符的个数
    int[] hash1 = new int[26];
    int m = p.length();
    for (int i = 0; i < m; i++) {
        char ch = p.charAt(i);
        hash1[ch - 'a']++;
    }

    int[] hash2 = new int[26];
    int n = s.length();
    for (int right = 0, left = 0; right < n; right++) {
        // 进窗口
        char in = s.charAt(right);
        hash2[in - 'a']++;
        if(right - left + 1 > m) {
            // 出窗口
            char out = s.charAt(left);
            hash2[out - 'a']--;
            left++;
        }

        // 更新结果
        if(check(hash1,hash2)) {
            ret.add(left);
        }
    }

    return ret;
}

private boolean check(int[] hash1, int[] hash2) {
    for (int i = 0; i < 26; i++) {
        if(hash1[i] != hash2[i]) {
            return false;
        }
    }

    return true;
}

上面的解法:我们再判断是否是字母异位词的时候,需要遍历存储个数的数组进行判断,时间复杂度比较高了,而且这里是每个字符,如果 p 是字符串数组的话,这个方法就不合适了,下面代码就是针对判断异位词的优化【引入了count遍历,只存储有效字符个数来进行判断是否是异位词】

public List<Integer> findAnagrams(String s, String p) {
    List<Integer> ret = new ArrayList<>();
    // 1. 先统计p的每个字符的个数
    int[] hash1 = new int[26];
    int m = p.length();
    for (int i = 0; i < m; i++) {
        char ch = p.charAt(i);
        hash1[ch - 'a']++;
    }

    int[] hash2 = new int[26];
    int count = 0; // 维护有效字符的个数
    int n = s.length();
    for (int right = 0, left = 0; right < n; right++) {
        // 进窗口
        char in = s.charAt(right);
        hash2[in - 'a']++;
        if(hash2[in  - 'a'] <= hash1[in - 'a']) {
            count++;
        }

        if(right - left + 1 > m) {
            // 出窗口
            char out = s.charAt(left);
            if(hash2[out  - 'a'] <= hash1[out - 'a']) {
                count--;
            }
            hash2[out - 'a']--;
            left++;
        }

        // 更新结果
        if(count == m) {
            ret.add(left);
        }
    }

    return ret;
}

30. 串联所有单词的子串

题目描述

该题相当于上面题目的升级版

注意题目:words 中每个单词长度相同

image.png

解题思路

这是不是就是438. 找到字符串中所有字母异位词 image.png

与 438题的不同点: image.png

代码实现

public List<Integer> findSubstring(String s, String[] words) {
    List<Integer> ret = new ArrayList<>();
    // 存储 words 中字符串对应的个数
    HashMap<String,Integer> hash1 = new HashMap<>();
    for(String word : words) {
        hash1.put(word,hash1.getOrDefault(word,0)+1);
    }

    int len = words[0].length();
    int m = words.length;  // m个单词
    // 滑动窗口执行次数
    for (int i = 0; i < len; i++) {
        // 窗口内所有单词的频次
        HashMap<String,Integer> hash2 = new HashMap<>();
        // count 存储有效字符串的个数
        for (int left = i, right = i, count = 0; right + len <= s.length(); right += len) {
            // 进窗口 + 维护count
            String in = s.substring(right,right + len);
            hash2.put(in,hash2.getOrDefault(in,0)+1);
            if(hash2.get(in) <= hash1.getOrDefault(in,0)) {
                count++;
            }
            // 判断 区间字符超过words长度,就需要出窗口了
            if(right - left + 1 > len * m) {
                // 出窗口 + 维护count
                String out = s.substring(left,left+len);
                // 如果出的是有效字符串,count--
                if(hash2.get(out) <= hash1.getOrDefault(out,0)) {
                    count--;
                }
                hash2.put(out,hash2.get(out)-1);
                left += len;
            }
            // 更新结果
            if(m == count) {
                ret.add(left);
            }
        }
    }

    return ret;
}

76. 最小覆盖子串

题目描述

image.png

解题思路

这里还是引入count遍历,但是此时count就不是表示有效字符的个数了,而是有效字符的类别数(个数:A2,B1;A1,B2 那么count就都是3,就无法区分了,但是类别只有两类,这个就好区分)

代码实现

public String minWindow(String s, String t) { // 1. 先统计p的每个字符的个数 int[] hash1 = new int[128]; int m = t.length(); int kinds = 0; for (int i = 0; i < m; i++) { char ch = t.charAt(i); if(hash1[ch]++ == 0) kinds++; }

    int[] hash2 = new int[128];
    int begin = -1;
    int count = 0; // 维护有效字符的个数
    int n = s.length();
    int minLen = Integer.MAX_VALUE;
    for (int right = 0, left = 0; right < n; right++) {
        // 进窗口
        char in = s.charAt(right);
        hash2[in]++;
        if(hash2[in] == hash1[in]) {
            count++;
        }

        while (kinds == count) {
            if(right - left + 1 < minLen) {
                minLen = right - left + 1;
                begin = left;
            }
            // 出窗口
            char out = s.charAt(left);
            if(hash2[out] == hash1[out]) {
                count--;
            }
            hash2[out]--;
            left++;
        }
    }
    if(begin == -1) return new String();
    else return s.substring(begin, begin + minLen);
}