From 3ccaf850a60239015ce0fa48565f8523b7bce882 Mon Sep 17 00:00:00 2001 From: Shine wOng <1551885@tongji.edu.cn> Date: Wed, 25 Sep 2019 17:58:33 +0800 Subject: [PATCH] finish heap.md. --- thu_dsa/chp10/{cbtree.png => cb_tree.png} | Bin thu_dsa/chp10/heap.md | 59 ++++++++++++++++++++++ 2 files changed, 59 insertions(+) rename thu_dsa/chp10/{cbtree.png => cb_tree.png} (100%) diff --git a/thu_dsa/chp10/cbtree.png b/thu_dsa/chp10/cb_tree.png similarity index 100% rename from thu_dsa/chp10/cbtree.png rename to thu_dsa/chp10/cb_tree.png diff --git a/thu_dsa/chp10/heap.md b/thu_dsa/chp10/heap.md index 8cf5d46..ee78896 100644 --- a/thu_dsa/chp10/heap.md +++ b/thu_dsa/chp10/heap.md @@ -142,3 +142,62 @@ entry CBHeap::delMax(){ ``` 和`上滤插入`一样,容易证明`下滤删除`策略的正确性,因为这种下滤至多只会进行`h`次,`h`是完全二叉树的树高。因此,删除最大结点的时间复杂度仍然是`O(logn)`。 + +## 批量建堆 + +很多具体的应用场景中,初始的输入都是成批给出的,此时需要考虑的一个问题是,如何高效地将这些初始的数据组织成一个完全二叉堆,也就是这里的批量建堆问题。 + +### 蛮力算法 + +实际上,这个问题可以借助上面已经实现的`insert`接口快速地解决。为了将n个初始给定的元素组织成一个完全二叉堆,只需要对这n个元素依次调用`insert`函数即可,于是形成了下面的代码: + +```cpp +template +void CBHeap::heapify(entry* 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 +void CBHeap heapify(entry* 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`函数,使得一次选择的时间消耗减少了。因为太简单,这里就不多叙述了。