排序算法之堆排序
堆的结构可以分为大根堆和小根堆,是一个完全二叉树,而堆排序是根据堆的这种数据结构设计的一种排序。
大根堆:每个结点的值都大于其左孩子和右孩子结点的值。所以对任一棵子树:根节点的值都是最大的。
小根堆:每个结点的值都小于其左孩子和右孩子结点的值。所以对任一棵子树:根节点的值都是最小的。
堆可以采用数组存储,结点的标号从 $0$ 开始,则对任一个结点 $i$,其左孩子的结点标号为 $2i+1$,右孩子的结点标号为 $2i+2$。
下面以大根堆为例,介绍堆排序的基本步骤。
1)首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端。
2)将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为 $n-1$。
3)将剩余的 $n-1$ 个数再构造成大根堆,再将顶端数与 $n-1$ 位置的数交换,如此反复执行,便能得到有序数组(一开始就是数组存储的)。
可见这个算法最重要的操作就是:将树调整为大顶堆。
对某个子树进行堆化(Heapify)的前提是:除自身外,该子树的任一棵子树都满足大顶堆结构。
因为 BuildHeap 的过程是自底向上,所以将一棵乱序的树调整为大顶堆的过程可以调用 Heapify,换句话说:将一棵树调整
为大顶堆之前,其任一棵子树就都已经被调整为大顶堆了。
/* * n: 树的结点总数 * i: 对以 i 为根节点的子树做堆化操作 * 之所以可以使用递归,必须有一个前提条件: 树 i 的任一棵子树都是大顶堆。 这样至上而下的调整时只需考虑下面的堆结构是否被破坏,而不用担心上面的结构是否被破坏。 */ void Heapify(int tree[], int n, int i) { if (i >= n) return; int c1 = (2 * i) + 1; int c2 = (2 * i) + 2; int max = i; if (c1 < n && tree[c1] > tree[max]) { max = c1; } if (c2 < n && tree[c2] > tree[max]) { max = c2; } // 孩子结点的值比父节点的大,需要交换 if(max != i) { swap(tree[max], tree[i]); // 被交换后的子节点,其堆结构可能被破坏,继续向下调整 Heapify(tree, n, max); } } void BuildHeap(int tree[], int n) { int last_node = n - 1; int parent = (last_node - 1) / 2; // 因为是从下到上调整的,所以满足 Heapify 的那个前提 for(int i = parent; i >= 0; --i) { Heapify(tree, n, i); } } void HeapSort(int tree[], int n) { // 先调整成一个堆 BuildHeap(tree, n); for(int i = n - 1; i >= 0; --i) { swap(tree[0], tree[i]); Heapify(tree, n - 1, 0); } }