滑动窗口篇

主要思想

两个滑动窗口的要点:

  • 单调性:数据在某一方向上呈现某一种单调性,此时滑动窗口移动的时候才是有依据的。

  • 左右两个指针:两个指针分别是滑动窗口的两个边,移动指针控制窗口大小。

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

209. 长度最小的子数组 

解题思路:定义两个指针 left 和 right,都从 0 开始移动。

先移动 right,每次向右移动都记录此时窗口内数据和,停止条件是 sum >= target。

此时由于数据的单调性(全是正数,相加一定越来越大),将 left 向右移动一次之后,不用再将 right 从头开始移动,只需将 sum -= nums[left] 即可。 

代码:

class Solution 
{
public:
    int minSubArrayLen(int target, vector<int>& nums) 
    {
        int n = nums.size(), sum = 0, ret = INT_MAX;
        for (int left = 0, right = 0; right < n; right++)
        {
            sum += nums[right];
            while (sum >= target)
            {
                ret = min(ret, right - left + 1);
                sum -= nums[left++];
            }
        }
        return ret == INT_MAX ? 0 : ret;
    }
};

时间复杂度:虽然有两层循环,但是最坏情况是:right 先移动到最后,left 再移动到最后,整个过程中 right 不会回退,所以时间复杂度是 O(n)。

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

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

对于这道题,也满足滑动窗口的两个要点,双指针显而易见,单调性是指:指针 right 向右移动过程中,任意一个字符的数目只可能增多或者不变(滑动窗口内),而 left 右移过程中,任意字符数量只可能不变或者减少。

所以,要借助一个哈希表来记录窗口内,任意时刻,每个字符的个数。

然后就是 right 向右,进窗口,字符数量增加;left 向右,出窗口,字符数量减少。

代码:

class Solution 
{
public:
    int lengthOfLongestSubstring(string s) 
    {
        int n = s.size(), ret = 1;
        vector<int> hash(128, 0);
        for (int left = 0, right = 0; right < n; right++)
        {
            hash[s[right]]++;
            while (hash[s[right]] > 1) hash[s[left++]]--;
            ret = max(ret, right - left + 1);
        }
        return n == 0 ? 0 : ret;
    }
};

时间复杂度还是 O(n)。

力扣 1004. 最大连续 1 的个数(三)

1004. 最大连续 1 的个数(三)

这个题目需要转换一下解决办法,才能用滑动窗口。

保证找到一个最长的区间,其中的 0 的个数不超过 k 即可。

所以单调性指的就是,滑动窗口中 0 的个数,right 向右移动只会不变或增加,left 向右移动只会不变或减少。

代码:

class Solution 
{
public:
    int longestOnes(vector<int>& nums, int k) 
    {
        int n = nums.size(), ret = 0, num = 0;
        for (int left = 0, right = 0; right < n; right++)
        {
            if (nums[right] == 0) num++;
            while (num > k)
            {
                if (nums[left++] == 0)
                {
                    num--;
                }
            }
            ret = max(ret, right - left + 1);
        }
        return ret;
    }
};

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

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

还是得转换,只需要找到一个最长的区间,使得其中的数字之和等于 整个数组的和减去 x,则这个区间之外的数字的个数就是最小操作数。

代码:

class Solution 
{
public:
    int minOperations(vector<int>& nums, int x) 
    {
        int n = nums.size(), sum = 0, len = -1;
        for (int i = 0; i < n; i++) sum += nums[i];
        int target = sum - x;
        if (target < 0) return -1;
        for (int left = 0, right = 0, tmp = 0; right < n; right++)
        {
            tmp += nums[right];
            while (tmp > target) tmp -= nums[left++];
            if (tmp == target) len = max(len, right - left + 1);
        }
        return len == -1 ? len : n - len;
    }
};

力扣 904. 水果成篮

904. 水果成篮

主要就是寻找一个最长的区间,使得其中的数字不超过两种。

所以单调性就是,在一段区间中,不同数字的个数。right 向右移动过程中,区间中的数目只会不变或增加;left 向右移动过程中,区间中的数目只会不变或减少。

class Solution 
{
public:
    int totalFruit(vector<int>& fruits) 
    {
        unordered_map<int, int> hash;
        int n = fruits.size(), ret = 0;
        for (int left = 0, right = 0; right < n; right++)
        {
            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);
        }
        return ret;
    }
};

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

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

这道题能用滑动窗口的一个关键点是,字母异位词只要求每一个出现的字母的个数相等,而不要求顺序。

所以就可以转换为找到几个区间,满足其中每个字母出现的次数和目标字符串相等,即单调性。

class Solution 
{
public:
    vector<int> findAnagrams(string s, string p) 
    {
        vector<int> ret;
        vector<int> hash1(26, 0), hash2(26, 0);
        for (char c : p) hash1[c - 'a']++;
        int m = p.size(), n = s.size();
        for (int left = 0, right = 0, count = 0; right < n; 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.push_back(left);
        }
        return ret;
    }
};

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

30. 串联所有单词的子串

// 只要求包含所有单词并且没有多余的单词, 而且顺序可以不同, 所以这道题里应该看重每一个单词的数量
// 但如果真的每次向后移动一位就判断一下是不是 words 里的某一个单词, 必然会超时
// 所以可以借助一个哈希表进行优化, 就可达到一个类似于"降维"的效果, 将每一个单词看成一个字符
// 然后利用滑动窗口的思想向后移动并判断字符的数量是否一致
// 这里要注意, 有可能出现从第一个字母开始不符合要求, 但从第二个或第三个...开始就符合要求了
// 所以, 一次遍历还是无法得到正确答案, 应该遍历 words[0].size() 次
class Solution 
{
public:
    vector<int> findSubstring(string s, vector<string>& words) 
    {
        vector<int> ret;
        unordered_map<string, int> hash1;
        for (string word : words) hash1[word]++;
        int m = words.size(), n = s.size(), len = words[0].size();
        for (int i = 0; i < len; i++) // 确保不会遗漏
        {
            unordered_map<string, int> hash2; // 窗口内单词的个数
            for (int left = i, right = i, count = 0; right + len <= n; right += len)
            {
                // 进窗口 + 维护 count
                string in = s.substr(right, len);
                if (++hash2[in] <= hash1[in]) count++;
                // 判断需不需要出窗口
                if (right - left + 1 > len * m)
                {
                    // 出窗口 + 维护 count
                    string out = s.substr(left, len);
                    if (hash2[out]-- <= hash1[out]) count--;
                    left += len;
                }
                if (count == m) ret.push_back(left);
            }
        }
        return ret;
    }
};

力扣 76. 最小覆盖子串

76. 最小覆盖子串

// 这道题如果要用滑动窗口的思想解决, 需要注意一个点
// 寻找的区间中不止要满足字符数目相等, 字符的种类也要相同, 每种字符的个数也要相同
// 所以还要使用哈希表来优化, 以及使用两个变量来标记种类和数量
class Solution 
{
public:
    string minWindow(string s, string t) 
    {
        vector<int> hash1(128, 0), hash2(128, 0);
        int kinds = 0, len = INT_MAX, begin = -1, n = s.size();
        for (char c : t)
        {
            if (hash1[c]++ == 0) kinds++;
        }
        for (int left = 0, right = 0, count = 0; right < n; right++)
        {
            // 进窗口 + 维护 count
            char in = s[right];
            if (++hash2[in] == hash1[in]) count++;
            // 在已经满足条件的情况下, 寻找最短长度
            while (count == kinds)
            {
                // 更新结果
                if (right - left + 1 < len)
                {
                    len = right - left + 1;
                    begin = left;
                }
                // 出窗口 + 维护 count
                char out = s[left++];
                if (hash2[out]-- == hash1[out]) count--;
            }
        }
        return begin == -1 ? "" : s.substr(begin, len);
    }
};

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Undefined__yu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值