滑动窗口(灵神提单)

滑动窗口(Sliding Window)是一种常用的算法技术,通常用于处理序列(如数组或字符串)的问题,尤其是在寻找连续子序列、子数组或优化某些条件时非常有效。其基本思想是使用两个指针来表示当前窗口的边界,并通过移动这些指针来动态调整窗口的大小。

一、基本原理

  1. 定义窗口:通过两个指针(通常是左指针和右指针)来定义一个“窗口”,这个窗口可以是当前考虑的子数组或子字符串。

  2. 移动窗口

    • 扩展窗口:通过移动右指针来扩大窗口的范围,包含更多元素。
    • 收缩窗口:通过移动左指针来缩小窗口,排除某些元素。
  3. 条件判断:在每次移动指针时,检查当前窗口是否满足特定条件,比如和、长度、包含的元素种类等。

  4. 记录结果:根据窗口状态更新所需的结果,比如最小长度、最大和等。

二、应用场景

滑动窗口常用于以下几类问题:

  • 寻找最大或最小子数组:如在给定和的情况下寻找子数组的最小长度。
  • 字符串处理:如找到最小覆盖子串、无重复字符的最长子串等。
  • 动态数据流处理:处理连续的数据流,以维护某种统计特征。

三、示例:
分享丨【题单】滑动窗口与双指针(定长/不定长/至多/至少/恰好/单序列/双序列/三指针) - 力扣(LeetCode)https://leetcode.cn/circle/discuss/0viNMK/

一、基础区:

eg 1、1456. 定长子串中元音的最大数目 - 力扣(LeetCode)(典型)

class Solution {
public:
    int maxVowels(string s, int k) {
        int ans = 0, tmp = 0;

        for (int i = 0; i < s.length(); i++) {
            // 进入窗口
            if (s[i] == 'a' || s[i] == 'e' || s[i] == 'i' || s[i] == 'o' ||
                s[i] == 'u')
                tmp++;
            if (i < k - 1)
                continue;

            // 更新答案(找出最大的元音字母数)
            ans = max(ans, tmp);

            // 出窗口
            char out = s[i - k + 1];
            if (out == 'a' || out == 'e' || out == 'i' || out == 'o' ||
                out == 'u')
                tmp--;
            
        }
        return ans;
    }
};

eg 2、643. 子数组最大平均数 I - 力扣(LeetCode)

class Solution {
public:
    double findMaxAverage(vector<int>& nums, int k) {
        double ans = INT_MIN, sum = 0;
        int n = nums.size();

        for(int i = 0; i < n; i++){
            // 进入窗口
            sum += nums[i];
            if(i < k - 1)
                continue;

            // 更新答案
            ans = max(ans, sum);

            // 出窗口
            int out = nums[i - k + 1];
            sum -= out;
        }
        
        return ans / k;
    }
};

eg 3、1343. 大小为 K 且平均值大于等于阈值的子数组数目 - 力扣(LeetCode)

class Solution {
public:
    int numOfSubarrays(vector<int>& arr, int k, int threshold) {
        double ans = 0, sum = 0;
        int n = arr.size();

        for(int i = 0; i < n; i++){
            // 进入窗口
            sum += arr[i];
            if(i < k - 1)
                continue;

            // 更新答案
            if(sum / k >= threshold)
                ans++;

            // 出窗口
            sum -= arr[i - k + 1];
        }
        
        return ans;
    }
};

eg 4、2090. 半径为 k 的子数组平均值 - 力扣(LeetCode)

class Solution {
public:
    vector<int> getAverages(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> ans(n, -1);
        if(k == 0) return nums; // k == 0 不选--就是nums数组
        if(k > n) return ans;   // k > n 数组不够长选不了--答案数组全都是-1

        // 第一次进入窗口
        long long sum = 0;
        for(int i = 0; i < 2 * k + 1 && i < n; i++)
            sum += nums[i];
        
        for(int i = k; i < n - k; i++){
            // 更新答案
            ans[i] = (sum / (2 * k + 1));
            // 第二次进窗口
            if(i + k + 1 < n)
                sum += nums[i + k + 1];
            // 出窗口
            sum -= nums[i - k];
        }

        return ans;
    }
};

eg 5、2379. 得到 K 个黑块的最少涂色次数 - 力扣(LeetCode)

class Solution {
public:
    int minimumRecolors(string blocks, int k) {
        int n = blocks.size();
        int ans = INT_MAX;
        int sum = 0; // 操作次数--白块的个数
        int len = 0; // 操作后的子串长度
        for (int i = 0; i < n; i++) {
            // 进入窗口
            len++;
            if (blocks[i] == 'W')
                sum++;
            // 更新答案
            if (len == k)
                ans = min(ans, sum);
            // 出窗口
            if (len == k) {
                if (blocks[i - len + 1] == 'W')
                    sum--;
                len--;
            }
        }
        return ans;
    }
};

eg 6、1052. 爱生气的书店老板 - 力扣(LeetCode)

class Solution {
public:
    int maxSatisfied(vector<int>& customers, vector<int>& grumpy, int minutes) {
        int n = customers.size();
        int sum = 0;
        int ans = 0;
        int windows = 0;

        // 初步处理 sum
        for (int i = 0; i < n; i++)
            if (!grumpy[i]) // 先将不生气时的店长的顾客加入到sum
                sum += customers[i];

        // 初始化窗口顾客人数
        for (int i = 0; i < minutes; i++)
            if (grumpy[i])
                windows += customers[i];

        // 更新答案
        ans = sum + windows;

        // 开始滑动秘诀 minutes
        for (int i = minutes; i < n; i++) {
            // 出窗口
            if (grumpy[i - minutes])
                windows -= customers[i - minutes];
            // 进入窗口
            if (grumpy[i])
                windows += customers[i];
            // 更新答案
            ans = max(ans, sum + windows);
        }
        return ans;
    }
};

eg 7、1461. 检查一个字符串是否包含所有长度为 K 的二进制子串 - 力扣(LeetCode)

class Solution {
public:
    bool hasAllCodes(string s, int k) {
        int n = s.size();
        if (n < k) return false; // 字符串长度小于k,肯定不能包含所有子串
        
        unordered_set<int> seen;
        int totalSubstrings = 1 << k; // 2^k,表示所有可能的子串个数
        int currentHash = 0;
        int maxMask = (1 << k) - 1; // 用于保持k位的掩码,去掉超出部分

        // 初始化--计算前k位子串的哈希值
        for (int i = 0; i < k; ++i) {
            currentHash = (currentHash << 1) | (s[i] - '0');
        }
        // 进入窗口--插入第一个k位子串的哈希
        seen.insert(currentHash);

        // 滑动窗口,逐步计算后续子串的哈希
        for (int i = k; i < n; ++i) {
            // 出窗口--左移一位,丢弃左侧的高位,添加当前字符的值(0或1)
            currentHash = ((currentHash << 1) & maxMask) | (s[i] - '0');
            // 进入窗口
            seen.insert(currentHash);
            // 更新答案--如果已经找到所有可能的子串,返回true
            if (seen.size() == totalSubstrings) {
                return true;
            }
        }

        return false; // 没有找到所有子串
    }
};

  // 初始化--计算前k位子串的哈希值
        for (int i = 0; i < k; ++i) {
            currentHash = (currentHash << 1) | (s[i] - '0');
        }

  • currentHash << 1:这是一个位移操作,将 currentHash 的值左移一位。也就是说,将 currentHash 的二进制表示向左移动一个位置,并且右侧补零。例如,原来 currentHash 的二进制是 101,左移一位后变成 1010

  • (s[i] - '0'):这部分是将字符 s[i] 转换为对应的数字。假设 s[i] 是一个字符 '0''1's[i] - '0' 的作用是将字符 '0' 转换为整数 0,将字符 '1' 转换为整数 1

  • (currentHash << 1) | (s[i] - '0'):将 currentHash 左移一位后,再将 s[i] - '0' 这个值通过按位或(|)的操作,加入到 currentHash 的最低位。如果 s[i]'1',那么就把 1 放在 currentHash 的最低位;如果 s[i]'0',则把 0 放在最低位。

总结:将当前 currentHash 左移一位,并且把字符 s[i]('0' 或 '1')的数值添加到 currentHash 的最低位。

同理:

 currentHash = ((currentHash << 1) & maxMask) | (s[i] - '0');

  • currentHash << 1:同第一行代码,将 currentHash 左移一位。

  • (currentHash << 1) & maxMask:这里有一个按位与(&)操作。maxMask 是一个掩码,用来限制 currentHash 的某些位。例如,如果 maxMask 是一个全是 1 的二进制数,那么 currentHash << 1 仍然会被保留;如果 maxMask 只在某些位上是 1,那么通过按位与操作后,只保留这些位,其他位会被清零。这样做的目的是确保 currentHash 的高位没有超出某个范围。

  • (s[i] - '0'):将字符 s[i] 转换为数字,操作同上。

  • ((currentHash << 1) & maxMask) | (s[i] - '0'):最终,左移并掩码后的 currentHashs[i] - '0' 通过按位或操作结合。这样做的目的是将 s[i] 的值添加到 currentHash 的最低位,并且确保 currentHash 的高位不超出 maxMask 指定的范围。

总结:将 currentHash 左移一位,应用掩码 maxMask 限制高位,再将字符 s[i] 的数值添加到 currentHash 的最低位。

eg 8、2841. 几乎唯一子数组的最大和 - 力扣(LeetCode)

class Solution {
public:
    long long maxSum(vector<int>& nums, int m, int k) {
        long long ret = 0, sum = 0;
        int n = nums.size();
        unordered_map<long long, int> hash;

        // 先进 k - 1 个 -- 便于我们书写后面 更新答案 和 出窗口
        for(int i = 0; i < k - 1; i++){
            sum += nums[i];
            hash[nums[i]]++;
        }

        for (int i = k - 1; i < n; i++) {
            // 进入窗口
            sum += nums[i];
            hash[nums[i]]++;

            // 更新答案
            if (hash.size() >= m) {
                ret = max(ret, sum);
            }

            // 出窗口
            int out = nums[i + 1 - k];
            // 如果没有出现了, 就移除
            if (--hash[out] == 0)
                hash.erase(out);
            sum -= out;
        }

        return ret;
    }
};

 eg 9、2461. 长度为 K 子数组中的最大和 - 力扣(LeetCode)

// ------------------数组(vector会增加时间、空间复杂度) + 滑动-----------------
class Solution {
public:
    long long maximumSubarraySum(vector<int>& nums, int k) {
        int n = nums.size();
        long long ret = 0, sum = 0;
        int hash[100010] = {0};

        for (int i = 0, j = 0; i < n; i++) {
            // 进入窗口
            sum += nums[i];
            hash[nums[i]]++;
            // 出窗口
            while (hash[nums[i]] == 2) {
                sum -= nums[j];
                hash[nums[j++]]--;
            }
            // 更新答案
            if (i - j + 1 == k) {
                ret = max(ret, sum);
                sum -= nums[j];
                hash[nums[j++]]--;
                
            }
        }
        return ret;
    }
};

/*
--------------------hash表 + 滑动----------------------
class Solution {
public:
    long long maximumSubarraySum(vector<int>& nums, int k) {
        int n = nums.size();
        long long ret = 0, sum = 0;
        unordered_map<long long , int> hash;
        // 先进 k - 1 个
        for(int i = 0; i < k - 1; i++){
            sum += nums[i];
            hash[nums[i]]++;
        }

        for(int i = k - 1; i < n; i++){
            // 进入窗口
            sum += nums[i];
            hash[nums[i]]++;
            // 更新答案
            if(hash.size() == k)
                ret = max(ret, sum);
            // 出窗口
            int out = nums[i + 1 - k];
            sum -= out;
            if(--hash[out] == 0)
                hash.erase(out);
        }
        return ret;
    }
};
*/

eg 10、1423. 可获得的最大点数 - 力扣(LeetCode)

// --------------滑动窗口----------------
class Solution {
public:
    int maxScore(vector<int>& cardPoints, int k) {
        int n = cardPoints.size();
        int _k = n - k;
        int nums_sum = 0;
        int sum = 0;
        // 计算数组所有元素的总和 -- accumulate(cardPoints.begin(), cardPoints.end(), 0);
        for (auto i : cardPoints)
            nums_sum += i;

        // 先初始化sum为前_k个元素的和 -- accumulate(cardPoints.begin(), cardPoints.begin() + m, 0)
        for (int i = 0; i < _k; i++)
            sum += cardPoints[i];

        int min_s = sum;
        for (int i = _k; i < n; i++) {
            // 进入窗口 + 出窗口
            sum += cardPoints[i] - cardPoints[i - _k];
            // 更新答案
            min_s = min(min_s, sum);
        }

        return nums_sum - min_s;
    }
};

eg 11、1652. 拆炸弹 - 力扣(LeetCode)

// 时间复杂度O(n)
class Solution {
public:
    std::vector<int> decrypt(const std::vector<int>& code, int k) {
        int n = code.size();
        std::vector<int> ans(n);

        // 确定滑动窗口的起始和结束索引
        int start = k > 0 ? 1 : (n + k);
        int end = k > 0 ? (k + 1) : n;
        k = abs(k);

        // 初始化窗口内元素和
        int sum = reduce(code.begin() + start, code.begin() + end);

        for (int i = 0; i < n; ++i) {
            // 更新答案
            ans[i] = sum;
            // 进出窗口--最后的元素出窗口
            sum += code[end % n] - code[(end - k) % n];
            end++;
        }

        return ans;
    }
};

eg 12、1297. 子串的最大出现次数 - 力扣(LeetCode)

class Solution {
public:
    int maxFreq(string s, int maxLetters, int minSize, int maxSize) {
        unordered_map<int, int> hash1;
        unordered_map<string, int> hash2;
        string str = "";
        int ans = 0;
        // 先进minSize - 1个
        for (int i = 0; i < minSize - 1; i++) {
            str += s[i];
            hash1[s[i] - 'a']++;
        }

        for (int i = minSize - 1; i < s.size(); i++) {
            // 进窗口
            str += s[i];
            hash1[s[i] - 'a']++;
            hash2[str]++;

            // 出窗口
            if (str.size() > minSize) {
                hash1[s[i - minSize] - 'a']--;
                if (hash1[s[i - minSize] - 'a'] == 0)
                    hash1.erase(s[i - minSize] - 'a');
                str.erase(0, 1);
                hash2[str]++; // 注意这里的hash2[str]++
            }

            // 更新答案
            if (hash1.size() <= maxLetters) {
                ans = max(ans, hash2[str]);
            }
        }
        return ans;
    }
};

eg 13、2653. 滑动子数组的美丽值 - 力扣(LeetCode) 

class Solution {
public:
    vector<int> getSubarrayBeauty(vector<int>& nums, int k, int x) {
        const int N = 50; // 偏移量
        int cnt[2 * N + 1] = {0};
        // 先初始化窗口--进入(k - 1)个元素
        for(int i = 0; i < k - 1; i++)
            cnt[nums[i] + N]++;
        
        vector<int> ans(nums.size() - k + 1);
        for(int i  = k - 1; i < nums.size(); i++){
            // 进入窗口
            cnt[nums[i] + N]++;
            int target = x; //  <= num 的整数个数 >= x
            // 枚举负数范围[-50, -1]
            for(int j = 0; j < N; j++){
                target -= cnt[j];
                // 更新答案
                if(target <= 0){
                    ans[i - k + 1] = j - N;
                    break;
                }
            }
            // 滑出窗口
            cnt[nums[i - k + 1] + N]--;
        }
        return ans;
    }
};

二、进阶区:

eg 1、2134. 最少交换次数来组合所有的 1 II - 力扣(LeetCode)

// 统计窗口0的个数
class Solution {
public:
    int minSwaps(vector<int>& nums) {
        int n = nums.size();
        // 窗口大小
        int k = accumulate(nums.begin(), nums.end(), 0);
        /*
          for(auto i : nums)
            if(i == 1)
                k++;
        */
        // 初始化窗口--进入k个元素
        int num_0 = 0;
        for(int i = 0; i < k; i++){
            if(nums[i] == 0)
                num_0++;
        }
       int ans = num_0;
        // 滑动窗口  j--起始点  i--终止点
        for(int i = k, j = 0; j < n; i++, j++){
            // 滑出窗口
            if(nums[j] == 0)
                num_0--;
            // 进入窗口
            if(nums[i % n] == 0)
                num_0++;
            // 更新答案
            ans = min(ans, num_0);
        }
        return ans;
    }
};

eg 2、1888. 使二进制字符串字符交替的最少反转次数 - 力扣(LeetCode)

// 前后缀和
// class Solution {
// public:
//     // 计算最小翻转次数的函数
//     int minFlips(string s) {
//         int n = s.length();
//         int ans = n;

//         // 枚举开头是 '0' 还是 '1'
//         for (unsigned char head = '0'; head <= '1'; head++) {
//             // 左边每个位置的不同字母个数
//             vector<int> leftDiff(n);
//             int diff = 0;
//             for (size_t i = 0; i < n; i++) {
//                 if (s[i] != (head ^ (i & 1))) {
//                     diff++;
//                 }
//                 leftDiff[i] = diff;
//             }

//             // 右边每个位置的不同字母个数
//             unsigned char tail = head ^ 1;
//             diff = 0;
//             for (int i = n - 1; i >= 0; i--) {
//                 // 左边 + 右边即为整个字符串的不同字母个数,取最小值
//                 ans = min(ans, leftDiff[i] + diff);
//                 if (s[i] != (tail ^ ((n - 1 - i) & 1))) {
//                     diff++;
//                 }
//             }
//         }

//         return ans;
//     }
// };

// 滑动窗口
class Solution {
public:
    int minFlips(string s) {
        int n = s.length();
        vector<char> s01 = {'0', '1'};

        int cnt = 0;
        for (size_t i = 0; i < n; ++i) {
            if (s[i] == s01[i % 2]) {
                cnt++;
            }
        }

        int ans = min(cnt, n - cnt);
        for (int i = n; i < n * 2; ++i) {
            if (s[(i - n) % n] == s01[(i - n) % 2]) cnt--;
            if (s[i % n] == s01[i % 2]) cnt++;
            ans = min(ans, min(cnt, n - cnt));
        }

        return ans;
    }
};

eg 3、567. 字符串的排列 - 力扣(LeetCode)

class Solution {
public:
    bool checkInclusion(string s1, string s2) {
        int len_s1 = s1.length(); // 窗口长度
        int len_s2 = s2.length(); // 遍历长度
        // 处理特殊情况
        if(len_s1 > len_s2) return false;
        vector<int> hash1(26, 0), hash2(26, 0);

        // 先比较前len_s1个
        for (int i = 0; i < len_s1; i++) {
            hash1[s1[i] - 'a']++;
            hash2[s2[i] - 'a']++;
        }
        // 更新答案
        if(hash1 == hash2) return true;

        // 开始滑动窗口
        for (int i = len_s1; i < len_s2; i++) {
            // 进入窗口
            hash2[s2[i] - 'a']++;
            // 滑出窗口
            hash2[s2[i - len_s1] - 'a']--;
            // 更新答案
            if(hash1 == hash2) return true;
        }
        return false;
    }
};

eg 4、3439. 重新安排会议得到最多空余时间 I - 力扣(LeetCode)

class Solution {
public:
    int maxFreeTime(int eventTime, int k, vector<int>& startTime, vector<int>& endTime) {
        int n = startTime.size();
        
        // 这里可以写成一个 int get(int i) 的函数
        auto get = [&](int i) -> int {
            if (i == 0) // 第 1 个事件的空闲时间 = 在事件发生之前的时间
                return startTime[0];
            if (i == n) // 第 n 个事件的空闲时间 = 活动的总时间 - 事件最后结束的时间
                return eventTime - endTime[n - 1];
                // 第 i 个事件的空闲时间 = 当前事件的开始时间 - 上一个时间的结束时间
            return startTime[i] - endTime[i - 1];
        };

        int s = 0, ans = 0;
        for(int i = 0; i <= n; i++){
            // 进窗口
            s += get(i); // 获取到第 i 个事件的空闲时间
            if (i < k) // 空闲时间段数不够
                continue;
            // 更新答案
            ans = max(ans, s);
            // 出窗口
            s -= get(i - k); // 减去最左侧的空闲时间
        }
        return ans;
    }
};

重难点:

1、为什么要用定长滑动窗口 ?

        想象有一场持续一段时间的活动,期间安排了多场会议。每两场会议之间会有空闲时间,活动开始前和结束后也可能有空闲时间。现在有个规则,就是会议之间的先后顺序不能改变,也就是说每场会议必须按照原来安排的时间依次进行。

        已知活动中一共有 n 场会议,这样就会产生 n + 1 个空闲时间段(活动开始前、每两场会议之间、活动结束后)。现在你有一个 “合并机会”,最多可以把 k + 1 个相邻的空闲时间段合并成一个大的空闲时间段。你的任务是找出,通过这种合并方式,能得到的最长空闲时间段是多长。

        也就是在 n + 1 个空闲时间段中 把相邻的 k + 1 个空闲时间段 合并 求最长的空闲时间段

 2、为什么 for 循环中的 i <= n 而不是 i < n ?

i 的含义

i 在这里代表的是空闲时间段的索引。i 的取值范围从 0 到 n,对应着 n + 1 个空闲时间段:

  • 当 i = 0 时,代表的是第一个事件开始之前的空闲时间,通过 get(0) 函数计算得到 startTime[0]
  • 当 0 < i < n 时,代表的是第 i 个事件和第 i - 1 个事件之间的空闲时间,通过 get(i) 函数计算得到 startTime[i] - endTime[i - 1]
  • 当 i = n 时,代表的是最后一个事件结束之后的空闲时间,通过 get(n) 函数计算得到 eventTime - endTime[n - 1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值