定义
堆的定义如下:n个元素的序列
{k1,k2,…,kn}
{
k
1
,
k
2
,
…
,
k
n
}
当且仅当满足以下关系时,称之为堆。
若将和此序列对应的一维数组看成是一个完全二叉树,则堆的含义表明,完全二叉树中所有非终端结点的值无论不大于(或不小于)其左、右孩子结点的值。由此,若序列
{k1,k2,…,kn}
{
k
1
,
k
2
,
…
,
k
n
}
是堆,则堆顶元素(或完全二叉树的根)必为序列中n个元素的最小值(或最大值)。
若在输出堆顶的最小值之后,使得剩余n-1个元素的序列又重建成一个堆,则得到n个元素中的次小值。如此反复执行,便能得到一个有序序列,这个过程称之为堆排序。
实现堆排序的步骤
自堆顶至叶子的调整过程称为“筛选”。
从一个无序序列建堆的过程就是一个反复“筛选”的过程,若将此序列看成是一个完全二叉树,则最后一个非终端结点是第
⌊n2⌋
⌊
n
2
⌋
个元素,由此“筛选”只需从第
⌊n2⌋
⌊
n
2
⌋
个元素开始。
1. 由一个无序序列建成一个堆
2. 输出堆顶元素,调整剩下元素成为一个新的堆
C++实现
/*
iArr数组中,除iArr[iStart]以外均满足堆的定义
该函数调整iArr[iStart]的关键字,使iArr成为一个大顶堆(对其中关键字而言)
*/
void HeapAdjust(int iArr[], int iStart, int iEnd)
{
int rc = iArr[iStart]; // 先将需要调整的值保存
for (int lChild = 2 * iStart + 1; lChild <= iEnd; lChild = lChild*2+1) //沿key较大的孩子结点向下筛选
{
// 比较左、右孩子的大小,lChild为key较大的记录的下标
if (lChild < iEnd && iArr[lChild] < iArr[lChild + 1])
{
lChild++;
}
if (rc > iArr[lChild])
{
break; // 满足大顶堆的条件,则跳出循环
}
iArr[iStart] = iArr[lChild]; // 将孩子结点中key较大的记录直接覆盖父结点
iStart = lChild;
}
iArr[iStart] = rc; // 将需要调整的值放入最终的位置
}
void HeapSort(int iArr[], int iLength)
{
// 把iArr建成大顶堆
for (int i = iLength / 2 - 1; i >= 0; i--)
{
HeapAdjust(iArr, i, iLength-1);
}
int iTemp = 0;
for (int i = iLength-1; i > 0; i--)
{
// 交换堆顶元素iArr[0]和已排好序的前一位元素iArr[i]
iTemp = iArr[0];
iArr[0] = iArr[i];
iArr[i] = iTemp;
// 将iArr[0,i-1]重新调整为大顶堆
HeapAdjust(iArr, 0, i - 1);
}
}
总结
堆排序方法对记录数较少的文件并不提倡,但对n较大的文件还是很有效的。因为其运行时间主要耗费在建初始堆和调整建新堆时进行的反复“筛选”上。对深度为k的堆,筛选算法中进行的关键字比较次数至多为2(k-1)次,则在含n个元素、深度为h的堆时,总共进行的关键字比较次数不超过4n。又n个结点的完全二叉树的深度为
⌊log2n⌋+1
⌊
log
2
n
⌋
+
1
,则调整建新堆时调用HeapAdjust过程n-1次,总共进行的比较次数不超过
时间复杂度
由此,堆排序最坏的情况下,其时间复杂度也为O(nlogn)。相对于快速排序来说,这是堆排序的最大优点。
空间复杂度
堆排序仅需一个记录大小供交换用的辅助存储空间。
最坏时间复杂度 | 最优时间复杂度 | 平均时间复杂度 | 空间复杂度 |
---|---|---|---|
O(nlogn) O ( n l o g n ) | O(nlogn) O ( n l o g n ) | O(nlogn) O ( n l o g n ) | O(1) O ( 1 ) |