doocs/leetcode 单调队列实战:从滑动窗口到最优解

doocs/leetcode 单调队列实战:从滑动窗口到最优解

【免费下载链接】leetcode 🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解 【免费下载链接】leetcode 项目地址: https://gitcode.com/doocs/leetcode

引言:为什么需要单调队列?

在算法竞赛和实际开发中,我们经常遇到需要维护一个数据结构的"窗口"或"区间"的问题。传统的暴力解法时间复杂度往往无法满足大规模数据的需求,这时单调队列(Monotonic Queue)就成为了解决这类问题的利器。

单调队列是一种特殊的双端队列(Deque),它能够在O(1)或O(n)的时间复杂度内维护队列的单调性,特别适合解决滑动窗口相关的最值问题。本文将深入探讨单调队列的原理、实现,并通过doocs/leetcode中的经典题目来展示其强大威力。

单调队列核心原理

基本概念

单调队列维护的是一个具有单调性的序列,通常用于快速获取滑动窗口中的最大值或最小值。其核心思想是:

  1. 维护单调性:队列中的元素保持单调递增或递减
  2. 淘汰策略:新元素加入时,淘汰所有破坏单调性的旧元素
  3. 窗口管理:及时移除超出窗口范围的元素

算法流程

mermaid

经典实战:滑动窗口最大值

问题描述

给定一个整数数组 nums 和一个大小为 k 的滑动窗口,窗口从数组的最左侧移动到最右侧。你需要找出每个滑动窗口中的最大值。

示例

输入: nums = [1,3,-1,-3,5,3,6,7], k = 3
输出: [3,3,5,5,6,7]

暴力解法 vs 单调队列解法

方法时间复杂度空间复杂度适用场景
暴力遍历O(n*k)O(1)小规模数据
优先队列O(n*logk)O(k)中等规模
单调队列O(n)O(k)大规模数据

单调队列实现

from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        q = deque()  # 存储元素索引,维护递减序列
        ans = []
        
        for i, x in enumerate(nums):
            # 移除超出窗口的元素
            if q and i - q[0] >= k:
                q.popleft()
            
            # 维护单调递减性
            while q and nums[q[-1]] <= x:
                q.pop()
            
            q.append(i)
            
            # 当窗口形成时记录最大值
            if i >= k - 1:
                ans.append(nums[q[0]])
        
        return ans

执行过程分析

nums = [1,3,-1,-3,5,3,6,7], k = 3 为例:

步骤当前元素队列状态窗口最大值
11[0][1]-
23[1][1,3]-
3-1[1,2][1,3,-1]3
4-3[1,2,3][3,-1,-3]3
55[4][-1,-3,5]5
63[4,5][-3,5,3]5
76[6][5,3,6]6
87[7][3,6,7]7

进阶应用:满足不等式的最大值

问题描述

给定按x坐标排序的点数组 points 和整数 k,找出 yi + yj + |xi - xj| 的最大值,其中 |xi - xj| <= k

数学变换与单调队列应用

将原式进行变换:

yi + yj + |xi - xj| 
= yi + yj + (xj - xi)   # 因为 xj > xi
= (yi - xi) + (xj + yj)

因此问题转化为:对于每个点 (xj, yj),找到前面满足 xj - xi <= k 的点中 (yi - xi) 的最大值。

单调队列解法

from collections import deque

class Solution:
    def findMaxValueOfEquation(self, points: List[List[int]], k: int) -> int:
        ans = float('-inf')
        q = deque()  # 存储(x, y)元组
        
        for x, y in points:
            # 移除超出窗口的点
            while q and x - q[0][0] > k:
                q.popleft()
            
            # 计算当前最大值
            if q:
                ans = max(ans, x + y + q[0][1] - q[0][0])
            
            # 维护单调性:保持 y-x 递减
            while q and y - x >= q[-1][1] - q[-1][0]:
                q.pop()
            
            q.append((x, y))
        
        return ans

单调队列的变体与应用场景

常见变体

  1. 单调递增队列:用于求滑动窗口最小值
  2. 单调递减队列:用于求滑动窗口最大值
  3. 双单调队列:同时维护最大值和最小值

典型应用场景

应用场景问题特征解决方案
滑动窗口最值固定大小窗口,求最值单调队列
区间最值查询多次查询区间最值单调队列+预处理
优化动态规划DP状态转移需要区间最值单调队列优化
实时数据流流数据中维护最近k个元素的最值单调队列

性能对比与复杂度分析

时间复杂度对比

mermaid

空间复杂度分析

方法空间复杂度说明
暴力解法O(1)不需要额外空间
优先队列O(k)堆的大小为窗口大小
单调队列O(k)队列长度最多为k
线段树O(n)需要构建线段树

实战技巧与注意事项

实现技巧

  1. 索引存储:通常存储元素索引而非值本身,便于判断是否超出窗口
  2. 双端操作:使用双端队列支持头部删除和尾部操作
  3. 提前判断:在添加新元素前先处理窗口边界

常见错误

# 错误示例:先添加再判断窗口
q.append(i)
if i - q[0] >= k:  # 可能已经添加了超出窗口的元素
    q.popleft()

# 正确做法:先判断窗口再添加
if q and i - q[0] >= k:
    q.popleft()
# ...维护单调性...
q.append(i)

边界情况处理

  1. 空队列处理:操作前检查队列是否为空
  2. 窗口大小为1:特殊情况需要单独处理
  3. 重复元素:根据题目要求决定是否保留重复值

扩展应用:单调队列在动态规划中的优化

单调队列不仅可以用于滑动窗口问题,还能优化某些动态规划问题的时间复杂度。

带限制的子序列和

问题:给定整数数组 nums 和整数 k,找出长度不超过 k 的连续子数组的最大和。

def constrainedSubsetSum(self, nums: List[int], k: int) -> int:
    n = len(nums)
    dp = [0] * n
    q = deque()
    ans = float('-inf')
    
    for i in range(n):
        # 移除超出窗口的索引
        while q and i - q[0] > k:
            q.popleft()
        
        # 计算当前dp值
        dp[i] = nums[i]
        if q:
            dp[i] = max(dp[i], nums[i] + dp[q[0]])
        
        # 维护单调递减队列
        while q and dp[i] >= dp[q[-1]]:
            q.pop()
        
        q.append(i)
        ans = max(ans, dp[i])
    
    return ans

总结与展望

单调队列作为一种高效的数据结构,在解决滑动窗口和最值问题时表现出色。通过本文的实战分析,我们可以看到:

  1. 时间复杂度优势:从O(n*k)优化到O(n)
  2. 空间效率:仅需要O(k)的额外空间
  3. 应用广泛:不仅限于滑动窗口,还能优化动态规划等问题

在实际应用中,建议:

  • 熟练掌握单调队列的基本思想和实现
  • 理解不同问题如何转化为单调队列可解的形式
  • 注意边界条件的处理和代码的健壮性

通过doocs/leetcode中的丰富题目练习,开发者可以深入掌握这一重要算法技巧,为应对复杂的算法面试和实际开发问题打下坚实基础。

【免费下载链接】leetcode 🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解 【免费下载链接】leetcode 项目地址: https://gitcode.com/doocs/leetcode

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

抵扣说明:

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

余额充值