描述:
堆排序就是利用堆这种数据结构进行排序的算法,堆排序属于选择排序
。
堆是一棵顺序
存储的完全二叉树
堆排序的时间复杂度: O(nlogn),属于不稳定排序。
大根堆 | 小根堆 |
---|---|
每个节点的值大于等于 孩子节点得堆 | 每个节点得值小于等于 孩子节点得值 |
堆的存储结构:
堆排序就是利用堆得性质堆数组进行排序,待排序元素存放在一个数组Arr[0 ……n] 中,将Arr用一颗完全二叉树来表示,数组第一个元素就是完全二叉树的根,后面依次按层从左至右为,左孩子,右孩子,任意节点Arr[ i ] 的左孩子是Arr[ 2i+1 ],右孩子是 Arr[ 2i+2 ],对这个二叉树进行调整,使各个节点满足:
Arr[ i ].key >= Arr[ 2i ].key && Arr[ i ].key >= Arr[ 2i+1 ].key
(其中 i = 1, 2, 3,……, [n/2] )
构造过程:
<参考了大佬的博客截的图,如果对堆的调整过程不理解,可以仔细看看>
设有一个无序序列 { 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 }。
1、构造初始堆
2、构造初始堆后,就需要完整的堆排序
到此,就构造了有序的堆
代码实现
首先给出完整部分的堆排序代码思路,主要分为两步,第一:建一个大堆,第二:交换根节点和最后一个叶子交换,再根据堆性质,调整堆,也就是第一步所做的工作,堆长度 -1 。
//堆排序
void HeapSort(int A[], int N)
{
int i;
//步骤一:根据A数组元素,创建大根堆
//从第 n/2 个记录开始进行建堆
for (i = N / 2; i >= 0; i--)
{
PercDown(A, i, N);
}
//步骤二:调整大根堆
for (i = N - 1; i > 0; i--)
{
//首尾交换
Swap(&A[0], &A[i]);
//将最后一个叶子节点与根节点进行交换,
//再根据堆性质进行调整
PercDown(A, 0, i);
}
}
交换根节点和最后一个叶子结点的操作。在传递参数中,其实已经将根和最后一个叶子节点下标传了进来(0和i),所以直接交换元素就可以了。
//交换两个元素的位置
//为什么传指针,就不啰嗦了
void Swap(int *num1, int *num2)
{
int tmp = *num1;
*num1 = *num2;
*num2 = tmp;
}
调整部分是堆排序的重点,也是难点。调整都是从父节点、左孩子节点、右孩子节点三者中选择最大者跟父节点进行交换(交换之后可能造成被交换的孩子节点不满足堆的性质,因此每次交换之后要重新对被交换的孩子节点进行调整)。
//给定父节点的索引,得到左子节点的索引,跟的索引为0
#define LeftChild(i) (2*(i)+1)
//元素向下调整方法
void PercDown(int A[], int i, int N)
{
//子节点的索引号
int child;
//存储当前父节点元素的临时变量
//(注:每一个节点都可以看作是其子树的根节点)
int tmp;
for (tmp = A[i]; LeftChild(i)<N; i = child)
{
child = LeftChild(i); //左子节点索引
//挑选出左、右子节点中较大者
if (child != N - 1 && A[child + 1] > A[child])
{
child++;
}
//比较当前父节点与较大子节点
if (A[i]<A[child])
{
//交换当前父节点处的元素值与较大子节点的元素值
//此处也可以调用:Swap(A[i], A[child])
tmp = A[i];
A[i] = A[child];
A[child] = tmp;
}
else
{
break;
}
}
}
时间复杂度及总结
1、堆排序最大的优点就是,在最坏的情况下,时间复杂度仍为 O(n*logn);
2、堆排序只需要存放一个记录的辅助空间,所以堆排序也称原地排序;
3、堆排序是不稳定的;
4、不适用于待排序数n较小的情况,但对n较大的文件还是很有效的;
4、堆排序可以通过树形结构保存部分比较结果,减少比较次数。