滑动窗口(有趣的算法讲解)

2,042 阅读10分钟

简介(来源于科大讯飞)

滑动窗口是一种高效的数据处理技术,主要用于处理数组和字符串相关问题,通过减少循环的嵌套深度来降低算法的时间复杂度。在计算机科学中,滑动窗口通常指的是在数据流或数组中维护一个区间,这个区间会随着数据的处理而“滑动”,故名滑动窗口。以下是关于滑动窗口的相关介绍:

  1. 滑动窗口的基本概念

    • 定义:滑动窗口是在一个特定大小的字符串或数组上进行操作的一种技术,它可以在数据结构如队列、堆栈或列表上实现,通过调整窗口的起始和结束位置来适应不同的问题需求。
    • 工作原理:滑动窗口的实现主要依赖于双指针或者迭代器来标识当前处理的子序列(窗口)。在数组或字符串的处理中,窗口逐步移动,并在每个位置执行相应的操作(如计算最大值、最小值或其他聚合函数)。
    • 应用场景:滑动窗口法经常用于解决涉及到子字符串或子数组的问题,例如寻找给定长度的最大/最小值,或满足特定条件的子序列。
  2. 滑动窗口的工作机制

    • 动态调整:滑动窗口的大小可以根据实际问题动态调整。对于一些需要动态改变窗口大小以适应不同数据变化的问题,滑动窗口提供了极大的灵活性。
    • 数据控制:在数据传输如TCP中,滑动窗口机制允许发送方在未收到确认的情况下继续发送多个数据分组,通过接收方返回的确认信息来控制数据的发送速率,优化网络吞吐量和防止数据丢失。
    • 流量控制:特别是在网络传输中,滑动窗口不仅仅是一个顺序控制的工具,更是一个流量控制和拥塞控制的重要手段,通过控制窗口的大小来应对网络拥塞和数据包丢失的情况。
  3. 滑动窗口的类型

    • 固定窗口和可变窗口:根据窗口大小是否固定,滑动窗口可以分为固定窗口和可变窗口。固定窗口大小不变,而可变窗口则根据特定条件调整大小。
    • 应用场景差异:在不同的应用中,滑动窗口的作用也不同。在数据处理中,它用来高效解决连续数据的问题;在网络通信中,它用来控制数据的流量和速度,避免拥塞。
  4. 滑动窗口的算法实例

    • 寻找最大值:给定一个数组和一个窗口大小k,求出所有可能的k大小窗口中的最大值。这类问题可以通过维护一个双端队列来解决,队列中存储当前窗口内所有元素的索引,队列头指向当前最大值。
    • 字符串转化:在两个字符串之间,找出使得一个字符串转换成另一个字符串所需的最小编辑距离,这也可以通过滑动窗口的思想来解决,通过动态比较两个字符串的字符差异来减少不必要的计算。

总的来说,滑动窗口作为一种强大的数据处理技术,不仅在算法竞赛和数据科学领域中有着广泛的应用,在网络通信领域也扮演着重要的角色。通过有效的窗口管理和数据控制,可以显著提高数据处理效率和网络传输的稳定性。

漫画展示流程

在理解“窗口”这一概念时,我们应将其视为一种容器,能够包含其他事物。例如,你眼前的屏幕,图片中的容器或者是这张图片,甚至整个地球,在抽象的层面上,都可以被视为一个窗口。 image.png 接下来,让我们深入探讨“滑动”的概念。当黄色物体从左向右移动,再从右向左返回时,这种位移过程在抽象意义上可以被定义为“滑动”。 doutub_gif.gif 我们已经初步了解了“滑动”和“窗口”的概念。那么,将这两个概念结合起来,便形成了“滑动窗口”。实际上,滑动窗口即是一个可以移动的窗口,它可以根据需要调整位置和大小,类似于在屏幕上滑动的窗口。

设想这样一个场景:多人排成一行,每人手中持有不同数量的苹果。此时,有一位想要收集苹果的大佬,他给你提出了一个挑战——你需要从连续的人群中,选出人数数量最少但同时能满足或者大于大佬指定苹果数量要求。面对这种复杂情况,你该如何巧妙地选取人以满足大佬的要求呢? image.png 在这个关键时刻,一只神秘的狸猫出现了,它带来了一个能够自动滑动的窗口工具,并递给了你。这个工具或许就是解决当前难题的关键。 image.png 于是,滑动窗口工具开始了它的运作。 image.png image.png 你询问滑动窗口工具:“大佬要求的苹果数量是7,我现在有8个,能否去掉一个人?”滑动窗口回答:“当然可以试试看!”于是,队列中最左边的那个人被剔除了。但是,不幸的是,剩余的苹果数量仅为6,无法满足大佬的要求。因此,不得不将最初被剔除的那个人重新纳入考虑范围。然后,滑动窗口工具继续其工作。 image.png image.png 当滑动窗口工具框选到第五个人时,它向你建议:“现在你可以尝试剔除队列中的第一个人。”即使进行了这样的调整,苹果的总数依然能够满足大佬的要求。 image.png 滑动窗口工具继续运作,建议再剔除一个人。这次操作后,苹果的总数恰好为7个,正好满足大佬的要求,并且所需的人数也比原来满足8个苹果的情况少了一个人。看到这种情况,你试图继续剔除更多的人,但滑动窗口工具立刻阻止了你:“停止,如果再继续剔除,就将不满足大佬的要求了!” image.png 滑动窗口工具再次启动,将最后一个人纳入了框选范围。 image.png 这时,你变得更加机智,意识到只有当剔除一个人后仍能满足要求的情况下,才能进行剔除。因此,你对滑动窗口工具说:“剔除两个人吧。”最终,在你巧妙地利用狸猫提供的滑动窗口工具的帮助下,完美地完成了这项挑战性的工作。在仅仅两个人的情况下就满足了大佬的要求。 image.png

大白话讲算法

滑动窗口,简而言之,是一个能够移动的容器,旨在通过遍历所有可能的情况来找出最优解。在这个过程中,关键是要及时排除那些非最优解的情况。以前面的例子为例,即使你已经满足了大佬对苹果数量的要求,你也需要及时地将多余的人从窗口中移除,这样才能够确保窗口中所呈现的始终是最优解。

具体实例

1、长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其总和大于等于 target 的长度最小的子数组[numsl, numsl+1, ..., numsr-1,numsr],并返回其长度。如果不存在符合条件的子数组,返回0。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组[4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:

  • 1 <= target <= 109
  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105

实现代码如下(看不懂可以回去看看漫画)

class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        n = len(nums)
        ans = inf  
        s = left = 0
        for right, x in enumerate(nums):  # 枚举子数组右端点
            s += x
            while s - nums[left] >= target:  # 尽量缩小子数组长度
                s -= nums[left]
                left += 1  # 左端点右移
            if s >= target:
                ans = min(ans, right-left+1)
        return ans if ans <= n else 0

2、乘积小于 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你返回子数组内所有元素的乘积严格小于 **k 的连续子数组的数目。

示例 1:

输入:nums = [10,5,2,6], k = 100
输出:8
解释:8 个乘积小于 100 的子数组分别为:[10][5][2],、[6][10,5][5,2][2,6][5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。

示例 2:

输入:nums = [1,2,3], k = 0
输出:0

提示:

  • 1 <= nums.length <= 3 * 104
  • 1 <= nums[i] <= 1000
  • 0 <= k <= 106

解题思路

此题与题目一类似,但需注意题目要求是严格小于k的连续子数组,且nums中的元素大于1。因此,当k小于等于1时,无法满足要求。当窗口元素为[n : n + k]时,包括[n: n + 1], [n : n + 2], [n : n + 3]... [n : n + k]在内的所有子数组都满足要求。当前满足要求的数组个数为窗口右端点减去左端点再加一,因为当窗口大小为一时,两个数重合仍然有一个数满足要求。

实现代码如下

class Solution:
    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
        if k <= 1:
            return 0
        ans = left = 0
        prod = 1
        for right, x in enumerate(nums):
            prod *= x
            while prod >= k:  # 不满足要求
                prod /= nums[left]
                left += 1
            ans += right - left + 1 # 当窗口大小为一时,两个数重合仍然有一个数满足要求
        return ans

3、无重复字符的最长子串

给定一个字符串s,请你找出其中不含有重复字符的最长子串的长度。

示例 1:

输入:s = "abcabcbb"
输出:3
解释: 因为无重复字符的最长子串是"abc",所以其长度为 3。

示例 2:

输入:s = "bbbbb"
输出:1
解释:因为无重复字符的最长子串是"b",所以其长度为 1。

示例 3:

输入:s = "pwwkew"
输出:3
解释:因为无重复字符的最长子串是"wke",所以其长度为 3。
     请注意,你的答案必须是子串的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

解题思路

使用滑动窗口来记录当前无重复字符的子串。当新字符进入滑动窗口时,如果该字符已存在于窗口中,则从左侧开始移除字符,直到窗口中不再包含重复字符为止。

实现代码如下

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        ans = left = 0
        window = set()  # 维护从下标 left 到下标 right 的字符
        for right, c in enumerate(s):
            # 如果窗口内已经包含 c,那么再加入一个 c 会导致窗口内有重复元素
            # 所以要在加入 c 之前,先移出窗口内的 c
            while c in window:  # 窗口内有 c
                window.remove(s[left])
                left += 1  # 缩小窗口
            window.add(c)  # 加入 c
            ans = max(ans, right - left + 1)  # 更新窗口长度最大值
        return ans

文章最后的碎碎念

算法的学习之旅漫长而充满挑战,希望大家能够持之以恒地学习下去。学无止境,每一次的努力都将为未来的成功铺就坚实的基石。在探索算法的世界中,愿各位不断进步,勇攀知识的高峰,最终达到理想的彼岸。让我们一起加油,为了更好的未来,为了更深入的理解,为了更广泛的应用,不断追求,永不止步!