固定时间窗口内的流式中位数
Streaming Median Over a Fixed Time Window
开始编码某市场微观结构监控面板接收带标签的事件流 (ts, value)——比如当前买卖价差(单位:基点)、或往返延迟(单位:微秒)——每来一笔新事件都要打印 **过去 window_seconds 内的中位数**。滚动中位数比滚动均值在生产环境里要稳健得多:单笔异常打印(乌龙指、过期报价、路由抖动)不会把曲线拉飞,曲线干净地反映市场的「典型」状态,基于其上的告警逻辑也不会被尖刺反复触发。
实现 solution(events: list[list], window_seconds: int) -> list[float]。按时间顺序处理事件。每来一笔:先驱逐 所有活动集中 ts <= current_ts - window_seconds 的旧事件,再加入 新值,再计算 中位数。最终按输入顺序返回每笔事件对应的中位数。例如,solution([[1, 5.0], [3, 2.0], [8, 7.0], [12, 4.0], [20, 9.0]], 10) 返回 [5.0, 3.5, 5.0, 4.0, 9.0]。在 t=12 时 cutoff=2,t=1 处的 5.0 被驱逐;活动集变为 {2, 7, 4},中位数=4.0。在 t=20 时 cutoff=10,所有更早的事件都被驱逐,活动集只剩 {9},中位数=9.0。
参考算法是 双堆 + 惰性删除:大顶堆 lower 存较小一半,小顶堆 upper 存较大一半。用一个按 event id 索引的 FIFO 队列定位每步要驱逐的条目;把被驱逐的条目标记为墓碑,读堆顶前先剪掉墓碑。维护的尺寸不变量是 有效(未墓碑化)条目的 lower_size - upper_size ∈ {0, 1}。每次插入后还要恢复有序不变量 lower_max ≤ upper_min,必要时把两个堆顶交换。每个事件至多入堆一次、出堆一次,总开销 O(n log n)——和一次排序同阶,但是在线的、且支持窗口驱逐。
实践背景
时间窗口滚动中位数是 实时微观结构监控 里的看家原语:滚动中位数价差、滚动中位数报价延迟、滚动中位数订单簿失衡——只要某台桌想在异常打印不断的环境里追踪「典型」状态的漂移,就用它。双堆模式是 LeetCode 295(数据流中位数)的教科书解法;时间窗口滑动是把它推向生产化的关键变形,因为「每步重排再取中间」是 O(n) per step 跑不大,而二叉堆没有按值索引,真正的 del 要 O(n)。惰性删除——用墓碑标记并在堆顶处跳过——和实时推荐系统的滑窗 top-k、操作系统支持取消的优先队列、以及堆式定时器轮里用的是同一个技巧;均摊每个事件 O(log n),因为每个条目至多入堆一次、出堆一次。side[event_id] 表(记录条目当前在哪个堆里)是细节中容易漏掉、但承重的一处:漏了它,驱逐时会扣错某一侧的尺寸计数,中位数会悄悄偏一格。
约束条件
- 0 ≤ `len(events)` ≤ 10000
- 每个 event 是 2 元素列表 `[ts, value]`,`ts` 为整数、`value` 为数值(int/float)
- `events` 按 `ts` 升序排列;允许 `ts` 重复(同一时刻多笔事件)
- 1 ≤ `window_seconds` ≤ 1000000
- 驱逐规则:`ts <= current_ts - window_seconds` 的事件移出活动窗口
- 偶数个活动元素的中位数取中间两值的 **算术平均**;奇数个取唯一中间值
- 空输入返回 `[]`;否则输出长度等于输入长度
- 比较容差:`rel_tol = 1e-9`,`abs_tol = 1e-12`
样例
Case 1 · statement-example: window of 10s, evicting old ticks
输入: [[[1,5],[3,2],[8,7],[12,4],[20,9]],10]
期望: [5,3.5,5,4,6.5]
窗口=10s,逐个事件:t=1 仅 5.0,中位数=5.0;t=3 加入 2.0,排序 [2,5],中位数=(2+5)/2=3.5;t=8 加入 7.0,活动集 {5,2,7},中位数=5.0;t=12 时 cutoff=2,t=1 的 5.0 被驱逐(ts=1<=2),活动集 {2,7,4},中位数=4.0;t=20 时 cutoff=10,所有更早的都被驱逐,只剩新值 9.0,中位数=9.0。
Case 2 · single event returns its own value
输入: [[[100,3.14]],60]
期望: [3.14]
只有一笔事件时,活动集就是它自己,中位数即该事件的 value。验证仅一个堆有元素时也能正确输出。
Case 3 · window=1 — every step has only the current event in window
输入: [[[1,10],[2,20],[3,30],[4,40]],1]
期望: [10,20,30,40]
窗口=1 时,任何之前的事件 ts ≤ 当前 ts - 1 都被驱逐(因为时间戳间隔 >= 1)。每步活动集只有当前一个事件,中位数等于当前 value。
最近提交
还没有提交记录。
编码区
实现 solution(...)。本地运行当前支持 Python 可见样例;服务端提交会运行可见样例和隐藏测试。
默认展示公开样例。点击「运行样例」后会在这里显示实际输出;点击「提交评测」会进入隐藏测试。
Case 1 · statement-example: window of 10s, evicting old ticks
输入: [[[1,5],[3,2],[8,7],[12,4],[20,9]],10]
期望: [5,3.5,5,4,6.5]
窗口=10s,逐个事件:t=1 仅 5.0,中位数=5.0;t=3 加入 2.0,排序 [2,5],中位数=(2+5)/2=3.5;t=8 加入 7.0,活动集 {5,2,7},中位数=5.0;t=12 时 cutoff=2,t=1 的 5.0 被驱逐(ts=1<=2),活动集 {2,7,4},中位数=4.0;t=20 时 cutoff=10,所有更早的都被驱逐,只剩新值 9.0,中位数=9.0。
Case 2 · single event returns its own value
输入: [[[100,3.14]],60]
期望: [3.14]
只有一笔事件时,活动集就是它自己,中位数即该事件的 value。验证仅一个堆有元素时也能正确输出。
Case 3 · window=1 — every step has only the current event in window
输入: [[[1,10],[2,20],[3,30],[4,40]],1]
期望: [10,20,30,40]
窗口=1 时,任何之前的事件 ts ≤ 当前 ts - 1 都被驱逐(因为时间戳间隔 >= 1)。每步活动集只有当前一个事件,中位数等于当前 value。