【滑动窗口】一文吃透滑动窗口算法:从理论到实战,彻底告别低效代码

在这里插入图片描述


前言

什么是滑动窗口算法呢?

滑动窗口算法是一种在数组或字符串等线性数据结构上进行高效数据处理的算法技术。
基本概念
想象有一个固定大小或者大小可变的窗口在数据序列上滑动,通过不断地移动窗口位置来对窗口内的数据进行特定操作和分析,从而解决各种问题。
工作原理
初始化窗口:在数据序列的起始位置创建一个窗口,确定窗口的大小和初始状态。
窗口滑动:按照一定的规则将窗口在数据序列上逐步移动,每次移动可以是一个元素的位置,也可以是多个元素的位置,同时根据窗口的移动更新窗口内的数据统计信息或执行相应的操作。
条件判断:在窗口滑动过程中,根据具体问题的要求,判断窗口是否满足特定条件,如窗口内元素的和达到某个值、窗口内包含特定的元素组合等。
结果记录或更新:当窗口满足条件时,记录下当前窗口的相关信息,如窗口的位置、窗口内的数据等,或者根据问题的要求对结果进行更新。

下面我们将通过例题展开文章,更加深刻吃透滑动窗口这一类算法!

例题

一 、长度最小的子数组

  1. 题目链接:长度最小的子数组

  2. 题目描述:
    给定⼀个含有 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

  3. 解法:滑动窗口:
    算法思路:
    由于此问题分析的对象是「⼀段连续的区间」,因此可以考虑「滑动窗⼝」的思想来解决这道题。 让滑动窗⼝满⾜:从 i 位置开始,窗⼝内所有元素的和⼩于 target ,(那么当窗⼝内元素之和 第⼀次⼤于等于⽬标值的时候,就是 i 位置开始,满⾜条件的最⼩⻓度)。
    做法:将右端元素划⼊窗⼝中,统计出此时窗⼝内元素的和:
    ▪ 如果窗⼝内元素之和⼤于等于 target :更新结果,并且将左端元素划出去的同时继续判 断是否满⾜条件并更新结果(因为左端元素可能很⼩,划出去之后依旧满⾜条件)
    ▪ 如果窗⼝内元素之和不满⾜条件: right++ ,另下⼀个元素进⼊窗⼝。

为何滑动窗口可以解决问题,并且时间复杂度更低?

▪ 这个窗⼝寻找的是:以当前窗⼝最左侧元素(记为 left1 )为基准,符合条件的情况。也 就是在这道题中,从 left1 开始,满⾜区间和 sum >= target 时的最右侧(记为right1)能到哪里。
▪ 我们既然已经找到从 left1 开始的最优的区间,那么就可以⼤胆舍去 left1 。但是如 果继续像⽅法⼀⼀样,重新开始统计第⼆个元素( left2 )往后的和,势必会有⼤量重复 的计算(因为我们在求第⼀段区间的时候,已经算出很多元素的和了,这些和是可以在计算 下次区间和的时候⽤上的)。
▪ 此时, rigth1 的作用就体现出来了,我们只需将 left1 这个值从 sum 中剔除。从right1 这个元素开始,往后找满⾜ left2 元素的区间(此时 right1 也有可能是满足的,因为 left1 可能很小。 sum 剔除掉 left1 之后,依旧满足大于等于target )。这样我们就能省掉⼤量重复的计算。 ▪ 这样我们不仅能解决问题,而且效率也会大大提升。

时间复杂度:虽然代码是两层循环,但是我们的 left 指针和 right 指针都是不回退的,两者 最多都往后移动 n 次。因此时间复杂度是 O(N)。

  1. 代码示例:
 public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        int sum = 0, len = Integer.MAX_VALUE;
        for(int right = 0,left = 0;right<n;right++){
            sum+=nums[right];//进窗口
            while(sum>=target){//判断
                len = Math.min(len,right-left+1);//更新结果
                sum-=nums[left];//出窗口
                left++;
            }
        }
        return len==Integer.MAX_VALUE? 0: len;
    }

二、无重复字符的最长子串

  1. 题⽬链接:无重复字符的最长子串

  2. 题⽬描述:
    给定⼀个字符串 s ,请你找出其中不含有重复字符的最⻓⼦串的⻓度。
    ⽰例 1:
    输⼊: s = “abcabcbb”
    输出: 3 解释: 因为⽆重复字符的最⻓⼦串是 “abc” ,所以其⻓度为 3 。 ⽰例 2:
    输⼊: s = “bbbbb”
    输出: 1 解释: 因为⽆重复字符的最⻓⼦串是 “b” ,所以其⻓度为 1 。 ⽰例 3:
    输⼊: s = “pwwkew”
    输出: 3 解释: 因为⽆重复字符的最⻓⼦串是 “wke” ,所以其⻓度为 3 。 请注意,你的答案必须是 ⼦串 的⻓度, “pwke” 是⼀个⼦序列,不是⼦串。 提⽰: • 0 <= s.length <= 5 * 10^4
    • s 由英⽂字⺟、数字、符号和空格组成

  3. 解法(滑动窗口):

算法思路:

研究的对象依旧是⼀段连续的区间,因此继续使⽤「滑动窗⼝」思想来优化。
让滑动窗⼝满⾜:窗⼝内所有元素都是不重复的。 做法:右端元素 ch 进⼊窗⼝的时候,哈希表统计这个字符的频次:
▪ 如果这个字符出现的频次超过 1 ,说明窗⼝内有重复元素,那么就从左侧开始划出窗⼝, 直到 ch 这个元素的频次变为 1 ,然后再更新结果。
▪ 如果没有超过 1 ,说明当前窗⼝没有重复元素,可以直接更新结果

  1. 代码示例:
  public int lengthOfLongestSubstring(String ss) {
        int n = ss.length();
        char[] s =ss.toCharArray();
        int[] hash =new int [128];//用数组模拟哈希表
        int left = 0,right = 0,ret = 0;
        while(right<n){
           hash[s[right]]++;//进入窗口
           while(hash[s[right]]>1){ //判断
           	hash[s[left++]]--;//出窗口
           }
           ret =  Math.max(ret ,right-left+1);//更新结果
           right++;//让下一个字符进入窗口
        }
        return ret;
    }

三 、最大连续 1 的个数 III

  1. 题目链接: 最大连续 1 的个数 III
  2. 题⽬描述:

给定⼀个⼆进制数组 nums 和⼀个整数 k ,如果可以翻转最多 k 个 0 ,则返回 数组中连续 1 的最⼤个数 。 ⽰例 1:输⼊: nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2
输出: 6
解释:
[1,1,1,0,0,1,1,1,1,1,1]
红⾊数字从 0 翻转到 1 ,最⻓的⼦数组⻓度为 6 。
⽰例 2:输⼊:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3
输出: 10
解释:
[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1]
红色数字从 0 翻转到 1 ,最长的子数组长度为 10 。

  1. 解法(滑动窗⼝):
    算法思路:
    不要去想怎么翻转,不要把问题想的很复杂,这道题的结果⽆⾮就是⼀段连续的 1 中间塞了 k 个 0 嘛。 因此,我们可以把问题转化成:求数组中⼀段最⻓的连续区间,要求这段区间内 0 的个数不超 过 k 个。既然是连续区间,可以考虑使用「滑动窗⼝」来解决问题。

算法流程:

a. 初始化⼀个大小为 2 的数组就可以当做哈希表 hash 了;初始化⼀些变量 left = 0 ,right = 0 , ret = 0 ;
b. 当 right ⼩于数组⼤⼩的时候,⼀直下列循环:
i. 让当前元素进⼊窗⼝,顺便统计到哈希表中;
ii. 检查 0 的个数是否超标: • 如果超标,依次让左侧元素滑出窗⼝,顺便更新哈希表的值,直到 0 的个数恢复正常;
iii. 程序到这⾥,说明窗⼝内元素是符合要求的,更新结果;
iv. right++ ,处理下⼀个元素;
c. 循环结束后, ret 存的就是最终结果。

 public int longestOnes(int[] nums, int k) {
       int ret = 0;
        for (int left = 0, right = 0, zero = 0; right < nums.length; right++) {
            if (nums[right] == 0) zero++; // 进窗⼝ 
            while(zero > k) // 判断
            if (nums[left++] == 0) zero--; // 出窗⼝
            ret = Math.max(ret, right - left + 1); // 更新结果
        }
        return ret;
    }

四 、将 x 减到 0 的最小操作数

  1. 题⽬链接:将 x 减到 0 的最小操作数
  2. 题⽬描述:

给你⼀个整数数组 nums 和⼀个整数 x 。每⼀次操作时,你应当移除数组 nums 最左边或最右边的 元素,然后从 x 中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使⽤。 如果可以将 x 恰好减到 0 ,返回最⼩操作数 ;否则,返回 -1 。
⽰例 1: 输⼊:nums = [1,1,4,2,3], x = 5
输出:2 解释:最佳解决⽅案是移除后两个元素,将 x 减到 0 。
⽰例 2: 输⼊:nums = [5,6,7,8,9], x = 4
输出:-1
⽰例 3: 输⼊:nums = [3,2,20,1,1,3], x = 10
输出:5
解释:最佳解决⽅案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。
提⽰: 1 <= nums.length <= 10^5
1 <= nums[i] <= 10^4
1 <= x <= 10^9

  1. 解法(滑动窗⼝):

算法思路:

题目要求的是数组「左端+右端」两段连续的、和为 x 的最短数组,信息量稍微多⼀些,不易理清思路;我们可以转化成求数组内⼀段连续的、和为 sum(nums) - x 的最长数组。此时,就是熟悉的「滑动窗口」问题了。

算法流程:

a. 转化问题:求 target = sum(nums) - x 。如果 target < 0 ,问题⽆解;
b. 初始化左右指针 l = 0 , r = 0 (滑动窗⼝区间表⽰为 [l, r) ,左右区间是否开闭很重 要,必须设定与代码⼀致),记录当前滑动窗⼝内数组和的变量 sum = 0 ,记录当前满⾜条 件数组的最⼤区间⻓度 maxLen = -1 ;
c. 当 r ⼩于等于数组⻓度时,⼀直循环:
i. 如果 sum < target ,右移右指针,直⾄变量和大于等于 target ,或右指针已经移到头;
ii. 如果 sum > target ,右移左指针,直⾄变量和小于等于 target ,或左指针已经移到头;
iii. 如果经过前两步的左右移动使得 sum == target ,维护满⾜条件数组的最大长度,并让下个元素进⼊窗⼝;
d. 循环结束后,如果 maxLen 的值有意义,则计算结果返回;否则,返回 -1 。

4.代码示例:

 public int minOperations(int[] nums, int x) {
        int sum = 0,n = nums.length;
        for(int a :nums) sum+=a;
        int target = sum-x;
        //处理细节
        if(target<0) return -1;
        int ret = -1;
        for(int left=0,right=0,sum1 = 0;right<n;right++){
            sum1+=nums[right];; // 进窗⼝
            while(sum1>target){//判断
                sum1-=nums[left++];//出窗口
            }
            if(sum1==target){
                ret = Math.max(ret,right-left+1);//更新结果
            }
        }
        if(ret==-1) return ret;
        else return n-ret;
    }

lan

五、水果成篮

  1. 题⽬链接:水果成篮
  2. 题⽬描述:

你正在探访⼀家农场,农场从左到右种植了⼀排果树。这些树⽤⼀个整数数组 fruits 表⽰,其中fruits[i] 是第 i 棵树上的⽔果 种类 。 你想要尽可能多地收集⽔果。然⽽,农场的主⼈设定了⼀些严格的规矩,你必须按照要求采摘⽔果:
• 你只有两个篮⼦,并且每个篮⼦只能装单⼀类型的⽔果。每个篮⼦能够装的⽔果总量没有限制。
• 你可以选择任意⼀棵树开始采摘,你必须从每棵树(包括开始采摘的树)上恰好摘⼀个⽔果 。采摘的⽔果应当符合篮⼦中的⽔果类型。每采摘⼀次,你将会向右移动到下⼀棵树,并继续采摘。
• ⼀旦你⾛到某棵树前,但⽔果不符合篮⼦的⽔果类型,那么就必须停⽌采摘。 给你⼀个整数数组 fruits ,返回你可以收集的⽔果的最⼤数⽬。
⽰例 1:输⼊:fruits = [1,2,1]
输出:3 解释:可以采摘全部 3 棵树。
⽰例 2:输⼊:fruits = [0,1,2,2]
输出:3 解释:可以采摘 [1,2,2] 这三棵树。 如果从第⼀棵树开始采摘,则只能采摘 [0,1] 这两棵树。
⽰例 3:输⼊:fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出:5 解释:可以采摘 [1,2,1,1,2]这五棵树

  1. 解法(滑动窗⼝):
    算法思路: 研究的对象是⼀段连续的区间,可以使⽤「滑动窗⼝」思想来解决问题。 让滑动窗⼝满⾜:窗⼝内⽔果的种类只有两种。
    做法:右端⽔果进⼊窗⼝的时候,⽤哈希表统计这个⽔果的频次。这个⽔果进来后,判断哈希表的 ⼤⼩:
    ▪ 如果⼤⼩超过 2:说明窗⼝内⽔果种类超过了两种。那么就从左侧开始依次将⽔果划出窗 ⼝,直到哈希表的⼤⼩⼩于等于 2,然后更新结果;
    ▪ 如果没有超过 2,说明当前窗⼝内⽔果的种类不超过两种,直接更新结果 ret。

算法流程:

a. 初始化哈希表 hash 来统计窗⼝内⽔果的种类和数量;
b. 初始化变量:左右指针 left = 0,right = 0,记录结果的变量 ret = 0;
c. 当 right ⼩于数组⼤⼩的时候,⼀直执行下列循环:
i. 将当前⽔果放⼊哈希表中;
ii. 判断当前⽔果进来后,哈希表的⼤⼩: • 如果超过 2: ◦ 将左侧元素滑出窗⼝,并且在哈希表中将该元素的频次减⼀; ◦ 如果这个元素的频次减⼀之后变成了 0,就把该元素从哈希表中删除; ◦ 重复上述两个过程,直到哈希表中的⼤⼩不超过 2;
iii. 更新结果 ret;
iv. right++,让下⼀个元素进⼊窗⼝;
d. 循环结束后,ret 存的就是最终结果。

4.代码示例:
此处为大家介绍两种代码
一、使⽤容器:

   public int totalFruit(int[] f) {
        Map<Integer, Integer> hash = new HashMap<Integer, Integer>(); // 统计窗 ⼝内⽔果的种类
        int ret = 0;
        for (int left = 0, right = 0; right < f.length; right++) {
            int in = f[right];
            hash.put(in, hash.getOrDefault(in, 0) + 1); // 进窗⼝
            while (hash.size() > 2) {
                int out = f[left];
                hash.put(out, hash.get(out) - 1); // 出窗⼝
                if (hash.get(out) == 0)
                    hash.remove(out);
                left++;
            }
            // 更新结果
            ret = Math.max(ret, right - left + 1);
        }
        return ret;
    }

二、用数组模拟哈希表

  public int totalFruit1(int[] f) {
        int n = f.length;
        int[] hash = new int[n + 1]; // 统计窗⼝内⽔果的种类
        int ret = 0;
        for (int left = 0, right = 0, kinds = 0; right < n; right++) {
            int in = f[right];
            if (hash[in] == 0) kinds++; // 维护⽔果种类
            hash[in]++; // 进窗⼝
            while (kinds > 2) // 判断
            {
                int out = f[left];
                hash[out]--; // 出窗⼝
                if (hash[out] == 0) kinds--;
                left++;
            }
            // 更新结果
            ret = Math.max(ret, right - left + 1);
        }
        return ret;
    }

六 、找到字符串中所有字母异位词

  1. 题⽬链接:找到字符串中所有字⺟异位词
  2. 题⽬描述:

给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的⼦串,返回这些⼦串的起始索引。不考虑答案 输出的顺序。 异位词 指由相同字⺟重排列形成的字符串(包括相同的字符串)。
⽰例 1:
输⼊: s = “cbaebabacd”, p = “abc”
输出:[0,6]
解释:起始索引等于 0 的⼦串是 “cba”, 它是 “abc” 的异位词。 起始索引等于 6 的⼦串是 “bac”, 它是 “abc” 的异位词。
⽰例 2:
输⼊: s = “abab”, p = “ab”
输出:[0,1,2]
解释:起始索引等于 0 的⼦串是 “ab”, 它是 “ab” 的异位词。 起始索引等于 1 的⼦串是 “ba”, 它是 “ab” 的异位词。 起始索引等于 2 的⼦串是 “ab”, 它是 “ab” 的异位词。 提⽰: 1 <= s.length, p.length <= 3 * 10^4
s 和 p 仅包含⼩写字⺟

  1. 解法(滑动窗口 + 哈希表):

算法思路:

◦ 因为字符串 p 的异位词的长度⼀定与字符串 p 的⻓度相同,所以我们可以在字符串 s 中构 造⼀个⻓度为与字符串 p 的⻓度相同的滑动窗口,并在滑动中维护窗口中每种字⺟的数量;
◦ 当窗口中每种字母的数量与字符串 p 中每种字⺟的数量相同时,则说明当前窗口为字符串 p 的异位词;
◦ 因此可以用两个大小为 26 的数组来模拟哈希表,⼀个来保存 s 中的子串每个字符出现的个 数,另⼀个来保存 p 中每⼀个字符出现的个数。这样就能判断两个串是否是异位词。

4.代码示例:

 public List<Integer> findAnagrams(String ss, String pp) {
        List<Integer> ret = new ArrayList<Integer>();
        char[] s = ss.toCharArray();
        char[] p = pp.toCharArray();
        int[] hash1 = new int[26]; // 统计字符串 p 中每⼀个字符出现的个数
        for (char ch : p) hash1[ch - 'a']++;
        int[] hash2 = new int[26]; // 统计窗⼝中每⼀个字符出现的个数
        int m = p.length;
        for (int left = 0, right = 0, count = 0; right < s.length; right++) {
            char in = s[right];
            // 进窗⼝ + 维护 count
            if (++hash2[in - 'a'] <= hash1[in - 'a']) count++;
            if (right - left + 1 > m) // 判断
            {
                char out = s[left++];
                // 出窗⼝ + 维护 count
                if (hash2[out - 'a']-- <= hash1[out - 'a']) count--;
            }
            // 更新结果
            if (count == m) ret.add(left);
        }
        return ret;
    }

七 、串联所有单词的子串

  1. 题⽬链接:串联所有单词的子串
  2. 题⽬描述:

给定⼀个字符串 s 和⼀个字符串数组 words。 words 中所有字符串 ⻓度相同。 s 中的 串联⼦串 是指⼀个包含 words 中所有字符串以任意顺序排列连接起来的⼦串。 ◦ 例如,如果 words = [“ab”,“cd”,“ef”], 那 么 “abcdef”,“abefcd”,“cdabef”, “cdefab”,“efabcd”, 和 “efcdab” 都是串联子串。 “acdbef” 不是串联⼦串,因为他不是任何 words 排列的连接。
返回所有串联字串在 s 中的开始索引。你可以以 任意顺序返回答案。
⽰例 1: 输⼊:s = “barfoothefoobarman”, words = [“foo”,“bar”] 输出:[0,9]
解释:因为 words.length == 2 同时 words[i].length == 3,连接的⼦字符串的⻓度必须为 6。 ⼦串 “barfoo” 开始位置是 0。它是 words 中以 [“bar”,“foo”] 顺序排列的连接。 ⼦串 “foobar” 开始位置是 9。它是 words 中以 [“foo”,“bar”] 顺序排列的连接。 输出顺序⽆关紧要。返回 [9,0] 也是可以的。
⽰例 2: 输⼊:s = “wordgoodgoodgoodbestword”, words [“word”,“good”,“best”,“word”] 输出:[]
解释:因为 words.length == 4 并且 words[i].length == 4,所以串联⼦串的⻓度必须为 16。 s 中没有⼦串⻓度为 16 并且等于 words 的任何顺序排列的连接。 所以我们返回⼀个空数组。
⽰例 3: 输⼊:s = “barfoofoobarthefoobarman”, words = [“bar”,“foo”,“the”] 输出:[6,9,12]
解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。 ⼦串 “foobarthe” 开始位置是 6。它是 words 中以 [“foo”,“bar”,“the”] 顺序排列的连接。 ⼦串 “barthefoo” 开始位置是 9。它是 words 中以 [“bar”,“the”,“foo”] 顺序排列的连接。 ⼦串 “thefoobar” 开始位置是 12。它是 words 中以 [“the”,“foo”,“bar”] 顺序排列的连接。
提示:1 <= s.length <= 104
1 <= words.length <= 5000
1 <= words[i].length <= 30
words[i] 和 s 由⼩写英⽂字⺟组成

3.解法(滑动窗口):

如果我们把每⼀个单词看成⼀个⼀个字⺟,问题就变成了找到「字符串中所有的字⺟异位词」。⽆ ⾮就是之前处理的对象是⼀个⼀个的字符,我们这⾥处理的对象是⼀个⼀个的单词。

4.代码示例:

 public List<Integer> findSubstring(String s, String[] words) {
        List<Integer> ret = new ArrayList<Integer>();
        // 保存字典中所有单词的频次
        Map<String, Integer> hash1 = new HashMap<String, Integer>();
        for (String str : words) hash1.put(str, hash1.getOrDefault(str, 0) + 1);
        int len = words[0].length(), m = words.length;
        for (int i = 0; i < len; i++) // 执⾏次数
        {
            // 保存窗⼝内所有单词的频次
            Map<String, Integer> hash2 = new HashMap<String, Integer>();
            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++;
				// 判断
                if (right - left + 1 > len * m) {
                    // 出窗⼝ + 维护 count
                    String out = s.substring(left, left + len);
                    if (hash2.get(out) <= hash1.getOrDefault(out, 0)) count--;
                    hash2.put(out, hash2.get(out) - 1);
                    left += len;
                }
                // 更新结果
                if (count == m) ret.add(left);
            }
        }
        return ret;
    }

八、最小覆盖子串

  1. 题⽬链接:最小覆盖子串
  2. 题⽬描述:

给你⼀个字符串 s 、⼀个字符串 t 。返回 s 中涵盖 t 所有字符的最⼩⼦串。如果 s 中不存在涵盖 t 所 有字符的⼦串,则返回空字符串 “” 。
注意: 对于 t 中重复字符,我们寻找的⼦字符串中该字符数量必须不少于 t 中该字符数量。 如果 s 中存在这样的⼦串,我们保证它是唯⼀的答案。
⽰例 1:输⼊:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”
解释:最⼩覆盖⼦串 “BANC” 包含来⾃字符串 t 的 ‘A’、‘B’ 和 ‘C’。
⽰例 2:输⼊:s = “a”, t = “a”
输出:“a”
解释:整个字符串 s 是最⼩覆盖⼦串。
⽰例 3:
输⼊: s = “a”, t = “aa”
输出: “”
解释:t 中两个字符 ‘a’ 均应包含在 s 的⼦串中, 因此没有符合条件的⼦字符串,返回空字符串。

  1. 解法(滑动窗⼝ + 哈希表):

算法思路:

◦ 研究对象是连续的区间,因此可以尝试使⽤滑动窗⼝的思想来解决。

◦ 如何判断当前窗⼝内的所有字符是符合要求的呢?
我们可以使⽤两个哈希表,其中⼀个将⽬标串的信息统计起来,另⼀个哈希表动态的维护窗⼝ 内字符串的信息。 当动态哈希表中包含⽬标串中所有的字符,并且对应的个数都不⼩于⽬标串的哈希表中各个字 符的个数,那么当前的窗⼝就是⼀种可⾏的⽅案。
算法流程:
a. 定义两个全局的哈希表: 1 号哈希表 hash1 ⽤来记录⼦串的信息, 2 号哈希表 hash2⽤来记录⽬标串 t 的信息;
b. 实现⼀个接⼝函数,判断当前窗⼝是否满⾜要求:
i. 遍历两个哈希表中对应位置的元素:
• 如果 t 中某个字符的数量⼤于窗⼝中字符的数量,也就是 2 号哈希表某个位置⼤于 1 号哈希表。说明不匹配,返回 false ; • 如果全都匹配,返回 true 。

主函数中:

a. 先将 t 的信息放⼊ 2 号哈希表中;
b. 初始化⼀些变量:左右指针: left = 0,right = 0 ;⽬标⼦串的⻓度: len = INT_MAX ;⽬标⼦串的起始位置: retleft ;(通过⽬标⼦串的起始位置和⻓度,我们就 能找到结果)
c. 当 right ⼩于字符串 s 的⻓度时,⼀直下列循环:
i. 将当前遍历到的元素扔进 1 号哈希表中;
ii. 检测当前窗⼝是否满⾜条件: • 如果满⾜条件: ◦ 判断当前窗⼝是否变⼩。如果变⼩:更新⻓度 len ,以及字符串的起始位置retleft ;
◦ 判断完毕后,将左侧元素滑出窗⼝,顺便更新 1 号哈希表;
◦ 重复上⾯两个过程,直到窗⼝不满⾜条件;
iii. right++ ,遍历下⼀个元素;
d. 判断 len 的⻓度是否等于 INT_MAX :
i. 如果相等,说明没有匹配,返回空串;
ii. 如果不想等,说明匹配,返回 s 中从 retleft 位置往后 len ⻓度的字符串。

4.代码示例:

 public String minWindow(String ss, String tt) {
        char[] s = ss.toCharArray();
        char[] t = tt.toCharArray();
        int[] hash1 = new int[128]; // 统计字符串 t 中每⼀个字符的频次
        int kinds = 0; // 统计有效字符有多少种
        for (char ch : t)
            if (hash1[ch]++ == 0) kinds++;
        int[] hash2 = new int[128]; // 统计窗⼝内每个字符的频次
        int minlen = Integer.MAX_VALUE, begin = -1
        for (int left = 0, right = 0, count = 0; right < s.length; right++) {
            char in = s[right];
            if (++hash2[in] == hash1[in]) count++; // 进窗⼝ + 维护 count
            while (count == kinds) // 判断条件
            {
                if (right - left + 1 < minlen) // 更新结果
                {
                    minlen = right - left + 1;
                    begin = left;
                }
                char out = s[left++];
                if (hash2[out]-- == hash1[out]) count--; // 出窗⼝ + 维护 count
            }
        }
        if (begin == -1) return new String();
        else return ss.substring(begin, begin + minlen);
    }

结语

本文到这里就结束了,主要介绍了滑动窗口相关知识以及讲解部分例题,希望能够对你有帮助。

以上就是本文全部内容,感谢各位能够看到最后,创作不易,希望大家多多支持!
最后,大家再见!祝好!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值