前言
堆排序(heapsort),是一种时间复杂度为O(nlgn)的排序方法。用到了数据结构“堆”。本篇文章将解答什么是“堆”?如何用“堆”进行排序?
二叉堆
首先来回答什么是“堆”。
堆通常是一个可以被看做一棵完全二叉树的数组对象,它可以被看成一种近似二叉树的结构,除了最底层外,其他位置是完全充满的。
在本篇文章所讲述的堆排序里,用的是“二叉堆”,它是一种特殊的堆。
下面是一个例子:
上图是以二叉树的形式展示了二叉堆堆的内在关系,树中每个节点上面的编号代表着它在数组中的位置。
注意:为了将数组的位置与树中节点的编号对应,我们这里把数组的起始下标定为1。
从图中可以看出,每个节点下属还有一个左节点和右节点,我们称之为左孩子和右孩子。不难发现一个序号为 i 的节点的左孩子序号为 2i,右孩子序号为 2i+1;而左右孩子的父节点的序号为 i/2。
即:
//父节点
int PARENT(int i) {
return i / 2;
}
//左孩子
int LEFT(int i) {
return 2 * i;
}
//右孩子
int RIGHT(int i) {
return 2 * i + 1;
}
A.length 与 A.heap-size
对于一个数组 A 来说,其中有两个特征量:数组的长度 A.length,形成的堆的大小 A.heap-size。值得注意的是,对于数组 A 来说,要建立一个堆不需要包含 A 的所有元素,但堆中的元素一定都属于A。因此,有 A.length ≥ A.heap-size。 可以说,给定一个数组 A,A.length就已经确定且不变了,但 A.heap-size 还要根据对应位置的元素是否符合堆的特性来确定形成多大的堆。这二者的区别到排序时你会有所体会。
二叉堆的分类
二叉堆可以分为两种形式:最大堆和最小堆。
在最大堆中,最大堆的性质是除了根以外的所有节点 i 都要满足 A[ PARENT( i ) ] ≥ A[ i ]。也就是说父节点要大于等于所有的子节点。
在最小堆中,除了根以外的所有节点 i 都要满足 A[ PARENT( i ) ] ≤ A[ i ]。也就是说父节点要小于等于所有的子节点。
在本章的堆排序中,我们以最大堆为例进行介绍,最小堆的原理大同小异。
二叉堆的基本操作
-
维护堆 MAX-HEAPIFY( A, i )
MAX-HEAPIFY用于维护最大堆性质。其输入为数组 A 和 下标 i 。
在使用这个操作时,有一个重要前提:以 LEFT( i ) 和 RIGHT( i ) 为根的堆均为最大堆。也就是说,LEFT( i ) 和 RIGHT( i ) 分别是 i 的左子树的最大值以及右子树的最大值。MAX-HEAPIFY的操作就是在 A[ i ],A[ LEFT( i ) ],A[ RIGHT( i ) ] 中选出最大的作为根节点,从而维护最大堆的性质。
例如,我们对下面这个堆进行维护。想在A[ 2 ] = 4 这个位置上维护最大堆的性质,并且其左右子树均为最大堆。
通过4、8、3三个数的对比,发现8最大,那么8应该是根节点,所以我们顺其自然地把 8 和 4 互换。变成了下图:
这样操作会破坏掉左子树的最大堆状态,但只需要在这个位置重新进行维护堆的操作就好。
下面我们给出维护堆的伪代码:
MAX-HEAPIFY( A, i )
1 l = LEFT( i ) r = RIGHT( i )
2 if l ≤ A.heap-size and A[ l ] > A[ i ] largest = l
3 else largest = i
4 if r ≤ A.heap-size and A[ r ] > A[ largest ] largest = r
5 if largest ≠ i
6 swap A[ i ] and A[ largest ]
7 MAX-HEAPIFY( A, largest )
如果你能理解上面那个例子,这个代码你一定能看懂。第1-4行是找出根节点以及左右孩子的最大项,然后第5行检查是否根节点就是最大,不是的话就和最大的交换,由于交换后会破坏子树的最大堆性质,那么第7行就是继续维护子树的最大堆性质。
-
建堆 BUILD-MAX-HEAP( A )
建堆的目的就是将一个大小为 A.length 的数组转换为最大堆。这里面借助了维护堆操作 MAX-HEAPIFY( A, i ),但在这个操作是有前提的:i 节点的左右子树必须是最大堆,这就为建堆提供了自底向上的设计思路。也就是说,应该先把最底层的节点维护成最大堆,然后再维护他们的父节点,直到根节点。这个思路很清晰明了。
我们来看伪代码:
BUILD-MAX-HEAP( A )
1 A.heap-size = A.length
2 for i = ( A.length / 2 ) to 1
3 MAX-HEAPIFY( A, i )
-
堆排序算法 HEAPSORT( A )
如果你已经清楚最大堆的性质,就会知道,最大二叉堆的根节点一定是整个堆元素的最大值。那么也就是根据这种特性,要把一个数组从小到大排序,只需要每次将最大的根节点元素从堆中取出,放在数组的最末尾,然后将数组最末尾的值来当作根节点,再重新维护一次堆。就像下图一样:
进行这样的 A.length-1 次操作,我们就可以将整个数组从小到大进行排序。
伪代码如下:
HEAPSORT( A )
1 BUILD-MAX-HEAP( A )
2 for i = A.length to 2
3 swap A[ 1 ] with A[ i ]
4 A.heap-size - -
5 MAX-HEAPIFY( A, 1 )
以上就是堆排序的算法了,对于时间复杂度的具体分析有些繁琐,有兴趣的话不妨自己推导一下。
最后附上整个C/C++实现的代码:
//查找父节点
int PARENT(int i) {
return i / 2;
}
//左孩子
int LEFT(int i) {
return 2 * i;
}
//右孩子
int RIGHT(int i) {
return 2 * i + 1;
}
//维护堆(最大堆)
void Max_Heapify(int* a, const int size, int i) { //参数:数组,堆大小,父节点
int l = LEFT(i); //获取左右孩子
int r = RIGHT(i);
int largest; //创建最大值,largest负责记录三个数中最大的节点
if (l <= size && a[l] > a[i]) //比较左孩子
largest = l;
else
largest = i;
if (r <= size && a[r] > a[largest]) //比较右孩子
largest = r;
if (largest != i) { //将最大的节点作为父节点
int t = a[i];
a[i] = a[largest];
a[largest] = t;
Max_Heapify(a, size, largest); //再将被替换的数的位置维护
}
return;
}
//维护堆(最小堆)
void Min_Heapify(int* a, const int size, int i) { //参数:数组,堆大小,父节点
int l = LEFT(i); //获取左右孩子
int r = RIGHT(i);
int smallest; //创建最小值,smallest负责记录三个数中最大的节点
if (l <= size && a[l] < a[i]) //比较左孩子
smallest = l;
else
smallest = i;
if (r <= size && a[r] < a[smallest]) //比较右孩子
smallest = r;
if (smallest != i) { //将最大的节点作为父节点
int t = a[i];
a[i] = a[smallest];
a[smallest] = t;
Min_Heapify(a, size, smallest); //再将被替换的数的位置维护
}
return;
}
//建堆(最大堆)
void Build_Max_Heap(int* a, const int length, int size) { //参数:数组,数组长度,堆大小
size = length;
for (int i = length / 2; i >= 1; i--) {
Max_Heapify(a, size, i);
}
return;
}
//建堆(最小堆)
void Build_Min_Heap(int* a, const int length, int size) { //参数:数组,数组长度,堆大小
size = length;
for (int i = length / 2; i >= 1; i--) {
Min_Heapify(a, size, i);
}
return;
}
//堆排序,从小到大
void Heap_Sort_Up(int* a, int length, int& size) { //参数:数组,数组长度,堆大小
Build_Max_Heap(a, length, size); //对数组进行整体构造堆
for (int i = length; i >= 2; i--) { //从最后一项开始到第二项
int t = a[i]; a[i] = a[1]; a[1] = t; //取出根节点
size--; //堆大小-1
Max_Heapify(a, size, 1); //重新维护堆
}
return;
}
//堆排序,从大到小
void Heap_Sort_Down(int* a, int length, int& size) {
Build_Min_Heap(a, length, size); //对数组进行整体构造堆
for (int i = length; i >= 2; i--) { //从最后一项开始到第二项
int t = a[i]; a[i] = a[1]; a[1] = t; //取出根节点
size--; //堆大小-1
Min_Heapify(a, size, 1); //重新维护堆
}
return;
}
本文是个人对《算法导论》的学习摘要。如有问题,请大家在评论区批评指正!