在直接选择排序中,待排序的数据元素集合构成一个线性表结构,要从有n个数据元素的线性表中选择出一个最小的数据元素需要比较n-1次。如果能把待排序的数据元素集合构成一个完全二叉树结构,则每次选择出一个最大(或最小)的数据元素只需比较完全二叉树的高度次,即lb(n)次。这就是堆排序的基本思想。
下面给出一个完全二叉树的性质(在代码中会用到):
对于具有n个结点的完全二叉树,如果按照从上到下和从左到右的顺序对所有结点从0开始顺序编号,则对于序号为i的结点有:
(1)如果i>0,则序号为i的结点的双亲结点的序号为(i-1)/2("/"表示);如果i=0,则序号为i的结点为根结点,无双亲结点。
(2)如果2i+1<n,则序号为i的结点的左孩子结点的序号为2i+1;如果2i+1≥n,则序号为i的结点无左孩子。
(3)如果2i+2<n,则序号为i的结点的右孩子结点的序号为2i+2;如果2i+2≥n,则序号为i的结点无右孩子。
堆实际上是一棵完全二叉树,其任何一非叶节点满足性质: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]称为小顶堆。即大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。
堆排序的基本思想是:循环执行如下过程直到数组为空:
(1)把堆顶元素data[0](最大元素)和当前最大堆的最后一个元素交换;
(2)最大堆的元素个数减1;
(3)由于第(1)步后根结点不再满足最大堆的定义,所以调整根结点使之满足最大堆的定义。重复第(1)步,直至数组元素为1。
下面是初始化堆的示意图:
示例代码如下:
//交换两个元素
void swap(int *numx, int *numy)
{
int temp;
temp = *numx;
*numx = *numy;
*numy = temp;
}
//创建大顶堆
//current是给定结点的编号
void createHeap(int data[], int size, int current)
{
int lChild = 2 * current + 1;
int rChild = 2 * current + 2;
int max = current;
//第一个非叶结点data[(size-1)/2],如果current是叶节点就不用进行调整
if (current <= (size - 1) / 2)
{
if (lChild < size && data[lChild] > data[max])
{
max = lChild;
}
if (rChild < size && data[rChild] > data[max])
{
max = rChild;
}
if (max != current)
{
//交换左右结点中最大的和其父结点
swap(&data[current], &data[max]);
createHeap(data, size, max);
}
}
}
//初始化堆
void initHeap(int data[], int size)
{
for (int i = (size - 1) / 2; i >= 0; i--)
{
createHeap(data, size, i);
}
}
//堆排序
void heapSort(int data[], int size)
{
//初始化创建最大堆
initHeap(data, size);
//当前最大堆个数每次递减1
for (int i = size - 1; i > 0; i--)
{
//把堆顶data[0]元素和当前最大堆的最后一个元素交换
swap(&data[0], &data[i]);
//调整根结点满足最大堆
createHeap(data, i, 0);
}
}