finish heap.md.
This commit is contained in:
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
@@ -142,3 +142,62 @@ entry<K, V> CBHeap<K, V>::delMax(){
|
||||
```
|
||||
|
||||
和`上滤插入`一样,容易证明`下滤删除`策略的正确性,因为这种下滤至多只会进行`h`次,`h`是完全二叉树的树高。因此,删除最大结点的时间复杂度仍然是`O(logn)`。
|
||||
|
||||
## 批量建堆
|
||||
|
||||
很多具体的应用场景中,初始的输入都是成批给出的,此时需要考虑的一个问题是,如何高效地将这些初始的数据组织成一个完全二叉堆,也就是这里的批量建堆问题。
|
||||
|
||||
### 蛮力算法
|
||||
|
||||
实际上,这个问题可以借助上面已经实现的`insert`接口快速地解决。为了将n个初始给定的元素组织成一个完全二叉堆,只需要对这n个元素依次调用`insert`函数即可,于是形成了下面的代码:
|
||||
|
||||
```cpp
|
||||
template <typename K, typename V>
|
||||
void CBHeap<K, V>::heapify(entry<K, V>* elems, int n){
|
||||
for(int idx = 0; idx != n; ++idx)
|
||||
insert(elems[idx]);
|
||||
}
|
||||
```
|
||||
|
||||
这种实现方法虽然简单,性能却比较糟糕。根据上面的分析,每次调用`insert`函数的时间复杂度为`O(logn)`,这样,将`n`个元素组织成一个完全二叉堆需要的时间成本为`O(nlogn)`,而在相同的时间内,已经可以对这`n`个元素做一个全排序了,我们这里却只得到了一个偏序关系,可见,这里的批量建堆算法是还有提升的余地的。
|
||||
|
||||
进一步考察这里的蛮力算法,可以看到,将所有的输入元素都组织成一个向量之后,第一次将一个元素插入到空堆当中,即形成了一个规模为1的堆;接下来插入第二个元素,等价于对向量中的第二个元素进行一次上滤操作`percolate_up`,于是形成了一个规模为2的堆;以下同理,再对接下来的元素进行上滤,形成一个规模为3的堆。
|
||||
|
||||
因此,这里的蛮力算法,其实是等价于将向量中的每一个元素,依次调用一次上滤操作`percolate_up`,就可以使之成为一个完全二叉堆。每个元素上滤的成本正比于它的深度,设最终完全二叉堆的高度为`h`,所以蛮力算法的时间复杂度为
|
||||
|
||||
$$
|
||||
2\times 1 + 2^2\times 2 + \cdots + 2^k\times k + \cdots + 2^h \times h = (h - 1)\times 2^{h + 1} + 2 = O(nlogn)
|
||||
$$
|
||||
|
||||
与上面分析的结论一致。
|
||||
|
||||
### `Floyd`算法
|
||||
|
||||
首先来考虑一个问题,对于两个给定的堆,和任意一个独立的节点,如何将它们组织成一个更大的堆。
|
||||
|
||||
实际上,这个问题已经在前面有过讨论了——在删除最大节点的算法中,不正是将向量末尾的节点作为新的树根,然后将该节点与左右子树两个堆进行合并吗?因此,只需要将两个给定的堆分别作为独立节点的左子树与右子树,然后对独立节点调用一次下滤操作`percolate_down`就可以了。
|
||||
|
||||
我们可以将上面的讨论推广到批量建堆的算法当中。首先需要明确,单个节点本身也形成一个完全二叉堆,满足`结构性`与`堆序性`。为了将所有的输入元素组织成一个完全二叉堆,首先仍然将它们组织成在一个向量当中,并且将它视作一棵完全二叉树,在初始状况下,只有树的所有叶节点各自构成一个规模为1的完全二叉堆。为了将这些完全二叉堆合并成一个更大的完全二叉堆,首先对于完全二叉树中的最后一个内部节点调用上面的合并算法,此时该节点连通它的孩子节点将构成一个更大的堆,此后只需要自后向前不断重复上述过程,就可以将各个子堆逐层合并,最终`堆序性`将在全局得到恢复。这就是`Floyd`建堆算法,它的代码如下:
|
||||
|
||||
```cpp
|
||||
#define LASTINTERNAL(n) PARENT(n - 1)
|
||||
|
||||
template <typename K, typename V>
|
||||
void CBHeap<K, V> heapify(entry<K, V>* elems, int n){
|
||||
copyfrom(elems, 0, n);
|
||||
for(int idx = LASTINTERNAL(n); idx >= 0; --idx)
|
||||
percolate_down(idx);
|
||||
}
|
||||
```
|
||||
|
||||
对`Floyd`算法进行分析,每一个内部节点,都将调用一次`percolate_down`函数,它的成本正比于每个节点的高度,因此整体的时间复杂度应该取决于所有内部节点的高度总和。假设完全二叉树的高度为`h`,则`Floyd`算法的时间复杂度为:
|
||||
|
||||
```
|
||||
h \times 1 + (h - 1)\times 2 + \cdots + (h - k)\times 2^{k} + \cdots + 1 \times 2^{h-1} = 2^{h + 1} - h - 2 = n - log(n) = O(n)
|
||||
```
|
||||
|
||||
可以看到,`Floyd`算法和蛮力算法相比,蛮力算法是每个节点都调用一次上滤操作`percolate_up`,而`Floyd`算法是每个节点调用一次下滤操作`percolate_down`,而这就导致了它们性能上的巨大差别。具体说来,上滤操作的时间复杂度正比于节点的深度,而下滤操作的时间复杂度正比于节点的高度,对于完全二叉堆中的所有节点,大部分都是处于底层的节点,它们的高度较小而深度较大,因此`Floyd`算法就是让多数节点的操作较快,而蛮力算法则是使多数节点的操作更慢,这就是它们性能上差别的本质原因。实际上,`Floyd`算法就类似于`Huffman`树的思想——使处于底层的多数节点具有更小的权重,或者更快的操作。
|
||||
|
||||
## 堆排序
|
||||
|
||||
堆的一个具体的应用就是堆排序算法。它的本质其实就是选择排序`SelectSort`,只是在选择当前最大元素的过程中,调用了堆的`getMax`函数,使得一次选择的时间消耗减少了。因为太简单,这里就不多叙述了。
|
||||
|
||||
Reference in New Issue
Block a user