目录
什么是堆?
堆是一类特殊的数据结构,堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一棵完全二叉树。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。堆排序是指利用堆这种数据结构所设计的一种排序算法。
根据完全二叉树的性质,如果对一颗有n个节点的完全二叉树(其深度为 log2n(向下取整)+1)的节点按层序编号,对于任意节点i有:
如果i=1,则节点i是二叉树的根节点,无双亲;如果i>1,则其双亲节点是i/2(向下取整)
如果2i>n,则节点i无左孩子;否则其左孩子就是2i
如果2i+1>n,则节点i无有右孩子;否则其右孩子就是2i+1
根据完全二叉树的性质,完全二叉树可以由数组存储数据。
堆排序思路:以小根堆为例
- 先初始化堆,构建一颗完全二叉树(节点为n)
- 根据堆的性质,父节点要大于或等于左孩子以及右孩子的节点,从倒数第二层第一个节点(即倒数第一个非叶子节点)开始,比较左孩子以及右孩子的值,假设左孩子的值大于右孩子的值,那就以左孩子的值以父节点的值比较,假设左孩子的值大于父节点的值,则交换父孩子以及左孩子的值,假设右孩子值大于左孩子的,则以右孩子的值与父节点比较,依次进行对比,只到遍历到根节点为止。
- 此时,根节点为整棵树最大的值,然后将根节点的值与最后一个叶子节点(即n节点)的值进行交换,然后重新2,然后将根节点的值与(n-1)节点的值进行交换,依次类推,直到根节点。
以数据3,2,5,7,9,4为例进行堆排序:
1.构建完全二叉树
2.以倒数第一个非叶子节点开始比较,倒数第一个非叶子节点为2,它的左孩子的值为7,右孩子的值为9,9大于7,则用9以2进行比较,2小于9,则交换父节点以及它右孩子的值。然后倒数第二个非叶子节点为5,它只有左孩子,左孩子的值为4,小于父节点,不交换。依次从树的低往上进行比较。
3.根节点为3,它的左孩子为9,右孩子为5,左孩子大于右孩子的值,则左孩子以父节点的值进行比较,左孩子大,交换连个节点的元素。继续遍历,只至二叉树具有堆的性质。
4.此时根节点为树最大的节点,将根节点值与树最后一个元素进行交换。
5.重复进行2,3,4,只至排序完成
代码实现
public static void dump(int arr[]) {
// 初始化堆
for (int i = arr.length / 2 - 1; i >= 0; i--) {
// 从第一个非叶子节点开始
adjust(arr, i, arr.length);
}
// 将堆排序
for (int i = arr.length - 1; i > 0; i--) {
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
// 筛选 arr[0]结点,得到i-1个结点的堆
adjust(arr, 0, i);
}
}
public static void adjust(int arr[], int parent, int size) {
// 获取当前节点值
int temp = arr[parent];
// 获取左孩子
int child = parent * 2 + 1;
while (child < size) {
// 判断是否有右孩子,右孩子的值是不是大于左孩子的值
if (child + 1 < size && arr[child] < arr[child + 1]) {
child++;
}
// 父节点大于他的孩子节点,不交换
if (temp >= arr[child]) {
break;
}
// 进行交换,比较新的孩子节点是否大于子孙节点
arr[parent] = arr[child];
parent = child;
child = 2 * child + 1;
}
arr[parent] = temp;
}
堆排序的优缺点
- 堆排序在时间方面为基于比较的排序算法效率的峰值(时间复杂度为O(nlogn))
- 空间方面,只需要O(1)的辅助空间了,不用辅助数组
- 堆排序效率相对稳定,不像快排在最坏情况下时间复杂度会变成O(n^2)),所以无论待排序序列是否有序,堆排序的效率都是O(nlogn)不变(注意这里的稳定特指平均时间复杂度=最坏时间复杂度,不是那个“稳定”,因为堆排序本身是不稳定的)
- 缺点在于堆维护的问题,每次数据更新,都要将堆重新维护。