开始前,会这些就够了
- 会按下标读取列表,并能读懂 while 循环。
- 先读 数组、链表、栈与队列,认识队首与队尾。
仪表盘每次显示最近三条记录的最低耗时。只保存目前最小的数看起来很省事,但那条记录离开窗口后,程序还得知道谁能接替它。
本项目把“还能成为答案的记录”排成一个队列。它保存原下标,因此既能比较大小,也能判断哪条记录已经过期;原始记录不会被删除。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- demo.py:完整可运行源码
- README.md:运行步骤与练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和运行验证记录
- SHA256SUMS.txt:文件摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B demo.py解压后,在含 demo.py 的目录打开终端。命令分别回车执行。固定宽度整数列表;21324 组有限输入 oracle 核对和5类拒绝输入,含过期/重复值/负数/宽度1与全长;轨迹复制另计成本,未做性能基准。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
窗口向右走,哪些下标该离开队列?
正在准备实验控件…
固定六个数,队尾用 ≥ 删除被新值支配的旧候选,因此相等时保留更新的下标。展示的是最小值队列;不同等号策略可能保留不同候选,但应得到同样的窗口最小值。
先看问题:最小值也有有效期
把 [1, 2, 3, 4] 按宽度 2 向右滑动,三个完整窗口分别是 [1, 2]、[2, 3]、[3, 4],最低值应当是 [1, 2, 3]。如果只记住历史最小值 1,后两个窗口就错了。
每次重新扫描窗口容易理解,下面也用它作独立核对方式。但长度为 n、宽度为 k 时,这种直接做法可能重复查看同一项,最坏需要 O(nk) 次量级的工作。我们想保留足够的信息,避免每次从头找。
下标管有效期,数值管谁更有希望
处理下标 i 时,当前范围是 [i-k+1, i],两端都包含。队首下标若满足 <= i-k,就已经落在范围左侧,应先移除。窗口尚未凑够 k 项时只准备候选,不输出结果。
新值从右边进入。若队尾旧值大于或等于新值,就可以移除旧候选:新值不更大,而且来得更晚、也更晚过期。对于以后的完整窗口,只要这个旧候选仍有效,新候选也有效,因此旧候选不会独自成为更好的答案。
保存下标后,重复的 2 仍然可以区分来源。这里用 >= 移除相等旧值,保留较新的那个;若用 > 保留相等值,也能求出相同的最小值,但队列形状和最小值位置的选择规则会不同。
按轨迹看:队列只留可能接班的记录
默认数据是 [4, 2, 2, 5, 1, 3],宽度 3。读到下标 1 的 2,旧 4 被淘汰;读到下标 2 的另一个 2,旧 2 被更新的 2 替换。队列此时只剩下标 [2],第一个完整窗口的答案是 2。
下标 3 的 5 进入后,队列是 [2, 3]。下标 4 的 1 到来时,先弹出代表 5 的下标 3,再弹出代表 2 的下标 2,最终队列只剩 [4]。完整输出是 [2, 2, 1, 1]。
程序中的 expired 表示因窗口过期而从队首移除,dominated 表示被新值替代而从队尾移除。默认例子主要展示后者;独立的递增例子会打印 expired_indices,展示旧最小值真正离开窗口。
while 套在 for 里,为什么总量仍是线性的
一次新值到来,确实可能弹出许多候选,不能保证每一步都只做一次弹出。但一个下标只入队一次;离开队列后不会再次进入,所以全部队首、队尾移除次数之和至多为 n。把整次扫描的工作加起来,才得到 O(n)。
deque 的两端操作和端点读取适合这个过程;若改成 list.pop(0),每次还可能移动一批元素,原来的成本分析就不能照搬。队列辅助空间为 O(k),返回的答案列表占 O(n-k+1)。
trace=True 另外复制每一步队列,以便教学观察,最坏会增加 O(nk) 的时间和存储。线性结论针对关闭轨迹时的求解过程;穷举核对调用的就是 trace=False。这里没有把打印轨迹的教学模式说成线性内存算法。
保存一个文件,运行轨迹和穷举核对
将完整代码保存为 demo.py,在该目录执行 python demo.py。只使用 Python 标准库,不需要下载依赖。2026-10-23 在 Windows、CPython 3.14.0 实际运行。
程序穷举长度 1–7、值只取 -1、0、1 的所有列表,并逐一选择每个合法宽度。共 21,324 组列表与宽度组合,每组与逐窗 min 扫描比较,同时检查每项入队一次、移除总数不超过输入长度。检查使用显式异常,python -O 不会关闭它们。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Integer sliding minima; no third-party dependencies or benchmark claims."""
from collections import deque
from itertools import product
def require(condition, message):
if not condition:
raise AssertionError(message)
def sliding_minimum(values, width, trace=False):
if type(width) is not int or not 1 <= width <= len(values):
raise ValueError("width must be an integer in 1..len(values)")
if any(type(value) is not int for value in values):
raise TypeError("this example accepts integer values only")
queue, minima, events = deque(), [], []
pushes = removals = 0
for index, value in enumerate(values):
expired, dominated = [], []
while queue and queue[0] <= index - width:
expired.append(queue.popleft())
removals += 1
while queue and values[queue[-1]] >= value:
dominated.append(queue.pop())
removals += 1
queue.append(index)
pushes += 1
minimum = values[queue[0]] if index >= width - 1 else None
if index >= width - 1:
minima.append(minimum)
if trace:
events.append((index, expired, dominated, list(queue), minimum))
return minima, events, (pushes, removals)
def oracle(values, width):
return [min(values[i:i + width]) for i in range(len(values) - width + 1)]
def main():
values, width = [4, 2, 2, 5, 1, 3], 3
minima, events, counts = sliding_minimum(values, width, trace=True)
print(f"values={values}; width={width}")
for index, expired, dominated, queue, minimum in events:
print(f"i={index}; expired={expired}; dominated={dominated}; "
f"queue={queue}; min={minimum}")
print(f"minima={minima}; pushes={counts[0]}; removals={counts[1]}")
require(minima == [2, 2, 1, 1], "default example")
expiry, expiry_events, _ = sliding_minimum([1, 2, 3, 4], 2, trace=True)
require(expiry == [1, 2, 3], "expiry")
print(f"expiry_minima={expiry}; expired_indices={[e[1] for e in expiry_events]}")
require(sliding_minimum([2, 2, 2], 2)[0] == [2, 2], "equal values")
require(sliding_minimum([-3, 0, -1], 1)[0] == [-3, 0, -1], "width one")
require(sliding_minimum([5, 4, 3], 3)[0] == [3], "full width")
invalid = 0
for data, size in [([], 1), ([1], 0), ([1], 2), ([1], True)]:
try:
sliding_minimum(data, size)
except ValueError:
invalid += 1
else:
raise AssertionError("invalid width accepted")
try:
sliding_minimum([float("nan")], 1)
except TypeError:
invalid += 1
else:
raise AssertionError("unsupported numeric domain accepted")
cases = 0
for length in range(1, 8):
for data in product((-1, 0, 1), repeat=length):
for size in range(1, length + 1):
actual, _, (pushes, removals) = sliding_minimum(data, size)
require(actual == oracle(data, size), (data, size))
require(pushes == length and removals <= length, "operation bound")
cases += 1
print(f"oracle_cases={cases}; rejected_inputs={invalid}")
print("PASS")
if __name__ == "__main__":
main()
怎样运行:在独立练习文件夹中,依次执行下载区的命令。2026-10-23 使用 Python 3.10 或更新版本,无第三方依赖;实测 Windows / Python 3.14.0,Python 3.10 仅做语法兼容检查 复跑这份源码并核对完整输出。固定宽度整数列表;21324 组有限输入 oracle 核对和5类拒绝输入,含过期/重复值/负数/宽度1与全长;轨迹复制另计成本,未做性能基准。
运行后对照结果
values=[4, 2, 2, 5, 1, 3]; width=3
i=0; expired=[]; dominated=[]; queue=[0]; min=None
i=1; expired=[]; dominated=[0]; queue=[1]; min=None
i=2; expired=[]; dominated=[1]; queue=[2]; min=2
i=3; expired=[]; dominated=[]; queue=[2, 3]; min=2
i=4; expired=[]; dominated=[3, 2]; queue=[4]; min=1
i=5; expired=[]; dominated=[]; queue=[4, 5]; min=1
minima=[2, 2, 1, 1]; pushes=6; removals=4
expiry_minima=[1, 2, 3]; expired_indices=[[], [], [0], [1]]
oracle_cases=21324; rejected_inputs=5
PASS
把输入约定说清楚,再讨论结果
本例只接收整数值,宽度必须是 1 到列表长度之间的整数。空列表、宽度为 0、超过长度以及 True 这样的布尔宽度均拒绝;另外拒绝 NaN 所在的非整数数据域。不要把浮点 NaN 的比较规则默默带进单调性证明。
宽度为 1 时结果应与输入相同,宽度等于列表长度时只输出一个数。重复值、负数、递增和递减顺序也有检查。输入需要可取长度和按下标读取;它不是直接接收无限流的接口。
穷举覆盖的是明确给出的有限输入域,不是对任意 Python 对象的正确性证明,也没有测耗时排名。总量分析来自候选进出次数,实际运行用于核对实现和边界。
什么时候可以照这个思路改,什么时候需要重想
最近 k 条记录的最小值、最大值适合维护这类候选。若改求最大值,需要同时改变淘汰方向和比较规则,并重新核对重复值;不能只把输出函数名字改掉。
它依赖窗口按输入顺序向右推进。如果可以任意删除中间记录、回到更早窗口,或者查询任意区间,这个队列保留的信息就未必足够。本篇也没有处理乱序事件时间、并发更新和窗口中位数。
阅读自己的实现时,先检查过期公式、相等值策略和输出起点,再数每个下标会进出几次。这三处比记住“用单调队列”这个名字更有用。
记住这些要点
- 下标同时携带位置和有效期,不能只保留一个历史最小数。
- 淘汰旧候选的理由是新值不更大且更晚过期。
- 单次步骤可弹出多项,但每项一生至多进出一次。
- 轨迹复制的成本需要单独说明。
别把例子用过头
- 用队列长度代替原记录下标判断过期。
- 把 >= 和 > 当作没有差别,忽略相等值的身份策略。
- 用
list.pop(0)后仍直接沿用 deque 的复杂度。 - 只跑默认样例,漏掉旧最小值真正过期的情况。
自己试一小步
为每个完整窗口返回最小值和它在窗口内最早出现的原下标,你会怎样调整候选规则?
- 先选一个包含相等最小值的列表,手写每个窗口的预期二元组。
- 明确你的相等值处理规则,再修改函数返回结果。
- 加入宽度为 1、全长、递增和全相等用例。
- 编写按窗口完整扫描的独立核对函数,再穷举小输入。
做到这些,就算完成
- 每个返回下标都仍在当前窗口内。
- 同值时确实选择最早下标,而非碰巧拿到一个正确数值。
- 实验记录区分求解过程与轨迹展示的成本。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Competitive Programmer’s Handbook,8.3 Sliding window minimum (新标签页)↗
单调队列保留候选、移除过期元素,以及每项至多进入和离开一次的总工作量解释。
- Python 3.14 collections:deque objects (新标签页)↗
deque 两端追加和弹出、端点访问,以及
list.pop(0)与中间索引的成本边界。