[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;
    }
};

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

相关推荐
机器学习之心1 小时前
198种组合算法+优化CNN-LSTM+SHAP分析+新数据预测+多输出!深度学习可解释分析,强烈安利,粉丝必备
深度学习·算法·cnn-lstm·shap分析·198种组合算法
c++之路1 小时前
CMake 系列教程(一):CMake 基础知识
c语言·开发语言·c++
bIo7lyA8v1 小时前
算法复杂度与能耗关系的多变量分析研究的技术8
算法
Irissgwe1 小时前
C++ STL bitset 和位图详解
开发语言·c++·stl·位图·bitset
洛水水2 小时前
【力扣100题】76.搜索插入位置
数据结构·算法·leetcode
Techblog of HaoWANG2 小时前
智巡守卫:多模态巡检智能体算法服务端设计与实现——基于Ollama+Qwen3.5的自动化巡检报告生成系统
运维·人工智能·算法·目标检测·自动化·边缘计算
万法若空2 小时前
C/C++基本类型表示范围
c语言·开发语言·c++
小蒋学算法2 小时前
算法-灌溉花园的最少龙头数目-贪心
算法
满怀冰雪2 小时前
第07篇-差分算法-高效处理区间修改问题
数据结构·算法