Loading......

文章背景图

算法 - 滑动窗口专项

2026-04-10
1
-
- 分钟
|

算法 - 滑动窗口专项

核心:同向双指针,利用 "数组元素均为正数" 这一条件,通过右指针扩展、左指针收缩的方式,将暴力 O(n²) 优化到 O(n)。


一、基础知识点

1. 最小长度子数组(LeetCode 209)

题目描述

给定正整数数组和一个目标值 target,找出最短的连续子数组,使其和 ≥ target,若不存在则返回 0。

核心思路

利用正数性质——向右扩展时,结果是递增的。

  1. 右指针不断右移,把元素累加进 sum

  2. 左指针收缩:当 sum - nums[left] ≥ target 时,说明去掉 left 元素后仍满足条件,左指针右移

  3. 每次满足条件时更新最小长度

复杂度

  • 时间:O(n)(左右指针各最多移动 n 次)

  • 空间:O(1)

Python 实现

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        n = len(nums)          # 获取数组长度
        ans = float('inf')     # 初始化答案为无穷大(用于取最小值)
        s = 0                   # 当前窗口内元素和
        left = 0                # 左指针初始化为0

        # 枚举右指针,不断扩大窗口
        for right in range(n):
            s += nums[right]    # 将右指针指向的元素加入窗口

            # 尝试收缩左指针:当去掉左元素后仍满足条件,就继续收缩
            # 关键点:利用正数性质,若窗口和 - 左元素 >= target,
            #         说明收缩后仍满足条件,可以放心收缩
            while s - nums[left] >= target:
                s -= nums[left] # 从总和中移除左指针指向的元素
                left += 1       # 左指针右移,收缩窗口

            # 更新最短长度:当窗口和 >= target 时记录答案
            # right - left + 1 是因为 left 和 right 都是闭区间的索引
            if s >= target:
                ans = min(ans, right - left + 1)

        # 如果 ans 没被更新过,说明不存在满足条件的子数组,返回0
        return ans if ans != float('inf') else 0

2. 乘积小于 K 的子数组(LeetCode 713)

题目描述

给定正整数数组和一个整数 K,计算乘积小于 K 的连续子数组的个数。

核心思路

和上一题类似,右指针不断扩展,左指针收缩保证乘积 < K。

关键是计数公式:以 right 为右端点,满足条件的子数组个数 = right - left + 1

(若 left 到 right 乘积 < K,则 left 到 right、left+1 到 right… 共 right-left+1 个都满足)

复杂度

  • 时间:O(n)

  • 空间:O(1)

Python 实现

class Solution:
    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
        # 边界情况:k <= 1 时,正整数乘积不可能小于1,返回0
        if k <= 1:
            return 0

        ans = 0               # 累计满足条件的子数组个数
        s = 1                  # 当前窗口内元素的乘积(用1初始化)
        left = 0               # 左指针

        # 枚举右指针
        for right, x in enumerate(nums):
            s *= x              # 将右指针指向的元素乘入窗口乘积

            # 收缩左指针:直到乘积小于 K 为止
            # 注意:用除法是因为 nums[left] 是正整数
            while s >= k:
                s //= nums[left]  # 移除左指针指向元素的贡献
                left += 1         # 左指针右移

            # 关键计数公式:
            # 以 right 为右端点,左指针为 left 的窗口内,
            # 任意以 right 为右端点、向左扩展的子数组都满足条件
            # 个数 = 窗口内元素个数 = right - left + 1
            # 例如:left=0, right=2 时,有 [0~2], [1~2], [2~2] 共3个
            ans += right - left + 1

        return ans

3. 无重复字符的最长子串(LeetCode 3)

题目描述

找出字符串中不含重复字符的最长子串,返回其长度。

核心思路

用哈希表记录每个字符出现的次数。

  • 右指针扩展,把字符加入窗口

  • 若某字符出现次数 > 1,说明窗口内有重复,左指针收缩直到该字符出现次数 = 1

  • 维护最大长度

复杂度

  • 时间:O(n)

  • 空间:O(1)(字符集有限,ASCII 128 或 256)

Python 实现

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        ans = 0                 # 记录最长长度
        count = Counter()       # 哈希表:字符 -> 出现次数
        left = 0                # 左指针

        # 枚举右指针
        for right, x in enumerate(s):
            count[x] += 1       # 将当前字符加入窗口,出现次数+1

            # 若该字符出现次数 > 1,说明窗口内有重复字符
            # 需要收缩左指针,直到该字符出现次数恢复为1
            while count[x] > 1:
                count[s[left]] -= 1  # 移除左指针指向的字符
                left += 1            # 左指针右移

            # 更新最长长度:当前窗口 [left, right] 无重复,长度为 right-left+1
            ans = max(ans, right - left + 1)

        return ans

二、练习

1. 每个字符最多出现两次的最长子字符串 🔢

题目描述(LeetCode 3090 - 简单)

给你一个字符串 s,请找出满足每个字符最多出现两次的最长子字符串,并返回该子字符串的最大长度。

示例

输入:s = “bcbbbcba”
输出:4
解释:“bcbb”(或 “cbbc” 或 “bbbc” 或 “bbcb”)每个字符最多出现两次,长度为 4。

解题思路

滑动窗口 + 哈希表记录字符出现次数。

  • 右指针扩展,字符计数 +1

  • 若计数 > 2,左指针收缩直到该字符计数 ≤ 2

  • 维护最大长度

复杂度

  • 时间:O(n)

  • 空间:O(1)

Python 代码

class Solution:
    def maximumLengthSubstring(self, s: str) -> int:
        n = len(s)              # 字符串长度
        left = 0                 # 左指针初始化
        count = Counter()       # 哈希表:字符 -> 出现次数
        ans = 0                  # 记录最大长度

        # 枚举右指针
        for right, x in enumerate(s):
            count[x] += 1        # 将当前字符加入窗口

            # 若该字符出现次数超过2,收缩左指针直到恢复正常
            while count[x] > 2:
                count[s[left]] -= 1  # 移除左指针指向的字符
                left += 1            # 左指针右移

            # 更新最大长度
            ans = max(ans, right - left + 1)

        return ans

2. 最多 K 个重复元素的最长子数组 🔢

题目描述(LeetCode 2958 - 中等)

给你一个整数数组 nums 和一个整数 k。

一个元素 x 在数组中的频率指的是它在数组中的出现次数。

如果一个数组中所有元素的频率都小于等于 k,那么我们称这个数组是数组。

请你返回 nums 中最长好子数组的长度。

子数组指的是一个数组中一段连续非空的元素序列。

示例

输入:nums = [1,2,3,1,2,3,1,2], k = 2
输出:6
解释:最长好子数组是 [1,2,3,1,2,3],值 1、2 和 3 在子数组中的频率都没有超过 k = 2。[2,3,1,2,3,1] 和 [3,1,2,3,1,2] 也是好子数组。最长好子数组的长度为 6。

解题思路

和上题同一套路,只是把 "字符出现不超过 2 次" 泛化成 "出现次数不超过 k"。

  • 右指针扩展,元素计数 +1

  • 若该元素计数 > k,左指针收缩直到该元素计数 ≤ k

  • 维护最大长度

复杂度

  • 时间:O(n)

  • 空间:O(n)

Python 代码

class Solution:
    def maxSubarrayLength(self, nums: List[int], k: int) -> int:
        n = len(nums)           # 数组长度
        left = 0                 # 左指针
        ans = 0                  # 记录最大长度
        dic = defaultdict(int)  # 哈希表:元素值 -> 出现次数

        # 枚举右指针
        for right, x in enumerate(nums):
            dic[x] += 1          # 将当前元素加入窗口

            # 若该元素出现次数超过 k,收缩左指针直到恢复正常
            while dic[x] > k:
                dic[nums[left]] -= 1  # 移除左指针指向的元素
                left += 1            # 左指针右移

            # 更新最大长度
            ans = max(ans, right - left + 1)

        return ans

3. 最长的半重复子字符串 🧩

题目描述(LeetCode 2730 - 中等)

给你一个下标从 0 开始的字符串 s,这个字符串只包含 0 到 9 的数字字符。

如果一个字符串 t 中至多有一对相邻字符是相等的,那么称这个字符串 t 是半重复的。例如,“0010”、“002020”、“0123”、“2002” 和 “54944” 是半重复字符串,而 “00101022”(相邻的相同数字对是 “00” 和 “22”)和 “1101234883”(相邻的相同数字对是 “11” 和 “88”)不是半重复字符串。

请你返回 s 中最长半重复子字符串的长度。

示例

输入:s = “52233”
输出:4
解释:最长的半重复子字符串是 “5223”。整个字符串 “52233” 有两个相邻的相同数字对 “22” 和 “33”,但最多只能选取一个。

解题思路

这题稍有不同,统计的是相邻相同字符对的数量。

  • 用 cnt 记录当前窗口内有多少对相邻相等

  • 右指针右移时,若 s[right] == s[right-1],cnt++

  • 若 cnt > 1,左指针收缩,同时如果 left 与 left+1 相等要 cnt–

  • 维护最大长度

复杂度

  • 时间:O(n)

  • 空间:O(1)

Python 代码

class Solution:
    def longestSemiRepetitiveSubstring(self, s: str) -> int:
        n = len(s)              # 字符串长度
        left = 0                 # 左指针
        cnt = 0                  # 窗口内相邻相等字符对的数量
        ans = 1                  # 初始化为1(单字符一定是半重复的)

        # 枚举右指针(从1开始,因为需要和前一个字符比较)
        for right in range(1, n):
            # 若当前字符与前一个字符相等,窗口内多了一对相邻相等
            if s[right] == s[right - 1]:
                cnt += 1

            # 若相邻相等对超过1对,收缩左指针直到恢复正常
            while cnt > 1:
                # 收缩前先检查左指针和left+1是否相等,
                # 若相等则移出窗口后这对就不存在了,需要减1
                if s[left] == s[left + 1]:
                    cnt -= 1
                left += 1

            # 更新最大长度
            ans = max(ans, right - left + 1)

        return ans

三、滑动窗口总结 ✨

适用条件

  • 数组 / 字符串的连续子区间问题

  • 满足某种单调性:扩大窗口条件更易满足 / 缩小窗口条件更易不满足

固定套路

  1. 右指针 for 循环扩展

  2. while 循环内左指针收缩(根据具体条件)

  3. 更新答案

时间复杂度分析

不要被二重循环吓住——左指针最多移动 n 次,右指针也是 O(n),总复杂度 O(n)。

关键点

  • 乘积类问题:K ≤ 1 直接返回 0(正数乘积不可能小于 1)

  • 计数类问题:以 right 为右端点的满足条件的子数组数 = right - left + 1

  • 相邻字符对问题:用额外变量追踪相邻相等的情况

评论交流