优先队列(堆)可以使用于时间复杂度为的排序,基于该想法的排序被称为堆排序。
堆排序的一种简单实现方法是:对于要排序的数组A,首先通过线性时间将数组构建成一个堆,然后使用另一个数组B,每次deletemin后的结果都放入数组B中,当A变为空时,数组B就是排序后的结果,这个算法需要额外的空间来存放数组B,当数据量过大,并且内存较小时,这个算法可能会出现问题。
另一种方法不需要使用额外的空间,这种方法利用每次deletemin之后,堆中的元素个数-1这个事实,将删除的元素放在堆之后,当堆为空时,排序也就结束了。这个方法只需要在数组A中进行操作,且由于删除的元素会放在堆后,所以大根堆的排序结果为正序,小根堆排序结果为逆序。
对于如下数组进行堆排序:
63 | 46 | 38 | 20 | 40 | 43 | 5 | 30 | 78 | 26 |
首先建立堆:在优先队列中,元素下标从1开始,而在堆排序中,元素下标从0开始。对于N个元素的排序,首先找到下标为N/2的元素mid(由二叉堆的结构特性可知,mid后的元素一定没有孩子节点,所以它们已经满足堆序性),mid(包括mid)之前的元素都可能有孩子节点,那么借鉴于deletemin,将该位置的节点值保存在临时变量中,并且将该位置视为空穴,则通过下沉操作,可以使以该节点为根节点的子堆满足堆序性,当完成下标为0元素的下沉后,整个数组就变成了一个二叉堆。对于上面的数组,mid=43没有孩子节点,所以下沉从40真正开始。以 i 表示下标
下沉:孩子节点
,有
,满足堆序性。
63 | 46 | 38 | 20 | 40 | 43 | 5 | 30 | 78 | 26 |
下沉:有两个孩子节点,找到其中的大者与父亲节点交换位置
63 | 46 | 38 | 20 | 40 | 43 | 5 | 30 | 78 | 26 |
63 | 46 | 38 | 78 | 40 | 43 | 5 | 30 | 20 | 26 |
下沉:
63 | 46 | 38 | 78 | 40 | 43 | 5 | 30 | 20 | 26 |
63 | 46 | 43 | 78 | 40 | 38 | 5 | 30 | 20 | 26 |
下沉:
63 | 46 | 43 | 78 | 40 | 38 | 5 | 30 | 20 | 26 |
63 | 78 | 43 | 46 | 40 | 38 | 5 | 30 | 20 | 26 |
而交换后的位置还有孩子节点,所以继续交换
63 | 78 | 43 | 46 | 40 | 38 | 5 | 30 | 20 | 26 |
下沉:
63 | 78 | 43 | 46 | 40 | 38 | 5 | 30 | 20 | 26 |
78 | 63 | 43 | 46 | 40 | 38 | 5 | 30 | 20 | 26 |
继续下沉
78 | 63 | 43 | 46 | 40 | 38 | 5 | 30 | 20 | 26 |
到此就建立好了一个二叉堆。由于下沉操作是从底层向根进行的,所以当上层节点进行下沉时,下层的节点实际上已经满足堆序性,所以大部分下沉操作可能只循环一次,所以构建堆是花费线性时间的。
下面就要进行排序工作:
首先将删除(实际上是与堆之后的一个元素交换),那么堆的大小就-1,这时候堆最后一个位置就不再属于堆,刚好可以用来存放删掉的元素(最大元素)。而原来堆末尾的元素仍然属于堆(将0处元素与堆之后的一个元素交换),但交换后不一定满足堆序性,所以对该元素进行一次下沉(下沉操作与建立堆时的下沉相同,就不再详细演示)。
78 | 63 | 43 | 46 | 40 | 38 | 5 | 30 | 20 | 26 |
26 | 63 | 43 | 46 | 40 | 38 | 5 | 30 | 20 | 78 |
63 | 46 | 43 | 30 | 40 | 38 | 5 | 26 | 20 | 78 |
现在78已经位于正确的位置,下面再次进行deletemin,这时候堆之后的第一个位置就是20所在的位置:
63 | 46 | 43 | 30 | 40 | 38 | 5 | 26 | 20 | 78 |
20 | 46 | 43 | 30 | 40 | 38 | 5 | 26 | 63 | 78 |
46 | 40 | 43 | 30 | 20 | 38 | 5 | 26 | 63 | 78 |
之后的操作都是相似的,当堆的大小为2时,就是最后一趟排序,因为在这次排序中,就决定了和
的大小,整个数组的排序也就完成了。
堆排序的实现:
//堆排序
void down(int* arr, int i, int size) {//下沉操作
int tmp = arr[i];//首先将i位置的元素存放在临时变量中,然后将i位置视为空穴
int j = 0;
int child = 0;
for (j = i; j * 2 + 1 <= size - 1; j = child) {
child = j * 2 + 1;
if (child < size - 1 && arr[child] < arr[child + 1])//找到j位置的孩子节点中的大者
child++;
if (arr[child] > tmp) {//如果这个大者要大于i位置本来的元素(tmp),那么tmp就要下沉到该孩子节点的位置
arr[j] = arr[child];
}
else {//如果tmp比两个(或一个)孩子节点都大,说明此时的j就是要插入的位置,就退出循环
break;
}
}
arr[j] = tmp;//将tmp放在正确的位置
}
void CreatHeap(int* arr, int size) {//构建堆,即size/2位置之前的所有元素进行下沉操作
for (int i = size / 2; i >= 0; i--) {
down(arr, i, size);
}
}
void swap(int* a, int* b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
void heap_sort(int* arr, int size) {
CreatHeap(arr,size);//建立堆
for (int i = size - 1; i > 0; i--) {
swap(&arr[0], &arr[i]);//交换值
down(arr, 0, i);
}
}
堆排序的时间复杂度为,但在实践中,堆排序却是慢于使用Sedgewick增量的希尔排序(时间复杂度为
)。