堆排序
堆排序就是利用堆的特点所设计的一种排序算法。(完全二叉树,每个节点的值都大于其左右子节点(或者小于))
堆排序的过程大致可以分解成两个步骤:建堆 和 排序。
建堆
我们可以将数组原地建成一个堆。原地就是不借助与别的数组,在原数组上操作建堆
有两种方法:若原数组有n个数据,可以假设数组起初只包含一个数据,就是下标为1的数据。
然后将下标从 2 到 n 的数据依次插入到堆中。这样就将包含 n 个数据的数组,组织成了堆。
这种建堆的处理过程是从前往后处理数据,这是一个从下往上堆化的建堆过程
第二种方法和第一种相反,是一个从后往前处理数据的过程。就是说每个数据插入堆中的时候都是从上往下堆化。
因为叶子节点无法从上往下堆化,所以从第一个非叶子节点开始,依次堆化。
排序
建堆结束之后,数组中的第一个元素就是堆顶,也就是最大的元素。
将它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。
排序类似于“删除堆顶元素”,下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。
堆化完成之后,我们再取堆顶的元素,放到下标是 n−1 的位置。
一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。
堆排序的执行效率
建堆
叶子节点不需要堆化,所以需要堆化的节点从倒数第二层开始。
每个节点堆化的过程中,需要比较和交换的节点个数,跟这个节点的高度 k 成正比。
将每个非叶子节点的高度求和,就是下面这个公式:
错位相减法
把公式左右都乘以 2,就得到另一个公式 S2。我们将 S2 错位对齐,并且用 S2 减去 S1,可以得到 S。
而S的有部分是等比数列,等比数列求和得到
因为 h=log2n,代入公式 S,就能得到 S=O(n),所以,建堆的时间复杂度就是 O(n)。
排序
排序过程类似于“删除堆顶元素”,所以排序过程的时间复杂度是 O(nlogn)
排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn)。
所以,堆排序整体的时间复杂度是 O(nlogn)。
堆排序的空间消耗
堆排序的不论建堆还是排序,都是在原数组上进行的,都只需要极个别临时存储空间,所以堆排序是原地排序算法。
堆排序的稳定性
堆排序不是稳定的排序算法。
因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。
堆排序的代码实现
public class Heap {
private int[] a; // 数组,从下标1开始存储数据
private int n; // 堆可以存储的最大数据个数
private int count; // 堆中已经存储的数据个数
public Heap(int capacity) {
a = new int[capacity + 1];
n = capacity;
count = 0;
}
//对于完全二叉树来说,下标从 2n+1 到 n 的节点都是叶子节点。叶子结点不需要堆化
private static void buildHeap(int[] a, int n) {
for (int i = n/2; i >= 1; --i) {
heapify(a, n, i);
}
}
private static void heapify(int[] a, int n, int i) {
while (true) {
int maxPos = i;
if (i*2 <= n && a[i] < a[i*2]) maxPos = i*2;
if (i*2+1 <= n && a[maxPos] < a[i*2+1]) maxPos = i*2+1;
if (maxPos == i) break;
swap(a, i, maxPos);
i = maxPos;
}
}
// n表示数据的个数,数组a中的数据从下标1到n的位置。
public static void sort(int[] a, int n) {
buildHeap(a, n);
int k = n;
while (k > 1) {
swap(a, 1, k);
--k;
heapify(a, k, 1);
}
}
}
以上代码都是假设堆中的数据是从数组下标为 1 的位置开始存储。
那如果从 0 开始存储,实际上处理思路是没有任何变化的。
唯一变化的就是代码实现的时候,计算子节点和父节点的下标的公式改变了。
如果节点的下标是 i,那左子节点的下标就是 2∗i+1,右子节点的下标就是 2∗i+2,父节点的下标就是 (i−1)/2。