滑动窗口算法:双指针与双向队列实现总结

滑动窗口算法:双指针与双向队列实现总结

一、引言

在算法领域,处理数组或序列中的连续子结构问题时,滑动窗口算法是一种高效且常用的策略。它通过动态调整窗口的边界,避免了对所有可能子数组的暴力枚举,从而显著降低时间复杂度。本文将深入探讨滑动窗口算法的两种主要实现方式:双指针法和双向队列法,包括其原理、模板代码、适用场景及选择依据。

二、滑动窗口问题的特点与共性
  1. 连续性
    滑动窗口处理的对象是数组或序列中的连续元素。窗口会在序列上按顺序滑动,每次移动一个或多个位置,始终保持元素的连续性。例如,在求数组中连续子数组的和时,窗口所覆盖的元素在原数组中是相邻的。
  2. 动态性
    窗口的大小并非固定不变,而是可以根据具体问题的要求进行动态调整。窗口可能会扩大以包含更多元素,也可能会缩小以排除不必要的元素。比如,在寻找满足特定和条件的最短子数组时,窗口会根据当前子数组的和与目标值的比较结果进行伸缩。
  3. 优化性
    滑动窗口算法的核心优势在于优化时间复杂度。相较于暴力枚举所有可能的子数组,它能将时间复杂度从 O ( n 2 ) O(n^2) O(n2) 或更高降低到 O ( n ) O(n) O(n) 级别,大大提高了算法的执行效率。
三、何时考虑使用滑动窗口算法
  1. 子数组求和问题
    当需要计算数组中满足特定和条件的子数组时,滑动窗口算法是一个不错的选择。例如,求数组中所有长度为 k k k 的子数组的和,或者找出和大于等于某个给定值的最短子数组。
  2. 子数组计数问题
    在统计满足特定条件的子数组数量时,滑动窗口可以高效地完成任务。比如,统计数组中不同元素个数不超过 k k k 的子数组的数量。
  3. 子数组最值问题
    对于求数组中每个固定长度滑动窗口内的最大值或最小值的问题,滑动窗口算法能够在一次遍历中完成计算,避免了多次重复比较。
四、双指针法实现滑动窗口
1. 原理

双指针法通过两个指针(通常称为左指针和右指针)来定义窗口的边界。右指针用于扩展窗口,不断将新的元素纳入窗口;左指针用于收缩窗口,当窗口内的元素不满足特定条件时,将左侧的元素移除。通过移动这两个指针,动态调整窗口的大小和位置,从而遍历所有可能的子数组。

2. 模板代码
# 假设 arr 是输入数组,target 是目标值
arr = [1, 2, 3, 4, 5]
target = 9
left = 0
right = 0
window_sum = 0
result = []

while right < len(arr):
    # 扩展窗口
    window_sum += arr[right]

    # 根据条件收缩窗口
    while window_sum > target:  # 这里的条件根据具体问题调整
        window_sum -= arr[left]
        left += 1

    # 记录满足条件的结果
    if window_sum == target:
        result.append((left, right))

    right += 1

print(result)
3. 代码解释
  • leftright 分别表示窗口的左右边界,初始时都指向数组的第一个元素。
  • window_sum 用于记录窗口内元素的和,随着窗口的扩展和收缩不断更新。
  • 在每次扩展窗口后,根据具体问题的条件,使用 while 循环收缩窗口,直到满足条件为止。
  • 记录满足条件的结果,然后继续扩展窗口。
五、双向队列法实现滑动窗口
1. 原理

双向队列法借助双向队列(如 Python 中的 deque)来维护窗口内的元素。队列中存储的是元素的索引或值,通过在队列头部和尾部进行元素的插入和删除操作,保证队列内元素的有序性和满足特定条件。在处理滑动窗口问题时,能够快速获取窗口内的最大值或最小值。

2. 模板代码
from collections import deque

# 定义函数来找出数组中每个固定长度为 k 的滑动窗口内的最大值
def max_sliding_window(nums, k):
    result = []
    # 初始化双向队列
    q = deque()
    for i, num in enumerate(nums):
        # 循环删除队列中不在当前窗口内的元素
        while q and q[0] <= i - k:
            q.popleft()

        # 循环删除队列中小于当前元素的元素,保证队列头部是最大值
        while q and nums[q[-1]] < num:
            q.pop()

        # 将当前元素的索引加入队列
        q.append(i)

        # 当窗口长度达到 k 时,记录窗口内的最大值
        if i >= k - 1:
            result.append(nums[q[0]])

    return result

# 示例输入
nums = [1, 3, -1, -3, 5, 3, 6, 7]
k = 3
# 调用函数并输出结果
print(max_sliding_window(nums, k))
3. 代码解释
  • 队列初始化:创建一个双向队列 q 来存储元素的索引。
  • 遍历数组:对于数组中的每个元素:
    • 删除不在当前窗口内的元素:使用 while 循环,当队列头部的元素索引小于等于 i - k 时,说明该元素已经不在当前窗口内,将其从队列头部移除。
    • 删除小于当前元素的元素:使用 while 循环,当队列尾部的元素对应的数组值小于当前元素时,说明这些元素不可能是当前窗口的最大值,将其从队列尾部移除。
    • 添加当前元素:将当前元素的索引加入队列。
  • 记录最大值:当窗口长度达到 k 时,队列头部的元素对应的数组值就是当前窗口的最大值,将其记录到结果列表中。
六、如何选择双指针或双向队列
  1. 双指针法
    当问题只需要关注窗口的边界信息,不需要频繁在窗口内部进行元素的插入和删除操作时,双指针法较为适用。例如,求数组中满足和大于等于某个值的最短子数组长度,只需要通过移动左右指针来调整窗口的大小,不需要维护窗口内元素的顺序。
  2. 双向队列法
    当问题需要在窗口内部频繁进行元素的插入和删除操作,或者需要快速获取窗口内的最大值、最小值等信息时,双向队列法更为合适。例如,求数组中每个固定长度滑动窗口内的最大值,使用双向队列可以在 O ( 1 ) O(1) O(1) 时间内获取最大值,避免了每次都遍历窗口内的所有元素。
七、总结

滑动窗口算法是解决数组和序列中连续子结构问题的有力工具。双指针法和双向队列法作为其两种主要实现方式,各有优势。在实际应用中,需要根据问题的具体特点和要求,灵活选择合适的实现方法,以达到最优的算法效率。通过不断练习和实践,能够更加熟练地运用滑动窗口算法解决各种复杂问题。

你可能感兴趣的:(算法)