(1) 最大堆和最小堆(max_heap and min_heap)
注意:(二叉)堆是一个数组,数组的下标随机存取是从0开始的,《算法导论》上计算左右孩子节点的坐标是从1开始考虑的,与此略有不同!!
堆实际上是一棵完全二叉树,其任何一非叶节点满足性质:
Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]或者Key[i]>=Key[2i+1]&&key>=key[2i+2]
即任何一非叶节点的关键字不大于或者不小于其左右孩子节点的关键字。
两个孩子节点的关键字大小不考虑!!
堆分为大顶堆和小顶堆,满足Key[i]>=Key[2i+1]&&key>=key[2i+2]称为大顶堆,满足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]称为小顶堆。由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。
(2) 堆排序的思想(heap_sort)
利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。
其基本思想为(大顶堆):
1)将初始待排序关键字序列(R1,R2....Rn)构建成大顶堆,此堆为初始的无序区;
2)将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,......Rn-1)和新的有序区(Rn),且满足R[1,2...n-1]<=R[n]; 无序堆的大小减一。
3)由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,......Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2....Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
操作过程如下:
1)初始化堆:将R[1..n]构造为堆;
2)将当前无序区的堆顶元素R[1]同该区间的最后一个记录交换,然后将新的无序区调整为新的堆。
因此对于堆排序,最重要的两个操作就是构造初始堆和调整堆,其实构造初始堆事实上也是调整堆的过程,只不过构造初始堆是对所有的非叶节点都进行调整。
(3)最大堆调整(max_heapify)
检查指定根节点的关键字大小是否满足定义,如果不满足,就要和孩子节点交换。问题是,如果和孩子几点交换了关键字,孩子节点是否也要调整呢,所以这里要考虑递归调整的情况。
void HeapSort::MaxHeapify(int nIndex)
46 {
47 int nLeft = LeftChild(nIndex);
48 int nRight = RightChild(nIndex);
49
50 int nLargest = nIndex;
51
52 if( (nLeft < m_nHeapSize) && (m_pA[nLeft] > m_pA[nIndex]) )
53 nLargest = nLeft;
54
55 if( (nRight < m_nHeapSize) && (m_pA[nRight] > m_pA[nLargest]) )
56 nLargest = nRight;
57
58 if ( nLargest != nIndex )
59 {
60 swap<int>(m_pA[nIndex], m_pA[nLargest]);
61 MaxHeapify(nLargest);
62 }
63 }
(4)建堆(build_heap)
这里考虑将无序数组A[N]转换为最大堆。
首先搞明白几点:
1、最大堆被看做近似的完全二叉树,二叉树的树高为log(N)
2、二叉树节点的高度h是指从该节点到对应叶子节点最远距离
3、对于有n个元素的堆(二叉树),高为h的节点最多有n/(2^(h+1))个
4、对于有n个元素的堆(二叉树),叶子结点最多有 n/2 个。所以,非叶子结点为A[(n-1)/2] ~A[0],也是根结点。
所以,建堆的思想是:从叶子结点到根结点 的方向,每次增加一个根结点,并调整调整该根结点。直到A[0]!!
void HeapSort::BuildMaxHeap()
{
if( m_pA == NULL )
return;
for( int i = (m_nHeapSize - 1)/2; i >= 0; i-- )
{
MaxHeapify(i);
}
}
(5)堆排序算法
#include <iostream>
#include <algorithm>
using namespace::std;
//对排序实现类的定义
class HeapSort
{
public:
HeapSort(int *pArray = NULL, int nArraySize = 0);//constructor:pArray points to a array, nArraySize stands for the size
~HeapSort();
public:
int *m_pA;
int m_nHeapSize;
public:
void Sort();
private:
int LeftChild(int node);
int RightChild(int node);
void MaxHeapify(int nIndex);//justify the heap
void BuildMaxHeap(); //build a heap
};
//对排序类成员函数的实现
HeapSort::HeapSort( int *pArray, int nArraySize )
{
m_pA = pArray;
m_nHeapSize = nArraySize;
}
HeapSort::~HeapSort()
{
}
int HeapSort::LeftChild(int node)
{
return 2*node + 1;// the array starts from 0
}
int HeapSort::RightChild(int node)
{
return 2*node + 2;
}
void HeapSort::MaxHeapify(int nIndex)
{
int nLeft = LeftChild(nIndex);
int nRight = RightChild(nIndex);
int nLargest = nIndex;
if( (nLeft < m_nHeapSize) && (m_pA[nLeft] > m_pA[nIndex]) )
nLargest = nLeft;
if( (nRight < m_nHeapSize) && (m_pA[nRight] > m_pA[nLargest]) )
nLargest = nRight;
if ( nLargest != nIndex )
{
swap<int>(m_pA[nIndex], m_pA[nLargest]);
MaxHeapify(nLargest);
}
}
void HeapSort::BuildMaxHeap()
{
if( m_pA == NULL )
return;
for( int i = (m_nHeapSize - 1)/2; i >= 0; i-- )
{
MaxHeapify(i);
}
}
void HeapSort::Sort()
{
if( m_pA == NULL )
return;
if( m_nHeapSize == 0 )
return;
BuildMaxHeap();
for( int i = m_nHeapSize - 1; i > 0; i-- )
{
swap<int>(m_pA[i], m_pA[0]);
m_nHeapSize -= 1;
MaxHeapify(0);
}
}
测试main函数:
int main()
{
//int A[14] = { 27,17,3,16,13,10,1,5,7,12,4,8,9,0 };
int A[10] = {4, 1, 3, 2, 16, 9, 10, 14, 8, 7};
cout<<"heapsort before:";
for(int i=0;i<10;++i)
cout<<A[i]<<" ";
cout<<endl;
HeapSort mySort(A,10);
mySort.Sort();
cout<<"heapsort after:";
for( int i = 0; i < 10; i++ )
cout << A[i] << " ";
cout << endl;
}
结果:
(6)复杂度分析
对于n个关键字序列,最坏情况下每个节点需比较log2(n)次,因此其最坏情况下时间复杂度为nlogn。堆排序为不稳定排序,不适合记录较少的排序。