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=1i12x=2i2, 所以总处理的项数为 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 次.

因此可以列出方程:

  1. 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=4nh=0lgnh(1/2)h1.
  2. 关注其极限情况, 由于这是一个正项级数, 有 T ( n ) < n 4 ∑ h = 0 ∞ h x h − 1 T(n)<{n\over4}\sum_{h=0}^{\infty}hx^{h-1} T(n)<4nh=0hxh1, x = 1 / 2 x=1/2 x=1/2.
  3. h x h − 1 hx^{h-1} hxh1可以看作是 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=0xh).
  4. 因为几何级数 ∑ h = 0 ∞ x h = 1 1 − x \sum_{h=0}^{\infty}x^h={1\over1-x} h=0xh=1x1, ∣ 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(1x1).
  5. 可得 T ( n ) < n 4 1 ( 1 − x ) 2 T(n)<{n\over4}{1\over({1-x})^2} T(n)<4n(1x)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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值