力扣刷题记录——滑动窗口

一、滑动窗口概念

滑动窗口Sliding Window)是一种常用于处理子数组或子序列问题的高效算法技巧。滑动窗口的核心思想是通过维护一个窗口(即数组或序列的连续子集)在数据结构上滑动,以动态调整子集的范围,从而解决各种问题,如寻找最大/最小子数组、满足某些条件的子数组、最长无重复字符子串等。也就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果

在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。

滑动窗口的工作原理

  1. 定义窗口
    • 通常使用两个指针 leftright 来表示窗口的左右边界。初始时,这两个指针通常都指向数组的起始位置。
  2. 扩展窗口
    • 移动 right 指针扩展窗口,将新元素纳入窗口内。
    • 根据问题的具体要求(如窗口内元素的和、乘积、或者某些条件是否满足),判断当前窗口是否有效。
  3. 收缩窗口
    • 当窗口不再满足条件时,移动 left 指针收缩窗口,直到窗口重新满足条件或者彻底无效。
    • 在收缩窗口的过程中,通常会更新一些状态(如窗口的最大/最小值、窗口内元素的频率等)。
  4. 记录结果
    • 在整个过程中,实时更新和记录满足要求的窗口状态,如最大值、最小值、窗口长度等。

我们可以发现,滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n2)暴力解法降为O(n)。


二、滑动窗口应用场景以及题目解析

  1. 字符串问题
    • 最长无重复字符子串:例如,在一个字符串中寻找最长的无重复字符的子串。
    • 包含所有字符的最小子串:例如,在一个字符串中寻找包含另一个字符串所有字符的最小子串。

76.最小覆盖子串 (opens new window):这段代码实现了一个滑动窗口算法,用于解决经典的“最小覆盖子串”问题。该问题要求在字符串 s 中找到一个最小的子串,包含字符串 t 中的所有字符(包括频率相同的字符)。如果不存在这样的子串,则返回空字符串。

class Solution {
    // 判断当前窗口的字符计数是否覆盖了目标字符串的字符计数
    bool is_covered(const vector<int>& sourceCounts, const vector<int>& targetCounts) {
        // 检查大写字母是否满足条件
        for (int i = 'A'; i <= 'Z'; ++i) {
            if (sourceCounts[i] < targetCounts[i]) {
                return false;  // 如果当前窗口内某个字符的数量少于目标字符串所需数量,则不满足条件
            }
        }
        // 检查小写字母是否满足条件
        for (int i = 'a'; i <= 'z'; ++i) {
            if (sourceCounts[i] < targetCounts[i]) {
                return false;  // 如果当前窗口内某个字符的数量少于目标字符串所需数量,则不满足条件
            }
        }
        return true;  // 如果所有字符都满足条件,则返回 true
    }
public:
    string minWindow(string s, string t) {
        int sourceLength = s.size();
        int targetLength = t.size();
        
        // 如果源字符串的长度小于目标字符串,无法找到满足条件的子串,直接返回空字符串
        if (sourceLength < targetLength) return "";

        vector<int> targetCounts(128, 0);  // 存储目标字符串 t 中各字符的频率
        vector<int> sourceCounts(128, 0);  // 存储当前窗口中各字符的频率

        // 初始化目标字符串 t 中各字符的频率
        for (char c : t) {
            targetCounts[c]++;
        }

        int minWindowStart = 0;  // 最小覆盖子串的起始索引
        int minWindowEnd = sourceLength;  // 最小覆盖子串的结束索引
        int minWindowSize = INT_MAX;  // 最小覆盖子串的长度,初始化为最大整数值

        // 使用滑动窗口算法寻找最小覆盖子串
        for (int left = 0, right = 0; right < sourceLength; ++right) {
            // 将当前右指针指向的字符加入窗口
            sourceCounts[s[right]]++;

            // 当窗口内字符满足覆盖目标字符串 t 时,开始尝试收缩窗口
            while (is_covered(sourceCounts, targetCounts)) {
                // 如果当前窗口长度小于之前找到的最小窗口长度,则更新最小窗口的位置和大小
                if (right - left < minWindowEnd - minWindowStart) {
                    minWindowStart = left;
                    minWindowEnd = right;
                    minWindowSize = min(right - left + 1, minWindowSize);  // 更新最小窗口长度
                }
                // 将左指针指向的字符移出窗口,缩小窗口
                sourceCounts[s[left]]--;
                left++;  // 左指针右移
            }
        }

        // 如果没有找到满足条件的子串,返回空字符串;否则返回找到的最小覆盖子串
        return minWindowSize == INT_MAX ? "" : s.substr(minWindowStart, minWindowSize);
    }
};
  1. 数组问题

    • 固定大小的子数组的最大/最小和:在一个数组中查找固定大小的子数组,使其和最大或最小。

    • 和等于某个目标值的子数组:在数组中找到和为某个目标值的最长或最短子数组。

713. 乘积小于 K 的子数组: 用于计算数组中乘积小于给定值 k 的连续子数组的数量。

 class Solution {
public:
    int numSubarrayProductLessThanK(vector<int>& nums, int k) {
        int n = nums.size();  // 数组的长度
        
        // 如果 k <= 1,那么所有子数组的乘积都无法小于 k,直接返回 0
        if (k <= 1) {
            return 0;
        }
        
        int num = 1;  // 当前窗口内所有元素的乘积
        int ans = 0;  // 记录满足条件的子数组的数量
        
        // 使用滑动窗口的左右边界 left 和 right
        for (int left = 0, right = 0; right < n; right++) {
            num *= nums[right];  // 将当前元素 nums[right] 乘入窗口的乘积
            
            // 如果当前乘积大于或等于 k,移动左边界 left 缩小窗口
            while (num >= k) {
                num /= nums[left++];  // 将左边界元素除出窗口,并右移左指针
            }
            
            // 每次右指针移动时,计算满足条件的子数组数量
            // 当前窗口长度为 (right - left + 1),这个长度范围内的所有子数组都是有效的
            ans += right - left + 1;
        }
        
        // 返回满足条件的子数组的总数
        return ans;
    }
};

滑动窗口的注意事项

  • 窗口的状态维护:在滑动窗口过程中,维护好窗口内的状态(如总和、频率等)是关键,以便快速做出判断。
  • 边界条件的处理:确保指针的移动和窗口的更新不会导致越界或遗漏情况,特别是在收缩窗口时。

三、进阶题目

2302. 统计得分小于 K 的子数组数目:给定一个数组 nums 和一个整数 k,要求统计所有满足条件的子数组数目。条件是子数组的元素之和乘以子数组长度的得分小于 k

class Solution {
public:
    long long countSubarrays(vector<int>& nums, long long k) {
        long long ans= 0l,sum= 0l;
        int left = 0,right = 0;
        for(;right<nums.size() ;right++)
        {
            sum+=nums[right];
            while(sum*(right-left+1)>=k)
            {
                sum=(sum - nums[left++]);
            }
            ans+=right-left+1;
        }
        return ans;
    }
};

我们使用双指针(leftright)表示滑动窗口的左右边界。右指针 right 逐步向右扩展窗口,左指针 left 根据需要收缩窗口。sum 用于记录当前窗口的元素和,right-left+1 用于记录当前窗口的长度。当 sum * (right-left+1) >= k 时,收缩窗口,直到条件满足。

为什么使用 ans += right - left + 1

考虑滑动窗口的概念:

  1. 窗口的范围:假设当前窗口由 leftright 这两个指针界定,窗口内的子数组就是从 leftright 的所有元素。
  2. 子数组的数量
    • 对于固定的右边界 right,从 leftright 的每个位置都可以形成一个子数组,这些子数组的起点可以是 leftright 中的任意一个位置。
    • 因此,以 right 作为子数组的结束位置时,所有以 leftright 之间的任意位置为起点的子数组都是有效的。子数组的数量正好等于 right - left + 1

当我们找到一个满足 sum * (right - left + 1)< k 的窗口时,所有从 leftright 的子数组(包括 leftright)都是满足条件的。所以 right - left + 1 是以 right 结尾的、满足条件的子数组的数量。

2962. 统计最大元素出现至少 K 次的子数组:给定一个数组 nums 和一个整数 k,要求统计所有满足条件的子数组数目。条件是子数组中最大元素出现至少 k 次。

class Solution {
public:
    long long countSubarrays(vector<int>& nums, int k) {
        auto mx = *max_element(nums.begin(), nums.end());
        long long ans = 0;
        int cnt_mx=0,left = 0,right = 0;
        for(;right<nums.size() ; right++)
        { 
            if(nums[right]==mx)
                cnt_mx++;
            while(cnt_mx == k)
            {
                if(nums[left]==mx)
                    cnt_mx--;
                left++;
            }
            ans += left;
        }
        return ans;
    }
};

使用双指针(leftright)表示滑动窗口的左右边界。右指针 right 逐步向右扩展窗口,左指针 left 根据需要收缩窗口。cnt_mx 用于记录窗口内最大值 mx 出现的次数。当 cnt_mx == k 时,收缩窗口,直到 cnt_mx < k。每次窗口缩小时,统计 left 的值,因为此时窗口内所有以 left 为起点的子数组都不满足条件。

cntMx 达到 k 时,表示当前窗口 [left, right] 中,最大元素 mx 的出现次数至少为 k。需要调整左端点 left,不断右移 left,直到窗口内最大元素 mx 的出现次数小于 k 为止。这个过程确保在窗口内的最大元素出现次数为 k

为什么使用 ans += left

一旦窗口内最大元素的出现次数小于 k,意味着在此时,所有以 right 为右端点、左端点小于 left 的子数组,都满足最大元素出现至少 k 次的条件。

因此,当前窗口的有效子数组数目等于 left,因为从 leftright 的所有子数组都满足条件。

left 加到结果 ans 中,以统计所有符合条件的子数组数量。

好的,关于力扣刷题C++常用操作,我可以给你一些常见的操作和技巧: 1. 使用 STL 容器和算法库:STL(Standard Template Library)是 C++ 标准库中的一个重要组成部分,包含了许多常用的容器和算法。在力扣刷题中,使用 STL 可以大大提高代码的效率和可读性。例如,vector 可以用来存储动态数组,sort 可以用来排序等等。 2. 使用 auto 关键字:auto 关键字可以自动推导变量类型,可以减少代码量和提高可读性。例如,auto x = 1; 可以自动推导出 x 的类型为 int。 3. 使用 lambda 表达式:lambda 表达式是 C++11 中引入的一种匿名函数,可以方便地定义一些简单的函数对象。在力扣刷题中,使用 lambda 表达式可以简化代码,例如在 sort 函数中自定义比较函数。 4. 使用位运算:位运算是一种高效的运算方式,在力扣刷题中经常会用到。例如,左移运算符 << 可以用来计算 2 的幂次方,右移运算符 >> 可以用来除以 2 等等。 5. 使用递归:递归是一种常见的算法思想,在力扣刷题中也经常会用到。例如,二叉树的遍历、链表的反转等等。 6. 使用 STL 中的 priority_queue:priority_queue 是 STL 中的一个容器,可以用来实现堆。在力扣刷题中,使用 priority_queue 可以方便地实现一些需要维护最大值或最小值的算法。 7. 使用 STL 中的 unordered_map:unordered_map 是 STL 中的一个容器,可以用来实现哈希表。在力扣刷题中,使用 unordered_map 可以方便地实现一些需要快速查找和插入的算法。 8. 使用 STL 中的 string:string 是 STL 中的一个容器,可以用来存储字符串。在力扣刷题中,使用 string 可以方便地处理字符串相关的问题。 9. 注意边界条件:在力扣刷题中,边界条件往往是解决问题的关键。需要仔细分析题目,考虑各种边界情况,避免出现错误。 10. 注意时间复杂度:在力扣刷题中,时间复杂度往往是评判代码优劣的重要指标。需要仔细分析算法的时间复杂度,并尽可能优化代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

无敌岩雀

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

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

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

打赏作者

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

抵扣说明:

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

余额充值