文章目录
- 堆排序的概念
- 堆的分类
- 堆排序的算法思想
- 堆排序的实现
堆排序的概念
堆是一种叫做完全二叉树的数据结构,可分为大根堆、小根堆,而堆排序就是基于这种结构产生的一种排序的算法。
堆的分类
大根堆:每个节点的值都大于或者等于它的左、右孩子节点的值。
小根堆:每个节点的值都小于或者等于它的左、右孩子节点的值。
两种结构映射到数组:
大根堆:
小根堆:
堆排序的算法思想
要学习堆排序,首先要学习堆的向下调整算法,因为要用堆排序,你首先得建堆,而建堆需要执行多次堆的向下调整算法。
堆的向下调整算法(使用前提):
若想将其调整为小堆,那么根结点的左右子树必须都为小堆。
若想将其调整为大堆,那么根结点的左右子树必须都为大堆。
向下调整算法的基本思想(以建大堆为例):
1.从根结点处开始,选出左右孩子中值较大的孩子。
2.让大的孩子与其父亲进行比较。
若大的孩子比父亲还大,则该孩子与其父亲的位置进行交换。并将原来大的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。
若大的孩子比父亲小,则不需处理了,调整完成,整个树已经是大堆了。
图片示例:
堆的向下调整算法 代码示例:
//堆的向下调整算法
void AdjustDown(int* a, int n, int root)
{
int parent = root;
int child = 2 * parent + 1;//假设左孩子较大
while (child < n)
{
if (child + 1 < n&&a[child + 1] > a[child])//右孩子存在,并且比左孩子大
{
child++;//左右孩子的较大值
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else//已成堆
{
break;
}
}
}
使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = log2(N+1)(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN) 。
上面说到,使用堆的向下调整算法需要满足其根结点的左右子树均为大堆或是小堆才行,那么如何才能将一个任意树调整为堆呢?
答案很简单,我们只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可。
建堆代码示例:
//建堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(php->a, php->size, i);
}
那么建堆的时间复杂度又是多少呢?
当结点数无穷大时,完全二叉树与其层数相同的满二叉树相比较来说,它们相差的结点数可以忽略不计,所以计算时间复杂度的时候我们可以将完全二叉树看作与其层数相同的满二叉树来进行计算。
总结一下:
堆的向下调整算法的时间复杂度:T ( n ) =O(logN)。
建堆的时间复杂度:T ( n ) = O(N)。
堆排序的实现
那么堆建好后,如何进行堆排序呢?
步骤如下:
1、将堆顶数据与堆的最后一个数据交换,然后对根位置进行一次堆的向下调整,但是调整时被交换到最后的那个最大的数不参与向下调整。
2、完成步骤1后,这棵树除最后一个数之外,其余数又成一个大堆,然后又将堆顶数据与堆的最后一个数据交换,这样一来,第二大的数就被放到了倒数第二个位置上,然后该数又不参与堆的向下调整…反复执行下去,直到堆中只有一个数据时便结束。此时该序列就是一个升序。
堆排序代码示例:
//堆排序
void HeapSort(int* a, int n)
{
//排升序,建大堆
//从第一个非叶子结点开始向下调整,一直到根
int i = 0;
for (i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, n, i);
}
int end = n - 1;//记录堆的最后一个数据的下标
while (end)
{
Swap(&a[0], &a[end]);//将堆顶的数据和堆的最后一个数据交换
AdjustDown(a, end, 0);//对根进行一次向下调整
end--;//堆的最后一个数据的下标减一
}
}
时间复杂度:O(NlogN) 空间复杂度:O(1)