Offer必备算法_滑动窗口_八道力扣OJ题详解(由浅到深)

目录

滑动窗口算法介绍

①力扣209. 长度最小的子数组

解析及代码

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

解析及代码

③力扣1004. 最大连续1的个数 III

解析及代码

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

解析及代码

⑤力扣904. 水果成篮

解析及代码1(使用容器)

解析及代码2(开数组)

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

解析及代码1

解析及代码2

⑦力扣30. 串联所有单词的子串

解析及代码

⑧力扣76. 最小覆盖子串

解析及代码

本篇完。


滑动窗口算法介绍

        滑动窗口算法(Sliding Window Algorithm)是一种常见的算法技巧,用于解决一些数组或字符串相关的问题。滑动窗口算法的本质是同向双指针,双指针算法已经在前面博客讲过了,所谓滑动窗口,顾名思义可以理解成是一个会滑动的窗口,每次记录下窗口的状态,再找出符合条件的适合的窗口。它可以将双层嵌套的循环问题,转换为单层遍历的循环问题。使用两个指针一左一右构成一个窗口,就可以将二维循环的问题转化成一维循环一次遍历,相当于通过旧有的计算结果对搜索空间进行剪枝,使时间复杂度从O(N^2)降低至O(N),比如经典字符串查找算法Rabin-Karp 指纹字符串查找算法,它本质上也使用了滑动窗口的思想,通过公式推导降低窗口滑动时计算子串哈希值的复杂度。

算法原理:

  1. 窗口大小:滑动窗口算法通过设定一个窗口的大小来解决问题。窗口通常是一个连续的子数组或子字符串。
  2. 初始化窗口:初始化窗口的起始位置,并根据问题需求设定窗口的大小。
  3. 移动窗口:通过移动窗口的起始位置,不断调整窗口的大小和位置,以找到满足问题条件的解。
  4. 更新解:根据窗口的移动和调整,更新问题的解,并记录或返回所需的结果。

应用场景:

  • 最小/最大子数组/子字符串:寻找给定数组或字符串中满足特定条件的最小或最大的子数组或子字符串。
  • 字符串匹配:在一个字符串中寻找另一个字符串的出现或满足特定条件的子串。
  • 滑动窗口和哈希表结合:通过使用哈希表来优化滑动窗口算法,提高效率。
  • 优化窗口大小:根据问题的特性,调整窗口大小以寻找最佳解。

算法通常步骤:

  1. 初始化窗口的起始位置和结束位置,使其满足问题的要求。
  2. 进入循环,不断移动窗口的起始位置和结束位置,直到窗口滑动到数组或字符串的末尾。
  3. 在每一次循环中,检查窗口内的元素是否满足问题的要求。如果满足条件,则更新解或执行其他操作。如果不满足条件,则继续移动窗口。
  4. 在移动窗口时,要更新窗口内的元素和相应的数据结构,以确保窗口的正确性。
  5. 重复步骤2到步骤4,直到遍历完整个数组或字符串,返回解或所需的结果。

①力扣209. 长度最小的子数组

209. 长度最小的子数组 - 力扣(LeetCode)

难度 中等

给定一个含有 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 <= 10^9
  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^5

进阶:

  • 如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。
class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {

    }
};

解析及代码

首先想到的是暴力枚举,定义两个指针和一个变量,可以做到时间为O(N^2)。

由于此问题分析的对象是一段连续的区间,因此可以考虑滑动窗口的思想来解决这道题。

本题为变长的滑动窗口,其窗口大小由target决定(窗口总和大于等于target),所以定义一个变量(窗口内总和),小于target右指针右移,大于target左指针右移,等于记录更新窗口大小即可。

i 位置开始,窗口内所有元素的和小于 target让滑动窗口满足:(那么当窗口内元素之和第一次大于等于目标值的时候,就是 i 位置开始,满足条件的最小长度)
做法:将右端元素划入窗口中,统计出此时窗口内元素的和:如果窗口内元素之和大于等于 target : 更新结果,并且将左端元素划出去的同时继续判断是否满足条件并更新结果(因为左端元素可能很小,划出去之后依旧满足条件,如果窗口内元素之和不满足条件:right++ ,另下一个元素进入窗口。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        // 时间:O(N)
        int n = nums.size(), ret = n + 1; // 返回值不可能超过数组长度
        int sum = 0, left = 0, right = 0;
        while(right < n)
        {
            sum += nums[right]; // 进窗口
            while(sum >= target) // 判断
            {
                ret = min(ret, right - left + 1); // 更新结果
                sum -= nums[left++]; // 出窗口 然后left++
            }
            ++right; // 判断不符合了
        }
        return ret == n + 1 ? 0 : ret;
    }
};

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

本质是利用单调性规避了很多不必要的枚举。
这个窗口寻找的是:以当前窗口最左侧元素 (记为 left)为基准,符合条件的情况。也就是在这道题中,从 left 开始,满足区间和 sum >= target 时的最右侧 (记为right))能到哪里。
我们既然已经找到从 left 开始的最优的区间,那么就可以大胆舍去  left 。但是如果继续像暴力枚举,重新开始统计第二个元素 后的和,势必会有大量重复的计算 (因为我们在求第一段区间的时候,已经算出很多元素的和了,这些和是可以在计算下次区间和的时候用上的)。
此时,rigth 的作用就体现出来了,我们只需将 left 这个值从 sum 中剔除。从right 这个元素开始,往后找满足的区间(此时 right 也有可能是满足的,因为 left 可能很小。 sum 剔除掉 left 之后,依旧满足大于等于target )。这样就能省掉大量重复的计算。

这样不仅能解决问题,而且效率也会大大提升时间复杂度:虽然代码是两层循环,但是 left 指针和 right 指针都是不回退的,两者最多都往后移动 N 次。因此时间复杂度是 0(N)。


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

3. 无重复字符的最长子串 - 力扣(LeetCode)

难度 中等

给定一个字符串 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 由英文字母、数字、符号和空格组成
class Solution {
public:
    int lengthOfLongestSubstring(string s) {

    }
};

解析及代码

  • 研究的对象是一段连续的区间,因此继续使用「滑动窗口」思想来写。让滑动窗口满足:窗口内所有元素都是不重复的。
  • 假设选择字符串中的第 left 个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为 right。那么当我们选择第 left+1个字符作为起始位置时,首先从 left+1到 right的字符显然是不重复的,并且由于少了原本的第 left个字符,可以尝试继续增大 right,直到右侧出现了重复字符为止,出现重复字符时,left指针可以直接移动到重复字符的右边。
  • 那么使用两个指针表示字符串中的某个子串的左右边界。其中左指针代表着上文中「枚举子串的起始位置」,而右指针即为上文中的 right;
    在每一步的操作中,会将左指针向右移动一格,表示开始枚举下一个字符作为起始位置,然后可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。在移动结束后,这个子串就对应着以左指针开始的,不包含重复字符的最长子串。记录下这个子串的长度。
  • 在枚举结束后,找到的最长的子串的长度即为答案。

这里可以借助哈希的思想:

右端X元素进入窗口的时候,哈希表统计这个字符的频次:
如果这个字符出现的频次超过1,说明窗口内有重复元素,那么就从左侧开始划出窗口,
直到X这个元素的频次变为1,然后再更新结果。
如果没有超过1, 说明当前窗口没有重复元素,可以直接更新结果。

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        // 时间O(N), 空间O(1)
        int hash[128] = { 0 }; // 数组模拟哈希表->s由英文字母、数字、符号和空格组成
        int n =  s.size(), ret = 0, left = 0, right = 0;
        while(right < n)
        {
            hash[s[right]]++; // 进窗口->进入哈希表
            while(hash[s[right]] > 1)
            {
                hash[s[left++]]--; // 出窗口->哈希表的值减减然后left加加
            }
            ret = max(ret, right - left + 1); // 更新结果
            ++right; // 下一个元素进入窗口
        }
        return ret;
    }
};

③力扣1004. 最大连续1的个数 III

1004. 最大连续1的个数 III - 力扣(LeetCode)

难度 中等

给定一个二进制数组 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 <= nums.length <= 10^5
  • nums[i] 不是 0 就是 1
  • 0 <= k <= nums.length
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {

    }
};

解析及代码

此题可转化为找出最长子数组,子数组0的个数不超过K个,就和前两篇滑动窗口的题目一样了。

设置计数器,计算0的个数

首先数组不能越界,所以最外层循环进入条件为right<n(数组长度)

滑动窗口四步走:

  1. 进窗口,遇到0计数器++,否则忽略( ps:滑动窗口的进窗口操作是载入数据,而不是单纯的移动right指针,如果只移动了right而没有对数据进行处理,则不算进窗口。)
  2. 判断,0个数是否大于 题目中的k (不是等于而是大于,这是因为如果在计数器的0个数等于k时就停止进窗口,并更新状态,那么就会漏情况。比如k为2 那么数组 1  1  0  0  1  1  1 1 这个数组中最长子串的长度就是8,如果是在0等于2个时就停下,那么更新的结果就会是4。)
  3. 出窗口,left移动,遇到0时计数器--,直到计数器的0个数等于k个,符合题目要求为止。
  4. 更新结果,无论是否出窗口都更新状态。只要用个max函数来判断此时的子串是否是最大长度即可。
class Solution {
public:
    int longestOnes(vector<int>& nums, int k) {
        // 时间O(N), 空间O(1)
        int n = nums.size(), ret = 0, left = 0, right = 0, zero = 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;
    }
};

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

1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)

难度 中等

给你一个整数数组 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
class Solution {
public:
    int minOperations(vector<int>& nums, int x) {

    }
};

解析及代码

转化成了最长子数组的长度,所有元素的和正好等于数组和 - x。

题意是从两边找几个数,使其的和等于x,从两边找的话很难,因为有时候删左边,有时候删右边,所以“正难则反”,可以找中间有没有等于数组和 - x的区间,这样两边的和就是x了。

class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        // 时间O(N), 空间O(1)
        int tmp = 0;
        for(auto& e : nums)
        {
            tmp += e;
        }
        int target = tmp - x;
        if(target < 0)
        {
            return -1;
        }

        int n = nums.size(), len = -1, sum = 0, left = 0, right = 0;
        while(right < n) // 找最大子数组的和,长度为len
        {
            sum += nums[right];
            while(sum > target) // 判断
            {
                sum -= nums[left++]; // 出窗口 然后left++
            }
            if(sum == target)
            {
                len = max(len, right - left + 1); // 更新结果
            }
            ++right; // 判断不符合了
        }
        return len == -1 ? len : (n - len);
    }
};

⑤力扣904. 水果成篮

904. 水果成篮 - 力扣(LeetCode)

难度 中等

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 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 = [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] 这五棵树。

提示:

  • 1 <= fruits.length <= 10^5
  • 0 <= fruits[i] < fruits.length
class Solution {
public:
    int totalFruit(vector<int>& fruits) {

    }
};

解析及代码1(使用容器)

研究的对象是⼀段连续的区间,可以使用「滑动窗口」思想来解决问题。

让滑动窗口满足:窗口内水果的种类只有两种。

做法:右端水果进入窗口的时候,用哈希表统计这个水果的频次。这个水果进来后,判断哈希表的大小:

如果大小超过2:说明窗口内水果种类超过了两种。那么就从左侧开始依次将水果划出窗口,直到哈希表的大小小于等于2,然后更新结果
如果没有超过2:说明当前窗口内水果的种类不超过两种,直接更新结果 ret

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        unordered_map<int, int> hash;
        int ret = 0, left = 0, right = 0;
        while(right < fruits.size())
        {
            hash[fruits[right]]++; // 进窗口
            while(hash.size() > 2) // 判断
            {
                hash[fruits[left]]--; // 出窗口
                if(hash[fruits[left]] == 0)
                {
                    hash.erase(fruits[left]);
                }
                ++left;
            }
            ret = max(ret, right - left + 1);
            ++right;
        }
        return ret;
    }
};


解析及代码2(开数组)

可以看到上面的代码的通过效率还是很差的,因为容器的删除耗了时间,注意到这题的范围,此时就可以开一个数组来代替容器:

class Solution {
public:
    int totalFruit(vector<int>& fruits) {
        // unordered_map<int, int> hash;
        int hash[100001] = { 0 }, kinds = 0; // 有数据范围->数组代替容器->提高效率
        int ret = 0, left = 0, right = 0;
        while(right < fruits.size())
        {
            if(hash[fruits[right]] == 0) // 维护数组水果种类
            {
                ++kinds; 
            }
            hash[fruits[right]]++; // 进窗口
            // while(hash.size() > 2) // 判断
            while(kinds > 2) // 判断
            {
                hash[fruits[left]]--; // 出窗口
                if(hash[fruits[left]] == 0)
                {
                    // hash.erase(fruits[left]);
                    --kinds;
                }
                ++left;
            }
            ret = max(ret, right - left + 1);
            ++right;
        }
        return ret;
    }
};

此时效率就会提高一些。


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

438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

难度 中等

给定两个字符串 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 仅包含小写字母
class Solution {
public:
    vector<int> findAnagrams(string s, string p) {

    }
};

解析及代码1

首先用两个数组做哈希表敲出来的代码:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int hash1[26] = { 0 }, hash2[26] = { 0 };
        for (auto& e : p)
        {
            hash2[e - 'a']++;
        }
        int left = 0, right = 0, n1 = s.size(), n2 = p.size();

        vector<int> ret;
        if(n2 > n1)
            return ret;
        while (right < n2)
        {
            hash1[s[right++] - 'a']++;
        }
        while (right < n1)
        {
            int j = 0;
            for (; j < 26; ++j)
            {
                if (hash1[j] != hash2[j])
                {
                    break;
                }
            }
            if (j == 26)
            {
                ret.push_back(left);
            }
            hash1[s[right++] - 'a']++;
            hash1[s[left++] - 'a']--;
        }
        int j = 0;
        for (; j < 26; ++j) // 再判断一遍
        {
            if (hash1[j] != hash2[j])
            {
                break;
            }
        }
        if (j == 26)
        {
            ret.push_back(left);
        }
        return ret;
    }
};

解析及代码2

改进:用一个变量count维护判断逻辑

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
        int hash1[26] = { 0 }, hash2[26] = { 0 };
        for (auto& e : p)
        {
            hash2[e - 'a']++;
        }
        int left = 0, right = 0, n1 = s.size(), n2 = p.size(), count = 0;

        vector<int> ret;
        if(n2 > n1)
            return ret;

        while (right < n1)
        {
            char in = s[right];
            if(++hash1[in - 'a'] <= hash2[in - 'a'])
            {
                ++count;  // 进窗口和维护count
            }

            if(right - left + 1 > n2) // 判断
            {
                char out = s[left++];
                if(hash1[out - 'a']-- <= hash2[out - 'a'])
                {
                    --count; // 出窗口和维护count
                }
            }

            if (count == n2) // 更新结果
            {
                ret.push_back(left);
            }
            ++right;
        }
        return ret;
    }
};

⑦力扣30. 串联所有单词的子串

30. 串联所有单词的子串 - 力扣(LeetCode)

难度 困难

给定一个字符串 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 <= 10^4
  • 1 <= words.length <= 5000
  • 1 <= words[i].length <= 30
  • words[i] 和 s 由小写英文字母组成
class Solution {
public:
    vector<int> findSubstring(string s, vector<string>& words) {

    }
};

解析及代码

此题和上一题力扣438题思路类似,也是滑动窗口+哈希表,比较考验容器的使用,用一个变量count维护判断逻辑。

class Solution {
public:
	vector<int> findSubstring(string s, vector<string>& words) {
		vector<int> ret;
		unordered_map<string, int> hash2; // 保存 words ⾥⾯所有单词的频次
		for (auto& e : words) 
        {
            hash2[e]++;
        }
		int len = words[0].size(), m = words.size();
		for (int i = 0; i < len; i++) // 执⾏ len 次
		{
            int left = i, right = i, count = 0;
			unordered_map<string, int> hash1; // 维护窗⼝内单词的频次
			while (right + len <= s.size())
			{
				string in = s.substr(right, len); // 进窗⼝ + 维护 count
				hash1[in]++;
				if (hash2.count(in) && hash1[in] <= hash2[in])
                {
                    count++;
                }
				if (right - left + 1 > len * m) // 判断
				{
					// 出窗⼝ + 维护 count
					string out = s.substr(left, len);
					if (hash2.count(out) && hash1[out] <= hash2[out])
                    {
                        count--;
                    }
					hash1[out]--;
					left += len;
				}
				if (count == m) // 更新结果
                {
                    ret.push_back(left);
                }
                right += len;
			}
		}
		return ret;
	}
};

⑧力扣76. 最小覆盖子串

76. 最小覆盖子串 - 力扣(LeetCode)

难度 困难

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

提示:

  • m == s.length
  • n == t.length
  • 1 <= m, n <= 105
  • s 和 t 由英文字母组成
  • 进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗?
class Solution {
public:
	string minWindow(string s, string t){

    }
};

解析及代码

此题和上篇力扣30题思路类似,也是滑动窗口+哈希表,用一个变量count维护判断逻辑。

class Solution {
public:
	string minWindow(string s, string t){
		int hash1[128] = { 0 }; // 统计字符串 t 中每⼀个字符的频次
		int kinds = 0; // 统计有效字符有多少种
		for (auto& e : t)
		{
			if (hash1[e]++ == 0)
			{
				kinds++;
			}
		}
		int hash2[128] = { 0 }; // 统计窗⼝内每个字符的频次
		int minlen = INT_MAX, begin = -1;
		for (int left = 0, right = 0, count = 0; right < s.size(); 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--;
				}
			}
		}
		return begin == -1 ? "" : s.substr(begin, minlen);
	}
};

本篇完。

下一部分是二分查找算法的讲解和刷题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/344150.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

初识SQL注入

目录 注入攻击 SQL注入 手工注入 Information_schema数据库 自动注入 介绍一下这款工具&#xff1a;sqlmap 半自动注入 前面给大家通过学习练习的方式将XSS攻击的几种形式和一些简单的靶场和例题的演示&#xff0c;从本篇开始我将和小伙伴们通过边复习、边练习的方式来进…

2024年购买阿里云服务器多少钱?1000元左右预算可购买的8款云服务器参考

1000元左右预算可以买到哪些配置的阿里云服务器&#xff1f;目前阿里云活动中价格在1000元左右的云服务器有8款&#xff0c;其中经济型e实例云服务器三款&#xff0c;通用算力型u1实例云服务器五款&#xff0c;碰到阿里云有优惠券或者代金券活动时&#xff0c;购买过程中还能使…

Angular组件(一) 分割面板ShrinkSplitter

Angular组件(一) 分割面板ShrinkSplitter 前言 分割面板在日常开发中经常使用&#xff0c;可将一片区域&#xff0c;分割为可以拖拽整宽度或高度的两部分区域。模仿iview的分割面板组件&#xff0c;用angular实现该功能&#xff0c;支持拖拽和[(ngModel)]双向绑定的方式控制区…

Dock的安装部署和基础命令

1 Docker基础 1.1 Docker概述 Docker是一个开源的应用容器引擎&#xff0c;用来运行容器里的运用&#xff0c;可以用来管理容器和镜像的一种工具&#xff0c;基于go语言开发并遵循了apache2.0协议开源。 Docker是在Linux容器里运行应用的开源工具&#xff0c;是一种轻量级的…

Java(TM) Platform SE binary (Process Id: 4412)

Java™ Platform SE binary (Process Id: 4412&#xff09;JAVA8安装过程中出现上述问题win10解决方法 打开任务管理器 在任务管理器中找到详细信息&#xff0c;然后根据上边的进程id找到对应的程序&#xff0c;右键结束任务即可。 在安装jdk17时候&#xff0c;同时出现了上…

05 双向链表

目录 1.双向链表 2.实现 3.OJ题 4.链表和顺序表对比 1. 双向链表 前面写了单向链表&#xff0c;复习一下 无头单向非循环链表&#xff1a;结构简单&#xff0c;一般不会单独用来存数据。实际中更多作为其他数据结构的子结构&#xff0c;如哈希桶、图的邻接等。另外这种结构在…

Pycharm中出现Comparison with None performed with equality operators

此图中警告翻译过来是 &#xff1a;与使用相等运算符执行的None进行比较 这里不应该使用 或者 &#xff01; 而应改为 is 或者 is not

聚观早报 | 华为P70 Art细节曝光;红魔9 Pro龙年限定版官宣

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 1月24日消息 华为P70 Art细节曝光 红魔9 Pro龙年限定版官宣 努比亚Z60 Ultra龙年限定版 小米14 Ultra或没有双版…

创新医疗服务:宠物在线问诊系统的搭建与应用

随着科技的不断进步&#xff0c;创新的医疗服务方式也日渐成为宠物主人关心爱宠健康的首选。本文将深入介绍如何搭建一套创新的宠物在线问诊系统&#xff0c;并展示其应用的技术代码。 1. 系统架构与技术选择 在开始搭建之前&#xff0c;我们需要设计系统的架构并选择合适的…

【Linux工具篇】编辑器vim

目录 vim的基本操作 进入vim(正常模式&#xff09; 正常模式->插入模式 插入模式->正常模式 正常模式->底行模式 底行模式->正常模式 底行模式->退出vim vim正常模式命令集 vim插入模式命令集 vim末行模式命令集 vim操作总结 vim配置 Linux编译器…

网络安全的使命:守护数字世界的稳定和信任

在数字化时代&#xff0c;网络安全的角色不仅仅是技术系统的守护者&#xff0c;更是数字社会的信任保卫者。网络安全的使命是保护、维护和巩固数字世界的稳定性、可靠性以及人们对互联网的信任。本文将深入探讨网络安全是如何履行这一使命的。 第一部分&#xff1a;信息资产的…

使用Sobel算子把视频转换为只剩边缘部分

效果展示 原始视频 修改后的视频 整体代码 import cv2vc cv2.VideoCapture(test.mp4)if vc.isOpened():open, frame vc.read() else:open Falsei 0 while open:ret, frame vc.read()if frame is None:breakif ret True:i 1# 转换为灰度图gray cv2.cvtColor(frame, cv…

复合机器人颠覆传统上下料,实现高效精准生产

在追求高效、精准生产的现代制造业中&#xff0c;传统的上下料方式已经无法满足企业的需求。复合机器人的出现&#xff0c;为制造业带来了革命性的变革。它不仅提高了生产效率&#xff0c;降低了生产成本&#xff0c;还为企业创造了更大的竞争优势。复合机器人的广泛应用&#…

多维时序 | Matlab实现CNN-BiGRU-Mutilhead-Attention卷积双向门控循环单元融合多头注意力机制多变量时间序列预测

多维时序 | Matlab实现CNN-BiGRU-Mutilhead-Attention卷积双向门控循环单元融合多头注意力机制多变量时间序列预测 目录 多维时序 | Matlab实现CNN-BiGRU-Mutilhead-Attention卷积双向门控循环单元融合多头注意力机制多变量时间序列预测效果一览基本介绍程序设计参考资料 效果一…

GD32E230C8T6《调试篇》之 FMC(闪存)的读写 + USART打印

实验&#xff1a;按键DIG4&#xff08;保存键&#xff09;&#xff0c;任意按下一个数字后&#xff0c;再按保存键写入flash;断电后重新上电&#xff0c;从 flash里读值&#xff0c;显示到数码管 实验工具GD32E230C8T6查看GPIO查看Datasheet 2.6.7章节GPIO 复用 查看用户手册代…

Python Django的学生选课管理系统,实现多用户登录注册,可选课可评课

学生选课管理系统是一个基于Python Django开发的教务管理系统&#xff0c;旨在提供方便快捷的选课服务和学籍管理功能。该系统分为教师端和学生端两个角色&#xff0c;为教师和学生提供了不同的功能和权限。 教师端功能&#xff1a; 教师可以登录系统后&#xff0c;进行课程管…

如何在 Ubuntu 20.04 上安装 Nginx

前些天发现了一个人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;最重要的屌图甚多&#xff0c;忍不住分享一下给大家。点击跳转到网站。 如何在 Ubuntu 20.04 上安装 Nginx 介绍 Nginx是世界上最受欢迎的 Web 服务器之一&#xff0c;负责托管互联网…

【LeetCode-406】根据身高重建队列(贪心)

LeetCode406.根据身高重建队列 题目描述 题目链接 假设有打乱顺序的一群人站成一个队列&#xff0c;数组 people 表示队列中一些人的属性&#xff08;不一定按顺序&#xff09;。每个 people[i] [hi, ki] 表示第 i 个人的身高为 hi &#xff0c;前面 正好 有 ki 个身高大于…

[C++]使用yolov8的onnx模型仅用opencv和bytetrack实现目标追踪

【官方框架地址】 yolov8: https://github.com/ultralytics/ultralytics bytetrack: https://github.com/ifzhang/ByteTrack 【算法介绍】 随着人工智能技术的不断发展&#xff0c;目标追踪已成为计算机视觉领域的重要研究方向。Yolov8和ByTetrack作为当前先进的算法&…

设计模式——1_6 代理(Proxy)

诗有可解不可解&#xff0c;若镜花水月勿泥其迹可也 —— 谢榛 文章目录 定义图纸一个例子&#xff1a;图片搜索器图片加载搜索器直接在Image添加组合他们 各种各样的代理远程代理&#xff1a;镜中月&#xff0c;水中花保护代理&#xff1a;对象也该有隐私引用代理&#xff1a;…