如何用单调双端队列实现 O(n) 查询滑动窗口最大值索引的原理?
- 内容介绍
- 文章标签
- 相关推荐
问题背景与常见痛点
在实际项目中。往往需要在一个不断移动的窗口里实时获取最大值的索引。很多同学在实现时会遇到以下困惑:
-
窗口大小不固定:传统的暴力遍历每次都要重新扫描窗口,导致
O的时间开销。 - 难以同时维护“最大值”和“窗口边界”:既要把已经离开窗口的元素剔除,又要保证队首始终是当前最大值。
- 代码实现繁琐、易出错:手动维护两个指针、数组或链表会产生大量下标越界和重复元素残留的问题。
-
性能瓶颈:
O根本无法接受。
这些痛点正是我们引入单调双端队列的动机——它能一次遍历数组,即可在 O 时间内完成所有窗口的最大值索引查询。
单调双端队列的主要思想
单调双端队列是一种特殊的双端队列,它内部保存的是数组元素的索引而且这些索引对应的数值始终保持严格递减 的顺序。其实,
-
# 队头: 当前窗口内最大的元素索引。 -
# 队尾: 可能成为将来最大值的候选者。 -
# 单调性保证:
当新元素
x = nums到来时若它大于等于队尾对应的数值。就把队尾弹出,直至满足“从前到后数值递减”。这样被弹出的元素永远不可能再成为任何后续窗口的最大值。
为什么选择递减而不是递增?
因为我们关心的是"最大". 递减序列保证了"最大的"总是在最前面"读取最大值只需 d,不需要再遍历整个队列。
算法步骤详解
-
初始化:
from collections import deque dq = deque # 存放索引,保持对应数值递减 res = # 结果列表,保存每个窗口最大值所在索引 n = len k = window_size # 窗口长度,可变 - # 遍历数组。每个元素只进出一次:
-
移除已滑出窗口的索引:
while dq and dq <= i - k: dq.popleft # 超出左边界,直接弹出 -
维持单调递减性:
while dq and nums] <= nums: dq.pop # 当前元素更大,之前的小元素不可能再成为最大 dq.append # 把当前索引加入队尾 -
当窗口形成后记录答案:
if i>= k - 1: # 第 k-1 个位置起才有完整窗口 res.append # 队头就是当前窗口最大值的索引 -
# 最终返回结果:
return res
关键点回顾的观点是,一次遍历、每个元素最多进出 deque 一次 → 总体时间 O。空间上只存储最多 k 个索引 → O。话说回来,
完整代码示例
from collections import deque
from typing import List
def max_index_sliding_window -> List:
"""
返回每个长度为 k 的滑动窗口中最大值所在的 **索引**。
说到时间复杂度,O)
空间复杂度:O
"""
if not nums or k <= 0:
return
dq = deque # 保存候选最大值的下标,保持递减顺序
ans =
for i, val in enumerate:
# ① 移除已经不在窗口左侧的下标
while dq and dq <= i - k:
dq.popleft
# ② 删除所有比当前值小或相等的下标
while dq and nums] <= val:
dq.pop
# ③ 将当前下标加入队尾
dq.append
# ④ 当形成第一个完整窗口后记录答案
if i>= k - 1:
ans.append # 队首即为当前窗口最大值所在下标
return ans
# ------------------- 示例 -------------------
if __name__ == "__main__":
arr =
k = 3
print)
# 输出:
时间与空间复杂度分析
-
Total Time:
——每个元素最多进入 deque 一次、退出一次;所有循环操作均为常数时间。 -
Total Space:
——deque 中最多保存当前窗口内所有候选下标,最坏情况下为 window 长度。 - Avoided Pitfalls: 如果忘记在步骤 中先弹出超出左边界的下标。会导致 “过期” 索引仍然占据队首,从而返回错误答案。
常见错误与调试技巧
-
"忘记删除过期下标": 在加入新元素前一定要先检查并弹出所有 `
dq <= i - k` 的情况,否则结果会被旧的大数干扰。 - "比较方向写反了": 若想求最小值,请改为“保持递增”。否则会把正确答案误删掉,
- "直接存放数值而非下标": 存数值得不到“是否已滑出”信息;使用下标才能轻松判断是否仍在窗口内。
- "边界条件未处理": 当 `k == 1` 时每个位置本身就是答案;当 `k> len` 时应直接返回空列表或抛异常。
-
"打印调试": 在循环内部打印 `i`,`dq` 与 `ans` 能帮助快速定位逻辑错误,例如:
print}。ans={ans}")
与实战建议
- 单调双端队列是解决「滑动窗口最大/最小」这类问题的一把「瑞士军刀」;它让原本需要二重循环的问题降到线性时间。
- 在实际项目中,无论是实时日志分析、金融行情峰值检测还是机器学习特征抽取。都可以直接套用上述模板,只需根据业务需求把「递减」改成「递增」即可得到「最小」版本。
- 为了进一步提高鲁棒性。建议将上述函数封装成类,并加入参数校验、异常捕获还有可自定义比较函数。这样即使面对多维数据或自定义排序规则,也能轻松复用。
现在你已经掌握了使用单调双端队列在 O 时间内查询滑动窗口最大值索引** 的完整思路和实现细节。快去你的代码库里试一试吧!🚀
问题背景与常见痛点
在实际项目中。往往需要在一个不断移动的窗口里实时获取最大值的索引。很多同学在实现时会遇到以下困惑:
-
窗口大小不固定:传统的暴力遍历每次都要重新扫描窗口,导致
O的时间开销。 - 难以同时维护“最大值”和“窗口边界”:既要把已经离开窗口的元素剔除,又要保证队首始终是当前最大值。
- 代码实现繁琐、易出错:手动维护两个指针、数组或链表会产生大量下标越界和重复元素残留的问题。
-
性能瓶颈:
O根本无法接受。
这些痛点正是我们引入单调双端队列的动机——它能一次遍历数组,即可在 O 时间内完成所有窗口的最大值索引查询。
单调双端队列的主要思想
单调双端队列是一种特殊的双端队列,它内部保存的是数组元素的索引而且这些索引对应的数值始终保持严格递减 的顺序。其实,
-
# 队头: 当前窗口内最大的元素索引。 -
# 队尾: 可能成为将来最大值的候选者。 -
# 单调性保证:
当新元素
x = nums到来时若它大于等于队尾对应的数值。就把队尾弹出,直至满足“从前到后数值递减”。这样被弹出的元素永远不可能再成为任何后续窗口的最大值。
为什么选择递减而不是递增?
因为我们关心的是"最大". 递减序列保证了"最大的"总是在最前面"读取最大值只需 d,不需要再遍历整个队列。
算法步骤详解
-
初始化:
from collections import deque dq = deque # 存放索引,保持对应数值递减 res = # 结果列表,保存每个窗口最大值所在索引 n = len k = window_size # 窗口长度,可变 - # 遍历数组。每个元素只进出一次:
-
移除已滑出窗口的索引:
while dq and dq <= i - k: dq.popleft # 超出左边界,直接弹出 -
维持单调递减性:
while dq and nums] <= nums: dq.pop # 当前元素更大,之前的小元素不可能再成为最大 dq.append # 把当前索引加入队尾 -
当窗口形成后记录答案:
if i>= k - 1: # 第 k-1 个位置起才有完整窗口 res.append # 队头就是当前窗口最大值的索引 -
# 最终返回结果:
return res
关键点回顾的观点是,一次遍历、每个元素最多进出 deque 一次 → 总体时间 O。空间上只存储最多 k 个索引 → O。话说回来,
完整代码示例
from collections import deque
from typing import List
def max_index_sliding_window -> List:
"""
返回每个长度为 k 的滑动窗口中最大值所在的 **索引**。
说到时间复杂度,O)
空间复杂度:O
"""
if not nums or k <= 0:
return
dq = deque # 保存候选最大值的下标,保持递减顺序
ans =
for i, val in enumerate:
# ① 移除已经不在窗口左侧的下标
while dq and dq <= i - k:
dq.popleft
# ② 删除所有比当前值小或相等的下标
while dq and nums] <= val:
dq.pop
# ③ 将当前下标加入队尾
dq.append
# ④ 当形成第一个完整窗口后记录答案
if i>= k - 1:
ans.append # 队首即为当前窗口最大值所在下标
return ans
# ------------------- 示例 -------------------
if __name__ == "__main__":
arr =
k = 3
print)
# 输出:
时间与空间复杂度分析
-
Total Time:
——每个元素最多进入 deque 一次、退出一次;所有循环操作均为常数时间。 -
Total Space:
——deque 中最多保存当前窗口内所有候选下标,最坏情况下为 window 长度。 - Avoided Pitfalls: 如果忘记在步骤 中先弹出超出左边界的下标。会导致 “过期” 索引仍然占据队首,从而返回错误答案。
常见错误与调试技巧
-
"忘记删除过期下标": 在加入新元素前一定要先检查并弹出所有 `
dq <= i - k` 的情况,否则结果会被旧的大数干扰。 - "比较方向写反了": 若想求最小值,请改为“保持递增”。否则会把正确答案误删掉,
- "直接存放数值而非下标": 存数值得不到“是否已滑出”信息;使用下标才能轻松判断是否仍在窗口内。
- "边界条件未处理": 当 `k == 1` 时每个位置本身就是答案;当 `k> len` 时应直接返回空列表或抛异常。
-
"打印调试": 在循环内部打印 `i`,`dq` 与 `ans` 能帮助快速定位逻辑错误,例如:
print}。ans={ans}")
与实战建议
- 单调双端队列是解决「滑动窗口最大/最小」这类问题的一把「瑞士军刀」;它让原本需要二重循环的问题降到线性时间。
- 在实际项目中,无论是实时日志分析、金融行情峰值检测还是机器学习特征抽取。都可以直接套用上述模板,只需根据业务需求把「递减」改成「递增」即可得到「最小」版本。
- 为了进一步提高鲁棒性。建议将上述函数封装成类,并加入参数校验、异常捕获还有可自定义比较函数。这样即使面对多维数据或自定义排序规则,也能轻松复用。
现在你已经掌握了使用单调双端队列在 O 时间内查询滑动窗口最大值索引** 的完整思路和实现细节。快去你的代码库里试一试吧!🚀

