第6章|二分、堆与优先队列
Unit 11 · LeetCode #295 · 状态依赖训练版
一个堆暴露一条边界,
两个堆共同夹住动态中位数。
左侧最大堆保存较小一半,右侧最小堆保存较大一半。只要所有左侧值不大于右侧值,并让左堆与右堆大小相等或多一个,中位数就只依赖两个堆顶。
题目要求
中位数是有序整数列表的中间值。若列表长度为偶数,则中位数是两个中间值的平均数。
实现 MedianFinder:addNum(num) 向数据流加入整数,findMedian() 返回目前所有元素的中位数。
约束
-10⁵ ≤ num ≤ 10⁵;- 调用
findMedian前至少已有一个元素; addNum与findMedian总调用次数最多5×10⁴。
官方示例
addNum(1)
addNum(2)
findMedian() → 1.5
addNum(3)
findMedian() → 2.0
中位数定义
[2,3] → (2+3)/2 = 2.5
本页主方法
左侧使用最大堆、右侧使用最小堆。每次插入为 O(log n),查询中位数为 O(1),总空间为 O(n)。
较小的一半数据。概念上使用最大堆,使左半最大值始终位于堆顶;Python以负数模拟。
较大的一半数据。使用最小堆,使右半最小值始终位于堆顶。
左堆所有真实值均不大于右堆所有值,因此两个堆顶正好位于排序中心两侧。
本页约定左堆大小等于右堆,或仅多一个;奇数中位数因此位于左堆顶。
左侧概念最大堆 / Python负数堆
右侧最小堆
中位数由有序数据中心的一个或两个边界值决定。
| 加入后数据 | 左堆顶 | 右堆顶 | 中位数 |
|---|
插入与平衡记录
顺序不变量
左堆中的每个真实值都不大于右堆中的每个值;两个堆顶因此是排序中心附近的边界。
大小不变量
左堆与右堆等大时取两个堆顶平均;左堆多一个时直接取左堆顶。
每次重新排序成本高;有序数组插入需要移动元素;双堆以O(log n)增量更新中心边界,并以O(1)读取中位数。
双堆必须同时满足顺序和大小两个不变量;任何一个被破坏,中位数都会失去含义。
即使两个堆数量相等,若左堆含10而右堆含2,两个堆顶并不代表排序中心。
Python左堆保存负数;真实左堆顶是 -lower[0],不能直接使用lower[0]。
中心位置会落在较大堆内部而不一定在堆顶,O(1)查询失效。
若所有数都在[0,100],可维护101个频率计数并扫描累计频率定位中心;若99%位于该范围,可对区间内计数并单独维护少量区间外数据。
可同时出现在两个堆
不影响边界定义
左堆仍用负号模拟
注意双重符号
计数数组空间O(1)
查询扫描101格
迁移题:LeetCode #480 滑动窗口中位数
在 #295 的双堆基础上增加“旧值离开窗口”。堆无法高效删除任意内部元素,因此需要延迟删除、有效元素计数和堆顶清理。
官方任务
给定整数数组 nums 和窗口大小 k。窗口每次向右移动一格,返回每个窗口的中位数。
nums = [1,3,-1,-3,5,3,6,7] k = 3 输出 = [1,-1,-1,3,5,6]
新增困难
- 每一步都加入一个新值。
- 除第一个窗口外,还要删除一个离窗旧值。
- 堆只能高效弹出根,不能直接删除内部任意元素。
- 答案误差在
10^-5内可接受;1 ≤ k ≤ n ≤ 10^5。
#295 → #480 结构对照
#295 持续数据流 操作:只 add 大小:物理堆大小 = 有效大小 过期值:不存在 查询:读取两个有效堆顶
#480 固定窗口 操作:add + remove 大小:必须维护逻辑有效大小 过期值:delayed[value] 记账 查询:先 prune,再读堆顶
按依赖关系排列一次窗口右移
官方样例窗口
物理堆、逻辑大小与延迟删除
第6章 Unit 11 Skill卡
动态中位数需要同时维护排序中心两侧:左最大堆提供较小一半的最大值,右最小堆提供较大一半的最小值;顺序不变量决定边界正确,大小不变量决定中位数只需O(1)读取。