[LC优选算法#3] 滑动窗口 | 将x减到0的最⼩操作数 | ⽔果成篮 | 字⺟异位词

1. 算法思想

滑动窗口的本质是:维护一个满足条件的连续子数组/子串,通过移动左右边界来"滑动"这个窗口,从而找到最优解。 滑动窗口是更加严格的双指针算法,大致思路都是用两个不回退的指针维护窗口。而且滑动窗口仅支持元素为正数的情况,不适用于负数或0(出窗口的时机无法确定)。

基本步骤: 进窗口 -> 判断条件 -> 出窗口 -> 更新结果

(更新结果的时机因题而异)

解题的关键在于为什么而不是怎么做,因为大多数情况不是不会用而是想不到用滑动窗口,因此思考暴力解法的优化过程才是重中之重。

2. 经典例题

2.1 将 x 减到 0 的最⼩操作数

将 x 减到 0 的最⼩操作数

解题思路:

如果采用正面的思路解决,那么需要使用两个指针向中间移动,很难控制移动的先后与步数;因此我们可以试着采用逆向思维:既然x的加数都源于数组的两端,那么sum - x的加数一定是数组中的一段连续区间

有了连续区间,又有了固定的和作为区间的维护条件,而且数组的元素不存在非负数,此时就是熟悉的滑动窗口问题了。

  1. 滑动窗口 O(N):设sum - x为target,当窗口中的和小于target,进窗口;大于target,反复出窗口;然后更新结果。由于求的是和为x的最小操作数,那么和为target的区间长度就要最大

优化点 :如果数组中所有元素加起来都比x小,则可以直接返回false

计算target需要遍历一次数组,滑动窗口的过程也会遍历一次数组,因此时间为N + N,时间复杂度为O(N)

cpp 复制代码
class Solution {
public:
    int minOperations(vector<int>& nums, int x)
    {
        int left = 0, right = 0, minlen = INT_MAX;
        long long sum = 0;
        int arrsum = 0;
        int n = nums.size();

        // 求和
        for (auto i : nums)
        {
            arrsum += i;
        }
        long long key = arrsum - x;
        if(key < 0) return -1; //所有的数加起来都比x小,则不成立(滑动窗口仅支持大于零的元素)

        while (right < n)
        {
            sum += nums[right];
            
            while (sum > key) //注意>的判断要在==前
            {
                sum -= nums[left++];
            }
        
            if (sum == key)
            {
                int len = n - (right - left + 1);
                minlen = min(minlen, len);
            }

            right++;
        }

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

2.2 水果成篮

水果成篮

解题思路:

  1. 暴力枚举+哈希表 O(N^2):枚举出所有的子数组,用哈希表存储子数组中树的种类,最后得出收集水果的最大数目。
  2. 滑动窗口+哈希表 O(N):由于是连续采摘,我们可以使用滑动窗口算法,并让窗口满足内部的水果种类仅有两种;用哈希表存储篮子中水果的种类。当种类小于等于两种时,进窗口;大于两种时,反复出窗口;最后更新结果。

注意点 :走过一个下标只能摘一个水果,数组元素大小表示的是水果种类!

cpp 复制代码
class Solution {
public:
    int totalFruit(vector<int>& f)
    {
        unordered_map<int, int> hash; // 统计窗口内出现了多少种水果
        int ret = 0;
        for (int left = 0, right = 0; right < f.size(); right++)
        {
            hash[f[right]]++; // 进窗口
            while (hash.size() > 2)
            { // 判断
                // 出窗口
                hash[f[left]]--;
                if (hash[f[left]] == 0)
                    hash.erase(f[left]);
                left++;
            }
            ret = max(ret, right - left + 1);
        }
        return ret;
    }
};

优化点 :由于操作涉及哈希表的插入和删除,耗时较大。为了优化时间复杂度,可以用数组模拟哈希表 (空间换时间),并用kinds变量记录窗口中的有效种类。

ps:有效种类的含义为,若新水果进窗口,则kinds++;若该种类的水果已全部离开窗口,则kinds--。

cpp 复制代码
class Solution {
public:
    int totalFruit(vector<int>& f)
    {
        int hash[100001] = {0}; // 用数组统计窗口内每种水果出现的次数
        int ret = 0;
        
        for (int left = 0, right = 0, kinds = 0; right < f.size(); right++)
        {
            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;
    }
};

2.3 找到字符串中所有字⺟异位词

找到字符串中所有字⺟异位词

解题思路:

快速判断某个子串是否是异位词子串的方式,可以用排序 或者哈希表。但是每次枚举一个新的子串就会产生排序,耗时太大;因此使用哈希表存储和比对。

  1. 暴力枚举+两个哈希表 O(N^2):一个哈希表存储给定子串中的字符,从每个元素开始向后暴力枚举所有子串,并用另一个哈希表存储,通过对比两个哈希表是否相同来找出所有子串。
  2. 滑动窗口+两个哈希表 O(N):一个哈希表存储给定子串中的字符,用固定长度的滑动窗口遍历数组,并用另一个哈希表存储窗口中的元素,通过对比两个哈希表是否相同来找出所有子串。
cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p)
    {
        unordered_map<char, int> hash1;
        unordered_map<char, int> hash2;
        vector<int> ret;

        for(auto ch : p)
        {
            hash2[ch]++;
        }

        int m = p.size();
        for(int left=0, right=0; right < s.size(); right++)
        {
            hash1[s[right]]++;

            while(right - left + 1 > m)
            {
                hash1[s[left]]--;
                if(hash1[s[left]] == 0)
                {
                    hash1.erase(s[left]);
                }
                left++;
            }

            if(right - left + 1 == m && hash1 == hash2)
            {
                ret.push_back(left);
            }
        }

        return ret;
    }
};

优化点 :用两个哈希表辅助计算字符种类,空间复杂度过高,因此可以用两个数组模拟哈希表实现。数组hash2用于存储给定子串中的字符种类和个数,数组hash1和变量count用来计算窗口中的"有效字符"。(数组大小均为26

注意点 :数组的下标是ch - 'a'

cpp 复制代码
class Solution {
public:
    vector<int> findAnagrams(string s, string p)
    {
        int hash1[26] = {0};
        int hash2[26] = {0};
        vector<int> ret;

        for(auto ch : p)
        {
            hash2[ch - 'a']++;
        }

        int m = p.size();
        int count = 0;
        for(int left=0, right=0; right < s.size(); right++)
        {
            //进窗口
            hash1[s[right] - 'a']++;
            if(hash1[s[right] - 'a'] <= hash2[s[right] - 'a'])
            {
                count++;
            }

            //出窗口
            if(right - left + 1 > m)
            {
                if(hash1[s[left] - 'a'] <= hash2[s[left] - 'a'])
                {
                    count--;
                }

                hash1[s[left] - 'a']--;
                left++;
            }

            //更新结果
            if(count == m)
            {
                ret.push_back(left);
            } 
        }

        return ret;
    }
};

// 本期内容就到这里,如果对你有帮助,请三连支持!我是青云,我们下期见^_~

相关推荐
BothSavage3 小时前
Trae远程开发中DeepSeek自定义模型4054错误的排查与修复
算法
小林ixn3 小时前
从暴力到KMP:一道题彻底搞懂字符串匹配的前世今生
算法
烬羽5 小时前
字符串算法入门:从反转字符串到回文判断,面试不再慌
算法·面试
郝学胜_神的一滴5 小时前
CMake 034:生成器表达式:解耦构建时序、精简分支逻辑的终极利器
c++·cmake
先吃饱再说21 小时前
判断回文字符串,从一行代码到双指针优化
算法
见过夏天21 小时前
C++ 基础入门完全指南
c++
黄敬峰1 天前
深入理解算法核心:从递归思想、数组扁平化到快速排序
算法
得物技术1 天前
从狂野代码到按目标生产:得物推荐 AI Harness 的工程化实践|AICon 演讲整理
人工智能·算法·架构
AI小老六1 天前
SkillOpt 架构拆解:把 Skill 文本当参数,用执行轨迹训练 Agent
后端·算法·ai编程