算法:滑动窗口题目练习

目录

题目一:长度最小的子数组

题目二:无重复字符的最长子串

题目三:最大连续 1 的个数III

题目四:将 x 减到 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

解法一:暴力枚举出所有的子数组的和

首先两层循环枚举所有的子数组,然后每一个子数组需要遍历一遍得到总和,所以它的时间复杂度是O(N^3)

这里的暴力枚举策略可以优化为:提前定义一个sum,表示子数组的和,每次right往后移动,都sum+=该值,所以省去了一次遍历子数组的过程,优化为了O(N^2)


解法二:利用单调性,使用“ 同向双指针 "来优化

这里的同向双指针就称为滑动窗口,两个指针都不会回退,向同一个方向滑动的过程,形象的看做一个滑动窗口,维护left到right的这段区间

第一个规律:

下面分析一下,因为给的数组nums的值都是正整数,所以sum一旦加right指向的数大于等于target了,那此时right就没必要往后再遍历了

因为当前已经大于等于target了,再往后加只会使得数组的长度越来越长,不符合条件

假设数组nums[2, 3, 1, 2, 4, 3],target=7,当2,3,1,2时已经满足了,此时len=4,那继续right右移,2,3,1,2,4也满足条件,只不过len=5,比4大,就pass了,所以没必要继续往后了,因为越往后len肯定越大的

我们利用单调性,规避了很多没有必要的枚举行为

第二个规律:

假设数组nums[2, 3, 1, 2, 4, 3],target=7,当2,3,1,2加起来已经等于8,满足条件了,此时left指向的是2,right指向的是2(第4个数字)

该left++了,此时sum并不需要清0,从头加,因为我们知道2+3+1+2=8,那么left++就表示从第二个数字3开始,所以此时指向的子数组的和sum只需要-2即可,不需要重新加一遍从left到right的值

滑动窗口的使用:
①left= 0, right= 0
②进窗口
③判断什么时候出窗口

时间复杂度是O(N),因为虽然代码是两层循环,但是其实每次都是right右移或者left右移,相当于是n+n次,即2n次,也就是O(N)的时间复杂度


下面详细讲解上述的三步滑动窗口的使用过程:

假设数组nums[2, 3, 1, 2, 4, 3],target=7

第一步,left和right都是0,都指向2,sum初始值为0

第二步,right进入窗口,此时sum需要更新为2

第三步,此时判断sum的值是否满足要求,发现2 < 7,不满足,所以不出窗口,left不动,继续进窗口,也就是right右移,此时right指向3,sum更新为5,接下来继续判断:5 < 7,不满足,所以重复第二步

判断后不出窗口,left不动,继续进窗口,也就是right右移,此时right指向1,sum更新为6,判断6 < 7,不满足,所以重复第二步

判断后不出窗口,left不动,继续进窗口,也就是right右移,此时right指向2,sum更新为8,判断8 >= 7,满足条件,所以更新结果,长度len = 4

然后出窗口,因为此时已经满足条件了,就不用right继续右移了,直接出窗口left右移

出窗口left右移,sum更新为8-2=6,此时继续判断:6 < 7,不出窗口,left不动,right右移,此时right指向4,sum更新为10,满足条件,此时更新结果,len = 4不变

继续left右移,指向了1,sum更新为了7,此时继续判断:7 >= 7,满足条件,继续更新结果,len = 3

出窗口,left右移,指向2,sum更新为了6,判断:6 < 7,不满足

right右移指向3,sum更新为了9,9 >= 7,此时len = 3不变

继续left右移,指向了4,sum更新为7,此时继续判断:7 >= 7,满足条件,继续更新结果,len = 2

继续left右移,指向了3,sum更新为了3,不满足

此时本应该right右移,但是没有元素了,所以滑动窗口就结束了,返回结果len = 2,此题结束

上述便是滑动窗口的使用样例,比较详细,在后面的时候就不会再这么详细说明了,因为是第一道题所以详细列举一下用法,方便理解


代码如下:

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int len = INT_MAX, n = nums.size(), sum = 0;
        for(int left = 0, right = 0; right < n; right++)
        {
            //进窗口
            sum += nums[right];
            while(sum >= target)//判断
            {
                len = min(len,right-left+1);//更新结果
                //出窗口
                sum -= nums[left++];
            }
        }
        //可能会出现没有结果的情况,判断len是否没改变
        return len == INT_MAX ? 0 : len;
    }
};

题目二:无重复字符的最长子串

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke"是一个子序列,不是子串。

子串也就是连续的一段字符

解法一:暴力枚举 + 哈希表(判断字符是否重复出现)

暴力解法,也就是固定每一个起始位置,依次向后扩展,直到扩展到不能扩展为止,然后统计一下长度,把所有情况都枚举到,找一个最大值即可

这里的时间复杂度是O(N^2)的

解法二:利用滑动窗口来解决问题

我们发现,如果是"dabcabcbb",当left指向d,right指向a后,遇到了重复字符,此时不需要left++,然后right再从left的位置重新++

因为已经知道dabc是不重复的,所以如果left再++指向a,right依然会遇到a停止,此时的子串长度肯定是比上一次小的
所以优化一:可以让left先跳过这个重复字符,再继续计算不重复子串长度

因为left已经跳过这个重复字符了,所以right与left之间不会再有重复字符了,所以right就不需要再指向left的位置重新++

所以优化二:当left跳过重复字符时,right就可以继续从当前位置向后寻找不重复字符

所以步骤依然是:进窗口、判断什么时候出窗口、更新结果(这里的更新结果的顺序因题目而定)

进窗口:让字符进入哈希表
判断:窗口内是否出现重复字符
更新结果:如果没有出现重复字符,此时就可以更新结果
出窗口:最后如果发现有重复字符,就从哈希表中删除字符


代码如下:

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n = s.size(), left = 0, right = 0, len = 0;
        int hash[128] = {0};
        while(right < n)
        {
            hash[s[right]]++; // 进窗口
            while(hash[s[right]] > 1) // 判断是否出窗口
                hash[s[left++]]--; // 出窗口
            len = max(len, right-left+1); // 更新结果
            right++; // 下一个元素进窗口
        }
        return len;
    }
};

题目三:最大连续 1 的个数III

给定一个二进制数组 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。

这个题的题意很容易理解,也就是给一个K,可以将nums数组中最多K个0变为1(小于等于K),求最长的子数组长度

可以将这个问题转化为:找出最长的子数组,0的个数不超过k个

解法一:暴力枚举 + zero计数器(统计0的个数)

固定一个起点,依次枚举终点

例如nums = [1,1,1,0,0,0,1,1,1,1,0],K = 2,left指向1,right指向1,right遇到1无视,遇到0,zero++,直到zero > K为止,即走到下图所示的情况,right停止,红线即为当前情况连续最大的1个数,记录下来:

此时left++,right指向left的位置,重新向后移动,重复之前的操作,走到下图为止:

以此类推,直到right超过数组的长度,取之前记录下来的最长值,即为最终结果


解法二:滑动窗口 + zero计数器(统计0的个数)

通过暴力解法,我们可以发现,当zero > K时,left如果没有走过0的位置,那么right每次都会停到刚刚停下的位置,所以可以做以优化:
当zero > K时,left超过一个0,zero--后,right再往后走,进而提高效率

也就是当这种情况时:

我们只需让left一直++,直到超过一个0为止:

此时right再往后走,循环上述步骤,直到right越界为止

滑动窗口的步骤如下:

①left=0;right=0
②进窗口
③判断是否出窗口
④更新结果

进窗口就相当于让right依次向后移动,遇到1无视,遇到0,zero++

当zero > K进行出窗口操作,直到合法为止(即zero < K),出窗口即为遇到1无视,遇到0,zero--

更新结果即是在每次判断完,如果合法就更新结果


代码如下:

public:
    int longestOnes(vector<int>& nums, int k) {
        int left = 0, right = 0, n = nums.size();
        int zero = 0, ret = 0;
        while(right < n)
        {
            if(nums[right] == 0) zero++;      // 进窗口
            while(zero > k)                   // 判断是否出窗口
                if(nums[left++] == 0) zero--; // 出窗口
            ret = max(ret,right-left+1);      // 更新结果
            right++;                          // 下一个元素进窗口
        }
        return ret;
    }
};

题目四:将 x 减到 0 的最小操作数

给你一个整数数组 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 。

刚开始看这个题可能觉得比较难,因为是可能从数组的两边分别找几个数,最终使得相加等于x,并且还需要相加的数的个数最少,那么我们其实也可以换一种思路,正难则反,两边找最短比较困难,那就中间找最长

找两边比较难,不连续,且没有关联性,那能不能从中间找一段连续的区间,使得中间这一段连续的区间等于数组的总和sum - x,这样就间接的表示找到了所需的元素,接着从所找到的所有集合中,找到个数最多的情况,即为符合条件的情况

因为中间的个数越多,两边的个数就越少,也就是找到了最小操作数

所以题目转化为:找出最长的子数组的长度len,所有元素的和正好等于sum- x,设为target,此时所求的最小操作数就是数组长度 n - len


转化后的这道题和我们所做的题目一非常相似,利用单调性,使用“ 同向双指针 "来优化

同样是定义left和right最开始指向0,每次right进窗口,都加上right所指向的值,直到所加的值 >= target后,执行left++

这里优化暴力方法,right不需要重新回到left的地方,因此right停止的这个地方刚好是从left到right相加 >= target的地方,而此时left向右移动了,那么相加的更小了,所以right如果回到left所指向的位置,也一定会回到这个位置,因此,当left向右移动后,right只需要判断是否向后移动即可,不需要回到前面

所以滑动窗口的步骤:

①left=0,right=0
②进窗口,childsum += nums[right]
③判断是否出窗口,判断childsum > target,如果满足就出窗口,即childsum -= nums[left]
④合适的地方更新结果,即当childsum == target时更新结果

这里的时间复杂度是O(N)的


代码如下:

class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        int n = nums.size(),left = 0,right = 0;
        int len = -1,sum = 0, childsum = 0;
        for(auto& iter : nums) sum += iter;//计算数组总和
        if(sum < x) return -1; // 边界情况
        int target = sum - x;
        while(right < n)
        {
            childsum += nums[right];//进窗口
            while(childsum > target)//判断
                childsum -= nums[left++];//出窗口
            if(childsum == target)//更新结果
                len = max(len, right-left+1);
            right++;
        }
        if(len == -1) return len;
        return n - len;
    }
};

题目五:水果成篮

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类 。

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

  • 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
  • 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
  • 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。

给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。

提示:

  • 1 <= fruits.length <= 105
  • 0 <= fruits[i] < fruits.length

示例 1:

输入:fruits = [1,2,1]
输出:3
解释:可以采摘全部 3 棵树。

示例 2:

输入:fruits = [0,1,2,2]
输出:3
解释:可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。

示例 3:

输入:fruits = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。

示例 4:

输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:可以采摘 [1,2,1,1,2] 这五棵树。

这个题目刚开始读,可能会觉得比较绕,其实读完结合示例是不难理解的,也就是题目可以转化为:

在数组中找出一个连续的子数组的长度,使得这个连续的子数组不超过两种类型的水果,不同的数字表示不同的类型

解法一:暴力枚举 + 哈希表

哈希表用于统计暴力枚举中水果出现了多少种,也就是就是找到所有符合条件的子数组,找出最长的那一个子数组即可

解法二:滑动窗口 + 哈希表

在暴力枚举中,遍历每个数组时,每次left++,right都会回到left的位置,重新向后++,但是我们可以思考一下,这里的left++,水果的种类只会有两种情况
①种类减少,right可以++
②种类不变,right不能动

所以既然left++后只有这两种情况,right要么++要么不动,那么right就可以优化为不需要回到left处,只需要判断是否需要++即可

所以滑动窗口的方式解决:

①left=0,right=0
②进窗口
③判断是否出窗口
④更新结果

需要说明的一点:我们这里使用数组hash来模拟哈希表,也可以使用unordered_map<int ,int>来实现哈希操作,其中第一个int表示种类,第二个int表示数量,但是使用unordered_map会频繁的在哈希表中插入删除元素,比较耗时,所以可以考虑使用数组模拟这个哈希过程:

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        int left = 0,right = 0,n = fruits.size();
        int hash[100001] = {0};
        int kinds = 0,len = 0;
        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 = max(len,right-left+1);//更新结果
            right++;
        }
        return len;
    }
};

题目六:找到字符串中所有字母异位词

给定两个字符串 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" 的异位词。

关于异位词,比如"abc",它的异位词就是a、b、c这三个字母不同顺序排列出来的都称为"abc"的异位词,所以"abc"的异位词就是abc、acb、bac、bca、cab、cba

那么我们通过观察,只要这三个字母每一个字母出现的次数,和原字符串的的每一个字母出现的次数相等,也就是abc都各出现了1次,就说明是该字符串的异位词

所以这里如何记录出现的次数,使用哈希表就可以做到

解法一:暴力枚举 + 哈希表

将字符串中所有与p字符串长度相同的子串都找到,每次都与p字符串对应的哈希表作对比,直到比较完所有的字符串为止:

假设p字符串是abc,所给字符串是cbaeba,暴力方法也就是下图所示,依次将每一个红线所画的子串都映射入哈希表中,比较是否与p字符串的映射的哈希表相同


但是仔细观察上述暴力解法,没有必要每次重新哈希映射, 因为第一个子串cab和第二个子串bae,区别就是将c移出哈希表,而将e移进来,所以并不需要第二个子串映射时,将bae重新映射一遍,只需移出一个移进来一个字符即可

解法二:滑动窗口 + 哈希表

这道题与前面所做的题不同,这道题的滑动窗口大小是固定的

滑动窗口的第一种思路:left和right每次同时移动,每次删除一个元素,增加一个元素接口,并且在每次移动前,比较两个哈希表是否相同,相同就表示符合题意,将left下标push_back到vector中即可

过程依旧是:

①left=0,right=0
②进窗口
③判断是否出窗口
④更新结果

代码如下:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int ns = s.size(),np = p.size();
        int hashs[26] = {0}; //统计字符串 s 中每个字符出现的次数
        int hashp[26] = {0}; //统计字符串 p 中每个字符出现的次数
        vector<int> v;
        bool flag = true;
        for(int i = 0; i < np; ++i) hashp[p[i]-'a']++;
        for(int left = 0,right = 0; right < ns; right++)
        {
            hashs[s[right]-'a']++; //进窗口
            for(int j = 0; j < 26; ++j)
            {
                //如果不同就改为false
                if(hashs[j] != hashp[j]) flag = false;
            }
            if(flag) v.push_back(left);//没返回false表示相同,所以更新结果
            //在s中子串等于np后,每次left++right++
            if(right >= np-1) hashs[s[left++]-'a']--;//出窗口
            flag = true;
        }
        return v;
    }
};

滑动窗口的第二种思路:不需要比较两个哈希表是否相同,只需要引入一个变量count,表示有效字符的长度

因为虽然将哈希表用一个大小为26的数组表示后,每次比较只需要比较26次,但是还是效率不高,所以这里引入一个变量count,表示有效字符的长度,p字符串映射到hashp数组中,字符串s遍历时映射到hashs数组中,p字符串大小为np

如果hashs[right]++后,如果hashs[right] <= hashp[right],那就count++,表示是有效字符
如果hashs[right]++后,hashs[right] > hashp[right],count就不变,说明不是有效字符

 假设已经走到这里了

此时count等于np时,表示当前的字符串中包含了p字符串的异位词,所以此时仅需判断长度是否与np相同,相同就说明符合题意,上述例子就不同,因为np是3,而当前字符串是4

如果不同,就需要hashs[left]--,在--之前, 进行判断hashs[left] > hashp[left],如果大于count就不需要变,如果小于就需要count--,left++,直到长度相同为止,将left尾插到vector中

所以此时left需要++,还有hashs[left]--,--前发现hashs[left]是2,大于hashp中的1,所以此时hashs[left]--,不影响有效长度,--完成后,如下:

此时count == np,且子串长度也等于np,就可以将left尾插到vector中了,接着向后执行即可,直到right超过界限

所以步骤是:

①left=0,right=0
②进窗口,若hashs[right] <= hashp[right] -> count++
③判断是否出窗口,hashs[left] <= hashp[left] -> count--,否则count就不变
④更新结果,count == np时,更新

代码如下:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int ns = s.size(),np = p.size(), count = 0;
        int hashs[26] = {0}; //统计字符串 s 中每个字符出现的次数
        int hashp[26] = {0}; //统计字符串 p 中每个字符出现的次数
        vector<int> v;
        for(int i = 0; i < np; ++i) hashp[p[i]-'a']++;
        for(int left = 0,right = 0; right < ns; right++)
        {   //进窗口+维护count
            if(++hashs[s[right]-'a'] <= hashp[s[right]-'a']) count++;
            if(right - left + 1 > np) //判断
            {
                if(hashs[s[left]-'a'] <= hashp[s[left]-'a'])//出窗口
                    count--;
                hashs[s[left]-'a']--;
                left++;
            }
            if(count == np) v.push_back(left);//更新结果
        }
        return v;
    }
};

题目七:串联所有单词的子串

给定一个字符串 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"] 顺序排列的连接。

这道题初一看,比较难,但是我们转换一下思路,将words中的字符串看成一个字符,就相当于:

题目就变为了在"bacabd"字符串中找"ab",其中ab可以是不同的顺序,那么此时这个题目就和上一道题几乎一模一样了,只是此题中的ab是字符串而已

所以解法一:滑动窗口 + 哈希

下面着重说说和上一题的不同之处:

不同一:哈希表不同

上一道题的哈希表是用一个数组表示的,但是此题是一个字符串,所以采用unordered_map<string, int>来映射

不同二:left、right指针的移动不同

因为上一题是一个一个字符移动的,而此题是将固定长度的字符串当做一个字符,所以这里left和right移动时,就需要移动固定的长度,如果按上面画图的例子看,就需要移动三步,因为words中的字符串长度是3

所以left和right移动的步长是一个单词的长度len

不同三:滑动窗口执行的次数不同

如上图所示,有可能是红色线标注出来的对应的words中的字符串,也有可能是紫色或是绿色线标注出来的对应,所以这里的滑动窗口执行的次数是len次

之所以是len次,是因为第len+1次开始就与之前的重复了

下面的代码截取string中的一部分,使用的是给出起始位置,给出需要的数量,也就是
string tmp(s,right,len)这样使用的,其中s指从string s中获取,right和len是指从right所指向的位置开始截取len个

当然那也可以使用substr,例如s.substr(right,len),这两种方式的效果是一样的

代码如下:

class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {
        int len = words[0].size(), ns = s.size(), count = 0, wordlen = words.size();
        unordered_map<string, int> mp1;// 保存 words 中所有单词的频次
        unordered_map<string, int> mp2;
        vector<int> v;
        for(auto& iter : words) mp1[iter]++;
        for(int i = 0; i < len; i++)// 执行 len 次活动窗口
        {
            for(int left = i,right = i; right + len <= ns; right+=len)
            {
                string tmp(s,right,len); //tmp 类似于的单个字符的right指向的值
                mp2[tmp]++;
                if(mp1.count(tmp) && mp2[tmp] <= mp1[tmp]) count++; // 进窗口 + 维护count
                if(right-left+1 > wordlen*len) // 判断
                {
                    // 出窗口 + 维护count
                    string cur(s,left,len);//cur 类似于的单个字符的left指向的值
                    left += len;
                    if(mp1.count(cur) && mp2[cur] <= mp1[cur]) count--;
                    mp2[cur]--;
                }
                if(count == wordlen) v.push_back(left);// 更新结果
                tmp.clear();
            }
            mp2.clear(); // 每次执行完 mps 置空
            count = 0;   // 每次执行完 count 置空
        }
        return v;
    }
};

上面有个优化的点:在进窗口时先执行mp1.count(tmp),是为了确保mp1中是有这个tmp单词的,如果mp1中没有这个单词,就不需要比较出现的次数了,提高效率

如果不加这个语句,每次比较时即使mp1中没有出现tmp单词,mp1哈希表依旧会将tmp映射进入,这会造成一定的时间消耗


题目八:最小覆盖子串

给你一个字符串 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 的子串中,
因此没有符合条件的子字符串,返回空字符串。

解法一:暴力解法 + 哈希

设计两个哈希数组,将t中的字符哈希进数组1,然后依次遍历字符串s,每遍历一个字符映射进数组2,直到数组2中对应的字符大于等于数组1中对应字符的 个数,此时就满足条件

从字符串s中的第一个字符开始,依次向后遍历,直到找到包含字符串t中的元素或是超过s长度为止,找到所有符合要求的,找出最小的长度的子串

解法二:滑动窗口 + 哈希 + count计数

假设从left开始,直到right找到了符合条件的字符串,此时记录这个字符串的长度以后,再left++,此时会有两种情况:

①left++后符合要求,所以right不动,此时可以继续更新结果
②left++后不符合要求,此时right就需要右移


和上面异位词的一样,可以设一个count值,用于取代每次遍历两个hash数组,提高效率

这里的count与异位词的count代表的含义不同,这里的count代表的是hashs数组中的有效种类,不是个数,例如目标子串是"abb"此时就只需要统计有效种类是两种,在进窗口后,如果该字符对应的个数和目标子串的个数一样,那就表示是有效字符,此时count++

当出窗口时,如果此时的字符对应的个数和目标子串的个数一样,就需要count--

在出完窗口,如果还是满足条件的,就继续更新结果

①left=0,right=0,count = 0
②进窗口,若++hasht[in] == hasht[in] -> count++
④判断是否更新结果,count == kinds时,更新结果
③判断是否出窗口,hashs[out] == hasht[out] -> count--,否则count就不变,继续循环更新结果


代码如下:

class Solution {
public:
    string minWindow(string s, string t) {
        int ns = s.size(), len = INT_MAX, kinds = 0, begin = -1;
        int hashs[128] = {0};
        int hasht[128] = {0};
        for (auto& it : t) 
            if (hasht[it]++ == 0) kinds++;
        for (int left = 0, right = 0, count = 0; right < ns; right++) 
        {
            char in = s[right]; // 进窗口的字符in
            if (++hashs[in] == hasht[in]) count++; // 进窗口 + 维护count
            while (count == kinds) 
            {
                if (right - left + 1 < len) // 更新结果
                {
                    len = right - left + 1;
                    begin = left;
                }
                char out = s[left++];// 出窗口的字符out
                if (hashs[out]-- == hasht[out]) count--; // 出窗口 + 维护count
            }
        }
        if (begin == -1) return ""; //说明没有符合条件的子串
        return s.substr(begin, len);
    }
};

  • 29
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值