堆的概念
堆的结构可以分为大根堆和小根堆,是一个完全二叉树,而堆排序是根据堆的这种数据结构设计的一种排序,下面先来看看什么是大根堆和小根堆
大根堆和小根堆
性质:每个结点的值都大于其左孩子和右孩子结点的值,称之为大根堆;每个结点的值都小于其左孩子和右孩子结点的值,称之为小根堆。下图为大根图的示意图:
将完全二叉树的大根堆映射为一维数组(首位置0不用,置空),如下图所示:
还有一个基本概念:查找数组中某个数的父结点和左右孩子结点,比如已知索引为i的数,那么
- 父结点: ( i n t ) i / 2 (int)i/2 (int)i/2
- 左孩子: i ∗ 2 i*2 i∗2
- 右孩子: i ∗ 2 + 1 i*2+1 i∗2+1
堆排序的基本步骤
算法思想
- 构造堆:将待排序列生成一个大根堆。序列的最大元素也即大根堆的堆顶(根结点)
- 交换堆顶堆尾:将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
- 再构造堆:将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
2-1 构造堆
构造堆的算法有很多,本文采用的构造堆算法是基于《算法4》的sink(下沉)算法:
sink算法是从最后一个非叶子结点开始,依次将非叶子结点和其左右孩子的元素值相比较。如果父结点的元素是比两个孩子结点大,保持元素位置不变;如果父结点的元素值小于孩子结点的值,将父结点和最大的孩子结点元素进行位置交换。从最后一个非叶子结点开始,直到根结点结束。
算法过程:
算法代码:
void ArrSwap(int* arr, int pos1, int pos2)
{
int temp = arr[pos1];
arr[pos1] = arr[pos2];
arr[pos2] = temp;
}
void sink(int* arr, int length, int loc)
{
while (2 * loc <= length)
{
int lchild = 2 * loc;
if ((lchild + 1) < length && arr[lchild] < arr[lchild + 1])
++lchild;
if (arr[loc] > arr[lchild]) break;
//swap
ArrSwap(arr, loc, lchild);
loc = lchild;
}
}
void CreateBHT(int* arr, int length)
{
int first = length / 2;
for (int i = first; i > 0; --i)
{
sink(arr, length, i);
}
}
2-2 交换堆顶堆尾+再构造堆
上一步我们将一个无序序列构造成了一个大根堆,按照堆排序的算法思想,还有两个步骤需要完成:
- 交换堆顶和堆尾元素
一维数组交换堆顶堆尾元素,代码非常简单。大家可以想象一下,我们把堆尾的元素放在了二叉树的根结点位置处,此时该完全二叉树已经不满足大根堆的性质。因此,我们还需要重建该大根堆。 - 再构建堆
交换首尾元素后的二叉树,重构建堆的思想非常简单。通过将位于堆顶的小元素不断sink(下沉),即可重新得到一个大根堆:
算法代码:
void reCreateBHT(int* arr, int length)
{
for (int i = length; i > 1; --i)
{
//swap
ArrSwap(arr, i, 1);
sink(arr, i - 1, 1);
}
}
堆排序的完整代码:
void HeapSort(int* arr, int length)
{
//step 1: create BinaryHeap
CreateBHT(arr, length);
//step 2: sort
reCreateBHT(arr, length);
}
总结
- 算法时间复杂度: O ( n ∗ l o g n ) O(n*logn) O(n∗logn)
- 算法空间复杂度: O ( 1 ) O(1) O(1)
- 算法是否稳定:不稳定
算法稳定指的是序列通过算法排序后,比较值相同(key)的两个元素相对顺序不会发生改变 - 算法适用范围:由于堆排序运算快,而且占用缓存资源极少,一般用于单片机等嵌入式芯片内部的算法。正所谓优点也是缺点,堆排序由于不能有效的利用计算机的缓存,在当代计算机缓存资源及其丰富的今天已经逐渐被淘汰。