堆排序( Heapsort )是指利用“堆”这种数据结构所设计的一种排序算法。
堆是一种数据结构,是一个数组。它可以被看成一个近似的完全二叉树,树上的每一个结点对应数组中的一个元素。除了最底层外,即叶子结点,该树是完全充满的,而且是从左到右填充。
最大堆的每个结点都要满足堆的性质,此外还有其他的约束:堆中的最大元素存放在根结点中,并且在任一子树中,该子树所包含的所有结点的值都不大于该子树根结点的值。
堆排序就是利用最大堆的性质来排序。大致步骤如下:
①建最大堆
②将根结点(最大元素)与最后一个结点的元素互换,并将剩下的结点维护最大堆的性质。
③重复步骤②,直到将每个元素排完序。
下面就讲解几个重要过程:
维护最大堆的性质:MaxHeapify
传入参数:待排序数组、需要维护的下标、当前堆的大小。返回值:空
每次在传入参数的下标、该下标的左、右孩子结点三者中选出最大的元素。如果最大元素是该下标,则满足最大堆性质,结束。否则,需要将父亲与最大的孩子交换位置,使得当前下标及其左右孩子满足最大堆的性质。之后递归调用,使得整个子树都满足最大堆的性质。
特别需要注意,数组的大小和堆的大小是不一样的概念。堆的大小表示有多少个堆元素存储在数组中。在进行堆排序的过程中,每次需要把根结点元素与最后一个元素交换,这样堆的大小每次就会减一。而数组大小是不会变的。也就是:
0 ≤ 堆的大小 ≤ 数组的大小
建最大堆:BuildMaxHeap
传入参数:待排序数组、当前堆的大小。返回值:空
只需保证所有的非叶子结点满足最大堆的性质即可。非叶子结点的下标为从0到堆大小 / 2(向下取整)。
堆排序:HeapSort
传入参数:待排序数组、当前堆的大小。返回值:空
首先建堆,然后每次去除根结点元素(最大元素),然后维护最大堆的性质。如此循环,直到所有元素排序完毕。
实现代码如下:
#include <iostream>
using namespace std;
typedef int ElemType;
int getLeftChild( int i ) //返回左孩子下标。注:数组从位置0开始
{
return 2 * i + 1;
}
int getRightChild( int i ) //返回右孩子下标。注:数组从位置0开始
{
return 2 * i + 2;
}
void swap( ElemType *a, ElemType *b ) //交换两个元素
{
ElemType temp = *a;
*a = *b;
*b = temp;
}
/* 维护最大堆的性质。
* 参数:待排序数组、需要维护的下标、当前堆的大小
* 返回值:空
*/
void MaxHeapify( ElemType *A, int i, int num )
{
int left = getLeftChild( i );
int right = getRightChild( i );
int largest; // 存放父结点、左右子节点三者的最大值的下标位置
if( left <= num && A[left] > A[i] )
largest = left;
else
largest = i;
if( right <= num && A[right] > A[largest] )
largest = right;
if( largest != i )
{
swap( &A[i], &A[largest] );
MaxHeapify( A, largest, num );
}
}
/* 建最大堆
* 传入参数:待排序数组、当前堆的大小
* 返回值:空
*/
void buildMaxHeap( ElemType *A, int num )
{
for( int i=num/2; i>=0; i-- )
MaxHeapify( A, i, num );
}
/* 堆排序
* 待排序数组、当前堆的大小
* 返回值:空
*/
void heapSort( ElemType *A, int num )
{
buildMaxHeap( A, num-1 );
int HeapSize = num - 1; //当前堆的大小
for( int i=num-1; i>0; i-- )
{
swap( &A[0], &A[i] );
HeapSize--;
MaxHeapify( A, 0, HeapSize );
}
}
int main()
{
ElemType A[10] = {1,3,6,2,4,8,0,9,7,5};
heapSort( A, 10 );
for( int j=0; j<10; j++ )
cout << A[j] << " ";
cout << endl;
return 0;
}
时空复杂度:
由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面建立堆时N / 2次向下调整,每次调整时间复杂度也为O(logN)。二次操作时间相加还是O(N * logN)。故堆排序的时间复杂度为O(N * logN)。
由于建初始堆所需的比较次数较多,所以堆排序不适宜于记录数较少的文件。
堆排序是就地排序,辅助空间为O(1), 它是不稳定的排序方法。