【刷题】 滑动窗口进阶

简介: 这样的优化对于该题的提升是有限的,但是这是一种非常实用的算法,以后还会遇见哦!!!

送给大家一句话:

在最黑暗的那段人生,是我自己把自己拉出深渊。没有那个人,我就做那个人。——中岛美嘉

滑动窗口进阶

前言

继续学习滑动窗口问题。

入门篇我们了解滑动窗口的原理和算法思路:

  1. 双指针如果同方向移动即可使用滑动窗口
  2. 滑动窗口的原理是数组的单调性
  3. 基本步骤:进窗口-判断-出窗口-更新结果

接下来我们继续巩固我们的算法思维。来看三道题目:

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

上连接 !!!1658. 将 x 减到 0 的最小操作数

题目描述

根据描述,就是从左右两边分别取数,直到x减到零。来看一个样例:

  • 输入:nums = [3,2,20,1,1,3], x = 10
  • 输出:5
  • 解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。

算法思路

来看最最最直接的算法:暴力枚举!!!因为只能取两头的元素,所以等价于中间留下的值的和恰好等于数组和减去x。那这样遍历所有的子串,找到最小的即可!!!

怎么进行优化呢?当然是使用单调性来进行优化:

每次我们统计一个子串的和时,想一想right有必要回到 left++ 然后从新开始遍历吗?

当然不用!!!

因为left 到 right 就不满足要求(中间留下的值的和大于数组和减去x),left++后right从left开始最终也会回到原来位置,所以没有必要。

那这样就形成了滑动窗口:

class Solution {
public:
    int minOperations(vector<int>& nums, int x) {
        //进窗口
        //判断
        //出窗口

        //求和
        int sum = 0;
        for(auto s:nums) sum += s;
    
        int tmp = 0;//中间量
        int ans = INT_MAX;
        //特殊情况处理
        if(sum - x == 0) return nums.size();
        if(sum - x < 0) return -1;
        int n = nums.size();
        for(int left = 0,right = 0;right < n;right++)
        {
        //进窗口
            tmp += nums[right];
            //判断
            while(sum - x < tmp && left < right)
            {
              //出窗口
                tmp -= nums[left];
                //
                left++;
            }
            //更新结果
            if(sum - x == tmp) ans = min(ans,n - (right - left +1));

        }
        return ans == INT_MAX ? -1 : ans;
    }
};

这样完美解决!!!

过啦!!!

Leetcode 904. 水果成篮

家人们 !跟上我们的节奏!!! 904. 水果成篮

题目描述

这道题很有意思奥(奇怪的农场主人…)

根据题目描述,我们需要找到一个子串中(只能有两种水果)的最大长度!来看一个样例:

  • 输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]
  • 输出:5
  • 解释:可以采摘 [1,2,1,1,2] 这五棵树。只有两种水果并且最长!

算法思路

直接来最暴力的算法:暴力枚举。遍历所有的子串找到符合条件的最长字串即可(当然肯定会超时!!!)

那么怎么进行优化呢???(当然是滑动窗口)

每次找到不符合条件的子串,有没有必要将right指向left呢???

当然没有!!! 最长的已经遍历过,从头开始完全没有必要!!!

1,2,1,1,2,3 不和条件 就没有必要重新遍历2,1,1,2,3


这里的判断方法借助哈希算法来解决:

class Solution {
public:
    int totalFruit(vector<int>& f) {
        //进窗口
        //判断
        //出窗口
        //更新答案
    //用来统计水果种数
        int hash[100001] = {0} , ret = 0;
        int n = f.size();
        //滑动窗口
        for(int left = 0,right = 0,kinds = 0 ; right < n;right++){
            //进窗口
            //如果哈希表中是0 那说明是新品种
            if(hash[f[right]] == 0) kinds++;
            hash[f[right]]++;
            //判断
            while(kinds > 2){
                hash[f[left]]--;
                //出窗口前要检查种类是否变化
                if(hash[f[left]] == 0) kinds--;
                left++;
            }
            //更新结果
            ret = max(ret,right - left + 1);
        }
        return ret;
    }
};

提交看一下:过啦!!!!!!!!!

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

家人们!上车!!!438. 找到字符串中所有字母异位词

题目描述

这道题很有说法!

根据题目描述,寻找字符串中与 s 组成元素相同的子串。来看一个样例:

  • 输入: s = “abab”, p = “ab”
  • 输出: [0,1,2]
  • 解释:
    起始索引等于 0 的子串是 “ab”, 它是 “ab” 的异位词。
    起始索引等于 1 的子串是 “ba”, 它是 “ab” 的异位词。
    起始索引等于 2 的子串是 “ab”, 它是 “ab” 的异位词。

描述很简单奥

算法思路

不必多说,最简单的算法肯定是暴力枚举!

那么然后进行优化呢???

遇见不符合的新字母,right肯定没有必要回到left(根据前面的算法)。所以直接使用滑动窗口即可。

但是这道题的还有一个难点:如何来进行判断是不是一个字符串的异位词呢???

我的第一种思路是排序算法,先将 p 进行排序。然后之后的每次的子串都排序与之比较:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
    //先排序,方便比较
    sort(p.begin(), p.end());

    //unordered_map<string,int> hash;
    //答案
    vector<int> ans;

    int n = s.size();
    
    //进窗口
    for (int left = 0, right = p.size() - 1; right < n; right++, left++) {
        string tmp = "";
        for (int i = left; i <= right; i++) {
            tmp += s[i];
        }
        sort(tmp.begin(), tmp.end());
        if (strcmp(tmp.c_str(), p.c_str()) == 0) ans.push_back(left);

    }
        return ans;
    }
};

虽然可以通过简单的测试用例,但是这样有一个巨大的弊端,时间复杂度与p的长度成正比(O(nlogn))。看这个测试样例

s 有这么这么长!!!!

s = 
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef

更离谱的是 p 也老长!!!

p = 
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghi

你说我们这样简单算法怎么可能会不超时呢!?

那应该怎么办呢???

同样也是哈希算法,因为只有小写字母,我们创建一个hash[26]就够用了,然后每次长度与p一致,就一一比较就可以了。

来看暴力解法:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) { 
    int hash1[26] = {0};
    for(auto ch:p){
        hash1[ch - 'a']++;
    }
    //答案
    vector<int> ans;
    int n = s.size();

    for (int left = 0, right = p.size() - 1; right < n; right++, left++) {
        //string tmp = "";
        int hash2[26] = {0};
        for(int i = left;i <= right ;i++){
            hash2[s[i]-'a']++;
        }
        int flag = 1;
        //每次进行判断
        for(int i = 0;i < 26;i++){
            if(hash1[i] != hash2[i]) flag = 0;
        }
        if(flag == 1){
            ans.push_back(left);
        }

    }
        return ans;
    }
};

这个可以通过我们的题目但是时间是480ms,还是不够好,这是为什么呢???因为这里我们没有进行滑动窗口的算法核心:跳过不必要的子串,即对哈希表进行抽象的滑动窗口,每次我们没有必要重新遍历子串。对哈希表进行动态处理即可:

class Solution {
public:
    vector<int> findAnagrams(string s, string p) {
    int hash1[26] = {0};
    for(auto ch:p){
        hash1[ch - 'a']++;
    }
    //答案
    vector<int> ans;
    int n = s.size();

    int hash2[26] = {0};
    //进窗口
    for (int left = 0, right = 0; right < n; right++) {
        //string tmp = "";
        //进窗口
        hash2[s[right]-'a']++;
        //满足p的长度 才开始进行
        if(right - left + 1 == p.size()){
            //判断
            int flag = 1;
            for(int i = 0;i < 26;i++){
                if(hash1[i] != hash2[i]) flag = 0;
            }
            if(flag == 1){
                ans.push_back(left);
            }
            //出窗口 
            //将nums[left]移出哈希表即可
            hash2[s[left] -'a']--;
            left++;
        }
    }
        return ans;
    }
};

这样的运算时间就直接来到了 3ms 简直就是降维打击!!!

当然还可以继续优化,我们来看每次检查哈希表的工作是不是简单遍历检查,但是我们不一定有26个字母需要检查!!!所以我们可以设置一个新变量count用来比较有效字母个数

  1. 设置两个哈希表 分别是 p 的对应字母哈希表和子串的对应字母哈希表
  2. 每次进窗口的元素( in )的同时对count进行维护(++hash2[ in - ‘a’ ] <= hash1[ in - ‘a’ ] 就要 ++),只要所需的字母个数大于等于子串的对应字母个数,有效字母就加一
  3. 出窗口的元素(out)同理(hash2[ out - ‘a’]-- <= hash1[ out - ‘a’] 这样进行减一), 如果对应字母的个数大于所需的字母个数,那么就是无效字符,不需要减一。
  4. 如果count == 所需的字母总数,那么进行更新结果。
class Solution
{
public:
  vector<int> findAnagrams(string s, string p)
  {
    vector<int> ret;
    int hash1[26] = { 0 }; // 统计字符串 p 中每个字符出现的个数
    for (auto ch : p) hash1[ch - 'a']++;
    int hash2[26] = { 0 }; // 统计窗⼝⾥⾯的每⼀个字符出现的个数
    int m = p.size();
    for (int left = 0, right = 0, count = 0; right < s.size(); 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;
  }
};

这样的优化对于该题的提升是有限的,但是这是一种非常实用的算法,以后还会遇见哦!!!

送给大家一句话:

在最黑暗的那段人生,是我自己把自己拉出深渊。没有那个人,我就做那个人。——中岛美嘉

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

相关文章
|
7月前
OJ刷题日记:3、滑动窗口(1)
OJ刷题日记:3、滑动窗口(1)
59 0
|
7月前
|
算法
【刷题】滑动窗口入门
滑动窗口问题可以说是一种特殊的双指针问题
36 0
【刷题】滑动窗口入门
|
7月前
|
算法 测试技术 容器
【刷题】双指针进阶
请看入门篇 :双指针入门
33 0
【刷题】双指针进阶
|
7月前
|
算法 索引
【刷题】 二分查找进阶
二分查找的算法思想是很好理解的。朴素二分很容易,但一般常使用左端点查找与右端点查找来解决问题。
42 0
|
7月前
|
算法 C++ 索引
OJ刷题日记:4、滑动窗口(2)
OJ刷题日记:4、滑动窗口(2)
63 0
|
7月前
剑指Offer LeetCode 面试题59 - I. 滑动窗口的最大
剑指Offer LeetCode 面试题59 - I. 滑动窗口的最大
42 0
|
算法
代码随想录算法训练营第十二天 | LeetCode 239. 滑动窗口最大值、LeetCode 347. 前 K 个高频元素
代码随想录算法训练营第十二天 | LeetCode 239. 滑动窗口最大值、LeetCode 347. 前 K 个高频元素
34 0
|
算法
从小白开始刷算法 滑动窗口篇 leetcode.209
从小白开始刷算法 滑动窗口篇 leetcode.209
|
算法
从小白开始刷算法 滑动窗口篇 leetcode.1456
从小白开始刷算法 滑动窗口篇 leetcode.1456
|
存储 人工智能 算法
【C++算法图解专栏】一篇文章带你掌握尺取法(双指针)
【C++算法图解专栏】一篇文章带你掌握尺取法(双指针)
228 0