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

最长子串的长度 最长子串 的长度 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。 示例 2: 输入: “bbbbb” 输出: 1 解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。 示例 3: 输入: “pwwkew” 输出: 3 解释: 因为无重复字符的最长子串... 阅读详情

目录

滑动窗口算法介绍

①力扣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 <= 10^5
  • 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);
	}
};

本篇完。

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

(穿越回来复习二刷顺便贴个下篇链接:Offer必备算法03_二分查找_八道力扣OJ题详解(由易到难)-优快云博客)

每日算法:最长子串 题目 无重复字符的最长子串 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。 示例 2: 输入: “bbbbb” 输出: 1 解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。 示例 3: 输入: “pwwkew” 输出: 3 解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。 请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序 阅读详情

相关推荐

Sigmastar平台WDR调试实战:从参数解析到鬼影消除的完整指南

本文提供了Sigmastar平台宽动态(WDR)技术的实战调试指南。针对逆光、运动场景下的核心痛点,系统性地解析了从多曝光采集、HDR融合到色调映射的完整管线,并重点阐述了如何通过调整曝光比、运动阈值与调整曲线等关键参数,有效消除运动鬼影、平衡暗部噪声与细节,最终实现清晰稳定的图像效果。

Jerry的专栏 927

力扣大厂热门面试算法题 - 滑动窗口

最长和谐子序列、重复的DNA序列、找到字符串中所有字母异位词、滑动窗口最大值、最小区间。每题做详细思路梳理,配套Python&Java双语代码, 2024.03.06可通过leetcode所有测试用例。

引用原创内容标明出处即可 1337

《雷达入门课 》系列文章(5)线性调频连续波雷达基本原理(第1讲)

S=B/Tc,是调频斜率。这里的混频公式推导过程比较简单,具体的混频过程如果感兴趣可以看下面粗略的推导,混频的本质就是频谱搬移,分为上变频和下变频,一般我们用下变频,即差频信号,因此需要一个低通滤波器即可滤出我们所需要的信号。答案就是因为中频信号能够映射出目标的距离信息,换句话说就是目标的距离不一样,返回的时间τ也就不一样,因此中频信号的频率大小也就不一样,可以说距离和中频信号是成映射关系的。中频只是表示信号频段的程度词,不是表示信号的状态词,意思就是说中频是指信号的频率不是很高,正好处于中间的地位。

【调皮连续波】雷达技术领域知识博主 6024

NENU OJ算法2例题||搜索E

网格中的每个单元或是干的或是被淹没的,且恰有K个单元被水淹没,(1≤K≤N*M)。任何两个相邻的X,其网格方形区域在边界或者拐角处是重叠的,所以他们的网格方形区域是相邻的。对每个网格,第一行是网格的行列数(rows,columns),鼠标单击的行列号(row,column),其整数范围都是1-20.接下来就是rows行,由字符‘.’和‘X’构成。在网格2中有两个目标,其中一个目标只占左下角的一个网格方形区域,其余的X属于另一个目标。第一行描述与转发器A邻近的,第二行描述与B邻近的,直到描述完所有的转发器。

Florae006的博客 1073

HTB Attacking GraphQL Skills Assessment

通过枚举应用,利用GraphQL API,发现sql注入点,然后利用找到flag。

qq_33522010的博客 453

新入手的Atmel-ICE产品说明

【一句话自我介绍】 我是Atmel最新推出的功能强大的调试编程工具,适用于Atmel ARM® Cortex®-M的Atmel SAM和AVR® 系列芯片。 【我支持的软件】 完美支持Atmel Studio 6.2以上版本直接支持ICC8AVR以上版本支持烧录ICCAVR、CVAVR、IAR等生成的HEX文件 【我支持的器件】 支持AVR 32-bit MCUs的JTA

kaloha3的专栏 1万+

最长子串

描述: 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。 示例 2: 输入: “bbbbb” 输出: 1 解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。 示例 3: 输入: “pwwkew” 输出: 3 解释: 因为无重复字符的最长子串是 “wk...

qq_35367566的博客 1924

AI驱动的研发流程:定义高度专业和系统化的规划基准

在人工智能(AI)浪潮席卷全球的今天,软件研发的范式正经历着一场深刻而迅速的变革。AI技术的渗透,从最初的辅助工具角色,正逐步演变为驱动整个研发流程的核心引擎。传统的研发流程和项目管理方法论,在面对AI带来的高效率、高智能以及高复杂性挑战时,显得力不从心。企业对快速交付高质量软件产品、有效控制风险、并保证全流程可追溯性的需求日益迫切。

形上得其象,形下合于数;阴阳自济,玄理自明。 2181

LeetCode No3. 无重复字符的最长子串 题解

文章目录一、题目二、算法思想三、示例四、代码 一、题目 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。 示例 1: 输入: s = “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。 示例 2: 输入: s = “bbbbb” 输出: 1 解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。 示例 3: 输入: s = “pwwkew” 输出: 3 解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。

zheliku的博客 2540

Redis+Unity 数据库搭建

游戏中需要存放排行榜等数据,而且是实时存放,所以就涉及到数据库的问题。这里找服务器大神了解到可以用Redis来做存储,免费的效率极高。

chaixinke的专栏 879

学Simulink--基于电力电子变换器的建模与仿真实例:LLC谐振变换器在高频AC-DC转换中的建模

本文详细介绍了使用Simulink+Simscape Electrical对LLC谐振变换器进行建模与仿真的完整流程。LLC谐振变换器因其高效率、低噪声的特点,广泛应用于高频AC-DC转换场景。文章从背景原理出发,逐步讲解系统结构设计、建模步骤(包括AC源、整流器、LLC变换器、负载等模块搭建)、控制策略实现(固定频率/变频/PID控制)和仿真设置。通过分析仿真结果的关键指标,提出参数整定和算法优化建议。该建模方法为电力电子设计人员提供了实用的仿真工具,有助于优化LLC变换器性能,缩短开发周期。

xiaoheshang_123的博客 298

Offer必备算法01双指针_八道力扣OJ题详解(由易到难)

Offer必备算法01双指针_八道力扣OJ题详解,力扣283+1089+202+11+611+15+18+剑指Offer57。(由易到难)

参考书籍:《C语言程序设计》《数据结构》《C++ Primer》《Effective C++》《STL源码剖析》《现代操作系统》《UNIX环境高级编程》《图解TCP/IP》《图解HTTP》《Linux高性能服务器编程》《剑指Offer》《算法导论》 1684

【IC前端虚拟项目】spyglass lint环境组织与lint清理

和上个虚拟项目的lint清理环节一样,关于spyglass的lint清理功能与流程还是大家通过各种资料去学习下就好啦。和之前不同的事,这次的虚拟项目里我把流程封装为Makefile,更加的贴近了实际项目交付流程。而后就可以根据提示进行lint的清理工作了。

moon9999的博客 1886

LintCode之最长公共子串(Longest Common Substring)

1、 首先区别最长公共子串和最长公共子序列 找两个字符串的最长公共子串,这个子串要求在原字符串中是连续的。而最长公共子序列则并不要求连续。 2、 最长公共子串 其实这是一个序贯决策问题,可以用动态规划来求解。使用一个二维矩阵来记录中间结果。 eg: abcd和bca 列 a b c d b 0 1 0 0 c 0 0 2 0 a

Vtamins的博客 2534

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

视频隐私脱敏工程化:CSRT/KCF追踪算法在本地推理中的实现与边界

在短视频内容合规审查中,动态遮挡(Dynamic Masking)是高频且高耗时的环节。传统手动关键帧打码不仅效率低下,且在目标快速移动或遮挡时极易出现遮罩漂移。本文从计算机视觉工程角度,解析基于CSRT与KCF算法的目标追踪方案在视频隐私处理中的技术路径、性能权衡及落地实践。

Mysssone的博客 270

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

【算法拾光】链表 哈希表|K个一组翻转链表 + 两数之和

K个一组翻转链表需分段反转,借助哑节点与前驱指针维护连接,避免断链;两数之和利用哈希表实现O(1)查找,空间换时间,高效定位目标组合。前者考察链表指针操作,后者聚焦映射关系构建,均为面试高频题。

wuqingyigexiaoba的博客 324

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

把 KV Cache 压到 FP4:BitDecoding-FP4 的算法-系统协同设计复盘

长上下文推理卡在KV Cache的显存与带宽上,而现有FP4方案要么精度崩、要么速度废。这篇文章复盘BitDecoding-FP4如何用三个算法补丁加一个系统重构,在B300上把服务吞吐提升2.4–3.0倍,精度几乎不掉。

helloagent的博客 305

排序算法深度笔记(下):非比较类排序的线性突破与工程边界

排序算法时间复杂度额外空间复杂度稳定性是否基于比较选择排序O(N^2)(O(1))不稳定是冒泡排序O(N^2)(O(1))稳定是插入排序O(N^2)(O(1))稳定是归并排序O(N\log N)(O(N))稳定是快速排序O(N\log N)O(log N)不稳定是堆排序O(N\log N)O(1)不稳定是计数排序(O(N + M))O(M))稳定否基数排序O(N)稳定否桶排序平均O(N) / 最坏O(N^2)

2401_87069538的博客 599

优选算法:分治-归并排序

交易逆序对的总数

Wangshijie1015的博客 144

算法日常・每日刷题--<动态规划>42

本题为“不相交的线”,与最长公共子序列问题相似。使用动态规划求解:定义 dp[i][j] 表示 nums1[0:i] 与 nums2[0:j] 的最大不相交连线数。若元素相等,则继承左上值加1;否则取上方或左方最大值。最终返回 dp[m][n],即两数组的最大不相交连线数。时间复杂度 O(mn),空间复杂度 O(mn)。

2503_92261635的博客 23

408 算法 06|堆与 Top-K:最小的 10 个数,到底该用数组还是堆

2022(42) 找 100 万个数里最小的 10 个,官方给了"10 元素有序数组"和"大根堆"两种答案。我把两种都实现并数了比较次数:k=10 时两者几乎打平,k=10000 时堆少 111 倍。另附 2021(42) 计数排序的比较次数与稳定性实测。

m0_56226131的博客 358

408 算法 04|主元素与三元组最小距离:两种“扫描时做决定“的思路

2013(41) 的主元素用摩尔投票一遍选出候选,但真正的考点是"选完必须验票";2020(41) 的三元组最小距离,三指针每次只挪最小的那个,靠的是 D=2×(max−min) 这个恒等式。两套完整 C 代码加真实输出,附官方评分档位表。

m0_56226131的博客 416

牛客周赛165C题(前后缀性质,递推,排序,枚举)

题目大意:给定长度为 `n` 的数组,选择 `k ∈ [1,n]`,将前 `k` 个元素整体翻转,后面的元素不变。要求操作后的整个数组非递减,统计满足条件的 `k` 的数量。题目链接:https://ac.nowcoder.com/acm/contest/141721/C。时间复杂度:O(nlogn)

2501_94316951的博客 172

【MPC 算法】Python 实战:模型预测控制(MPC)算法原理与代码实现

本文介绍模型预测控制(MPC)原理与Python实现。MPC通过滚动优化预测未来状态,结合系统模型、目标函数与约束条件,动态生成最优控制序列。以一阶温度系统为例,使用CVXPY构建凸优化问题,实现加热功率的智能调控。代码包含模型预测、目标函数设计(含温度跟踪与控制平滑惩罚)、输入约束及滚动更新机制。仿真结果表明,MPC能快速稳定温度至目标值60℃,且控制动作平滑合理,体现其在复杂系统中的优越性。

赖德发的博客 33

算法61,判定是否为字符重排

两字符串能互为重排 ⇔。

sheshibenniaodan的博客 221

算法日常・每日刷题--<动态规划>32

给定严格递增的正整数数组,求最长斐波那契子序列长度。使用二维DP,dp[i][j]表示以arr[i]、arr[j]为最后两项的序列长度,初始化均为2。通过哈希表快速查找前一项target = arr[j] - arr[i],若存在且下标小于i,则状态转移:dp[i][j] = dp[k][i] + 1。遍历更新全局最大长度,最终若结果小于3则返回0。时间复杂度O(n²),空间复杂度O(n²)。

2503_92261635的博客 180
上一篇: Offer必备算法01双指针_八道力扣OJ题详解(由易到难)
下一篇: Offer必备算法03_二分查找_八道力扣OJ题详解(由易到难)
GR鲸鱼
GR鲸鱼 新星创作者: C/C++技术领域 新星创作者: C/C++技术领域
博客等级 码龄4年 6227粉丝 228原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GR鲸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值