堆排序是利用堆(一种数据结构)的性质进行排序的一种排序算法。在了解堆排序之前先要了解堆的定义及性质。
堆
堆是一种完全二叉树,分为大根堆和小根堆两种。大根堆的性质:每个父节点的值都不小于它的左右孩子节点的值。小根堆的性质:每个父节点的值都不大于它的左右孩子节点的值。
堆通常可以被看做一棵完全二叉树的数组对象。
很显然,当通过把堆映射到数组时,父节点与左右子节点在数组中的下标位置有着如下关系:
左子节点下标等于父节点下标*2+1,右子节点下标等于父节点下标*2+2。
堆排序思想
有了前面的知识储备我们再来看堆排序是如何进行的。首先将待排序序列构建成一个堆,堆的性质决定了堆顶元素总是最大值或最小值,之后我们将堆顶元素与堆底最后一个元素进行交换,此时堆的最大值或最小值被放到了数组末尾,然后忽略最后一个元素再进行调整成一个新堆。完成这一趟操作后堆的极值被放到了数组末尾,次极值被调整到了堆顶。重复以上操作直到所有数字都与堆顶交换过。这里值得注意的是如果想要排升序就要建立大堆,如果是降序要建立小堆,因为我们不期望交换一次数值就要重新建立一个堆。
建立堆
现在我们有一个数组arr[6]={56,26,25,10,15,70}要将它排升序,我们该如何先将该数组建成一个大堆?这里我们就要用到分治的思想,要将整个完全二叉树建立成一个堆,我们可以先从最后一个父节点开始,逐个向下调整直到堆顶的父节点也被调整。
在
在这个例子中我们先从第一个父节点25开始向下调整,如果有比自己大的子节点就与与大的子节点交换,然后又调整26,56,这两个父节点。当没有可调整的父节点时堆建立完毕。
C语言实现大堆建立
void AdjustDown(int* a, int size, int parent) {
int child = parent * 2 + 1;
while (child<size) {
//选择较大的子节点
if (child + 1 < size && a[child + 1] > a[child]) {
child++;
}
if (a[parent] < a[child]) {
swap(&a[parent], &a[child]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
//size为数组元素个数
void HeapBuild(int* a, int size) {
//利用向下调整建立堆,size - 1 - 1为最后一个父节点的下标位置
for (int i = (size - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(a, size, i);
}
}
C语言实现堆排序
void swap(int* x, int* y) {
int tmp = *x;
*x = *y;
*y = tmp;
}
void HeapSort(int* a, int size) {
//建立大堆
for (int i = (size - 1 - 1) / 2; i >= 0; i--) {
AdjustDown(a, size, i);
}
int end = size-1 ;
while (end > 0) {
//交换堆顶与堆底元素
swap(&a[0], &a[end]);
//从堆顶向下调整
AdjustDown(a, end, 0);
end--;
}
}
算法分析
我们来算一下堆排序的时间复杂度,先从建立堆开始。
则需要移动节点总的移动步数为n为元素个数,h为二叉树的深度:
T(n)=2^0*(h-1)+2^1*(h-2)+...+2^(h-3)*2+2^(h-2)*1
我们利用错位相减法算出T(n)=2^h-1-h,又因为h=log2(n+1),能算出T(n)=n-log2(n+1),约等于n。则算出建立堆的时间复杂度为T(n)。接下来我们再来计算排序过程中的时间复杂度:
从堆底开始每个数据都要到堆顶然后从堆顶向下调整,最后一层需要调整2^(h-1)*(h-1)次,带入公式得到2/n*log2(n),因为最后几层占据树的绝大部分数据,所以排序过程中的时间复杂度为n*log2(n)。
总结:堆排序中堆的建立时间复杂度为T(n),排序过程中的时间复杂度为n*log2(n),总的时间复杂度为n*log2(n)。