上期回顾:我们讲了希尔排序的实现方式。
这一期,我们来剖析一下堆排序的底层思路以及代码实现。
目录
堆排序的基本思想
关于堆排序,我们首先考虑的当然是建堆了。
堆,是二叉树的一种。
关于建堆,在我们学习堆的时候,一定学过两个建堆的方式:
1、向上建堆法。
就是子节点与父节点比较然后逐渐向上的一种建堆方式,最终,我们选择的最小的数或者最大的数会出现在堆顶。
例如:当我们要将下面的数据调整为堆结构的时候,会有以下调整。
2、向下建堆法
父节点与两个最大或最小的子节点比较然后逐渐向下传递的一种建堆方式最终,我们选择的最小的数或者最大的数会出现在堆底。
那么,哪种方式建堆的效率是最高的呢?
最终我们得到: 向下建堆的效率为O(N)。
利用同样的方法,我们可以算得向上建堆的效率为O(N*logN)。最终我们选择用向下建堆的方法。
接下来我们就要选数了。我们以从小到大排序为例,我们建堆时建大堆,然后每次把尾部的数和堆顶的最大的数字交换,那么我们就把最大的数字放到了它改在的地方。然后把堆顶的数字向下调整,最终堆顶的数字就是第二大的数字了,然后我们把他与倒数第二个数字交换……直到最后第二个数字排好,我们的堆排序就完成了。
向下调整
我们知道,在堆中,下标为 n 的父节点,他的两个孩子节点的下标分别为:2n+1 和 2n+2 。
所以向下调整的步骤大致分为以下几步:
1、左孩子和右孩子比较,如果右孩子比左孩子大,记录右孩子的下标。
2、父节点与记录子节点比较,如果子节点更大,则父与子交换值。
3、加上循环。
代码实现:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustDown(int* a, int n, int parent)
{
int minChild = parent * 2 + 1;
while (minChild < n)
{
// 找出大的那个孩子
if (minChild + 1 < n && a[minChild + 1] > a[minChild])
{
minChild++;
}
//把比双亲节点大的孩子换上去
if (a[minChild] > a[parent])
{
Swap(&a[minChild], &a[parent]);
parent = minChild;
minChild = parent * 2 + 1;
}
else
{
break;
}
}
}
注: n是整个堆元素个数,循环要让孩子节点的下标小于n以免越界访问。如果有右孩子,需要判断右孩子是否越界。
选数
我们首先让堆顶和最后一个元素交换,然后交换后的堆顶元素向下调整。然后第二次堆顶与倒数第二个元素交换……
代码:
void HeapSort(int* a, int n)
{
//建大堆
for (int i = (n - 1 -1 ) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
//选数
int i = 1;
while (i < n)
{
Swap(&a[0], &a[n - i]);
AdjustDown(a, n - i, 0);
++i;
}
}
注:通过 n-i ,我们可以逐渐缩小范围,最终排完第二个元素的时候停下。
总结
时间复杂度:O(N*logN) 。
在前文我们计算得到向下建堆的效率是O(N),选数的时候循环是O(logN)的。
空间复杂度:O(1)。
稳定性:不稳定。
堆是二叉树的一种,利用二叉树的调整法建成的堆只是具有相对有序的,向下调整的时候还是可能破坏原先的排序,所以是不稳定的。