Bagaking细讲算法 - 堆 II
堆 II: 堆的常见操作及其复杂度分析
现在我们来讨论一个完整堆作为数据结构, 其实现一般包含哪些操作, 以及这些操作的复杂度.
一般情况下堆的所有操作
- Basic
- find-max/find-min/peek: 查看最值
- insert/push/put: 插入值
- extract-max/extract-min/pop/poll: 弹出值
- delete-max/delete-min: 删除当前最值
- replace: 删除当前最值, 并插入一个新值
- Inspection
- size/length/count: 返回项数
- is-empty: 返回是否为空
- Creation
- create: 创建
- heapify: 由给定列表创建
- merge: 在保留原始堆的情况下, 合并两个堆
- meld: 在不保留原始堆的情况下, 合并两个堆
- Internal
- sift-up: 使一个元素在满足堆性质的情况下上浮到最高位置
- sift-down: 使一个元素在满足堆性质的情况下下沉到最低位置
- delete: 删除给定下标的元素, 并保证堆的性质
- increase-key/decrease-key (i, key): 在最大堆/最小堆中下标为i的元素增加/减少到值key, 值key不能小于/大于项i, 并保证堆的性质.
其中 Basic 操作的复杂度在 上篇文章 堆 I: 堆的原理和应用场景 中已经分析过, 这篇里主要讨论 Creation 操作的复杂度.
creation 操作的复杂度
正如上篇文章中提到的, 显然 sift-up 和 sift-down 操作的复杂度都是 O ( l g n ) O(lgn) O(lgn), 而所有单个元素的修改操作, 都是O(1) + sift-up 或 sift-down, 因此 insert, extract, delete, increase/decrease, replace 的复杂度也为 O ( l g n ) O(lgn) O(lgn).
我们重点来看一下几个 creation 操作的复杂度.
create
create 是从空堆开始一个一个添加项的操作, 每个项到达正确的位置, 都需要 O ( l g n ) O(lgn) O(lgn) 次搜索/交换, 因此, 总的复杂度是 O ( n l g n ) O(nlgn) O(nlgn)
heapify
heapify 是将已经存在的一个序列直接转换成堆, 容易想到 O ( n l g n ) O(nlgn) O(nlgn) 的方法.
Floyd 提出了一种复杂度为 O ( n ) O(n) O(n) 的方法 Floyd’s heap-construction, 以下给出实现和证明:
void heapify(int[] a, int count){
start = (count - 2) / 2; // get parent of element count - 1
while start >= 0 {
siftDown(a, start--, count - 1);
}
}
列表的最后一个节点是 end = count - 1
, 其父节点为 start = (end - 1) / 2
. 显然, 下标大于 start 的所有节点均为叶子节点. 显然, 遍历非叶子节点的所有节点, 并将其下沉, 则从 0 到 start 的所有节点, 满足堆的性质. 而对于叶子节点, 可以用反证法证明. 整个序列符合堆的性质.
这个方法乍看起来最差情况下复杂度上界似乎是 O ( n l g n ) O(nlgn) O(nlgn) : siftDown 的复杂度是 O(lgn), 共执行 O(n) 次. 如果仔细想想的话, 可以认为这个上界成立, 但一定不是紧逼 (asymptotically tight) 的. 所以我们从定义触发, 来考察asymptotically tight upper bound.
显然, 越靠近根节点越少, 而 siftDown 的次数越接近
l
g
n
lgn
lgn, 而越原理根节点越多, siftDown 的次数接近于 1. 如果我们将一棵二叉树的各层从上往下数, 分别标记为 i 层, i = 1,2,3,4...
, root即为 i=1
层. 则每一层对应的节点数为
2
i
2^i
2i. 而根据二进制的性质
∑
x
=
1
i
−
1
2
x
=
2
i
−
2
\sum_{x=1}^{i-1}2^x = 2^i-2
∑x=1i−12x=2i−2, 所以总处理的项数为 M(=n/2)
的话, 有接近 M/2
项最多移动1次, 接近 M/4
项最多移动2次, M/8
项最多移动3次 … M/2^h
(
n
/
2
h
+
1
n/2^{h+1}
n/2h+1) 项最多移动 h
次.
因此可以列出方程:
- T ( n ) = ∑ h = 0 l g n n h 2 h + 1 = n 4 ∑ h = 0 l g n h ( 1 / 2 ) h − 1 T(n)=\sum_{h=0}^{lgn}{nh\over2^{h+1}}={n\over4}\sum_{h=0}^{lgn}h{(1/2)}^{h-1} T(n)=∑h=0lgn2h+1nh=4n∑h=0lgnh(1/2)h−1.
- 关注其极限情况, 由于这是一个正项级数, 有 T ( n ) < n 4 ∑ h = 0 ∞ h x h − 1 T(n)<{n\over4}\sum_{h=0}^{\infty}hx^{h-1} T(n)<4n∑h=0∞hxh−1, x = 1 / 2 x=1/2 x=1/2.
- h x h − 1 hx^{h-1} hxh−1可以看作是 x h x^h xh的导数, 因此 T ( n ) = n 4 d d h ( ∑ h = 0 ∞ x h ) T(n)={n\over4}{d\over dh}(\sum_{h=0}^{\infty}x^h) T(n)=4ndhd(∑h=0∞xh).
- 因为几何级数 ∑ h = 0 ∞ x h = 1 1 − x \sum_{h=0}^{\infty}x^h={1\over1-x} ∑h=0∞xh=1−x1, ∣ x ∣ < 1 |x|<1 ∣x∣<1, 则 T ( n ) = n 4 d d x ( 1 1 − x ) T(n)={n\over4}{d\over dx}({1\over{1-x}}) T(n)=4ndxd(1−x1).
- 可得 T ( n ) < n 4 1 ( 1 − x ) 2 T(n)<{n\over4}{1\over({1-x})^2} T(n)<4n(1−x)21, 将 x = 1 / 2 x=1/2 x=1/2 带入, 得 T ( n ) = n T(n)=n T(n)=n.
得证其时间复杂度为 O ( n ) O(n) O(n).
Floyd’s heap-construction 的标准实现由于是逐层向根节点遍历, 相当于广度优先的回归过程. 这有一个问题是, 一旦数据量超过了CPU缓存, 可能会出现大量的缓存失效 (Cache misses), 一个更好的方法是使用深度优先的方法进行合并, 这样能够尽快的将子堆合并, 而不是逐层合并.
merge/meld
对于2叉堆, 没有特别好的合并方法. 一般来说直接把两个堆串联, 使用 heapify 重排, 时间复杂度是 O ( n ) O(n) O(n). n 为两个堆 size 之和.
不过这是对于2叉堆而言的, 之后我们会介绍几种其他类型的堆, 最好可以达到 O ( 1 ) O(1) O(1) 的时间复杂度
Reference
https://en.wikipedia.org/wiki/Heap_(data_structure)
https://en.wikipedia.org/wiki/Taylor_series
https://en.wikipedia.org/wiki/Heapsort