一、背景知识
- 堆:堆是一颗完全二叉树;堆中某个节点的值总是不大于或不小于其父节点的值。
- 用宽度优先遍历的方法,将一个堆保存在数组中,如下:
对于第 i 个节点(数组中的第 i 个元素, i 从0开始),如果它的左右子节点和父节点都存在,那么它们在数组中的位置:
Indexlchild = 2 * i + 1
Indexrchild = 2 * i + 2
Indexparent = (i - 1) / 2 - 大根堆(大顶堆):堆中任意节点的值总是小于等于父节点的值。
- 建立堆:
假如有一个数组【1,2,3,4,5】,用这个数组建立一个大顶堆:
(1)初始时,堆为空,将 1 加入到堆中,数组中剩下的元素为【2,3,4,5】:
(2)再将 2 加入到堆中,由于 2 大于它的父节点的值(1),因此把 2 和父节点交换位置,剩下【3,4,5】
(3)再把 3 加入到堆中,由于 3 比它的父节点 2 大,因此再交换位置,剩下【4,5】
(4)再把 4 加入到堆中,由于4 比它的父节点 1 大,因此交换位置;交换位置后发现 4 还是比它的父节点 3 大,因此还要交换一次位置。剩下【5】
(5)最后把 5 加入到堆中,由于5 比它的父节点 3 大,因此交换位置;交换位置后发现 3 还是比它的父节点 4 大,因此还要交换一次位置。
通过以上几步就建立了一个大顶堆,这个示例的数组可能不是特别好,因为它本就是一个有序的数组,可能会产生误导。建议大家自己写一个数组模拟一下这个过程。
- 调整堆
假如大顶堆中某个元素发生了变化,那么该如何调整呢?
(1)某个元素变大:如果一个元素变大了,那么调整的过程其实就是建立堆的过程,只要把变化的元素和它的父节点比较,如果比父节点大就交换位置,直到找到比他大的父节点就不需要交换了。
(2)元素变小:建立堆的过程其实就是一个元素“往上浮”的过程,而当一个元素变小时,就是把这个元素“往下沉”的过程,具体的做法是这样的:把这个元素和它的左右孩子节点比较,选取这个节点左右子节点中较大的和它自身比较,如果较大者比他大,那么就交换位置;一直进行这个步骤直到这个节点没有孩子节点或者它的孩子节点都比它小。
二、堆排序
前面介绍了那么多,终于进入了正题,那么什么是堆排序呢?
给定一个长度为 N 的数组,使用堆排序,基本步骤如下:
(1)先用这个数组建立一个大顶堆,我们可以肯定的是保存这个大顶堆的数组的第一个(下标为0)元素肯定是数组中的最大值,这是根据大顶堆的定义可以推导出来的。
(2)把数组的第一个元素(下标0)和最后一个元素(下标 N- 1)交换,这样最大的元素就到了数组的最后一个位置。
(3)因为大顶堆的第一个元素变小了,所以我们要调整这个堆,使他仍然是一个大顶堆,但是我们只要对第 0 ~ N-2个元素进行调整,最后一个元素已经是最大值,不要再动他。
(4)把调整后的第一个元素和倒数第二个元素交换,这样倒数第二大的元素到了倒数第二的位置;再调整堆,再交换,最后就成了一个有序的数组。
三、代码实现
public static void heapSort(int[] arr) {
if(arr == null || arr.length < 2) return;
int length = arr.length;
// 建立堆
for(int i = 0; i < length; i++) {
heapInsert(arr, i);
}
// 不断把第一个元素和最后一个元素交换,使数组有序
swap(arr, 0, --length);
while(length > 0) {
heapify(arr, 0, length);
swap(arr, 0, --length);
}
}
// 把一个元素插入到已经建好的大顶堆中
public static void heapInsert(int[] arr, int index) {
int parent = (index - 1) / 2;
while(arr[index] > arr[parent]) {
swap(arr, index, parent);
index = parent;
parent = (index - 1) / 2;
}
}
// 堆中某个值变小,调整堆
public static void heapify(int[] arr, int i, int heapSize) {
int left = 2 * i + 1;
while(left < heapSize) {
int largest = left + 1 < heapSize && arr[left+1] > arr[left] ? left + 1 : left;
largest = arr[largest] > arr[i] ? largest : i;
if(largest == i) break;
swap(arr, i, largest);
i = largest;
left = 2 * i + 1;
}
}
四、时间复杂度分析
使用堆排序主要花费的时间是在建立堆和不断调整堆这两个过程中,建立堆(heapInsert)和调整堆(heapify)的时间复杂度都为O(log2N),因为建立堆和调整堆都是把一个元素往下或者往上浮的过程,而一个有 N 个元素的堆,他应该有 log2N 这么多层,因此一个元素最多也就移动 log2N 次(大概这么多,包括层数也是大概估计,懒得算精确的了)。对于 N 个元素执行这么多次操作,因此可以得出结论,堆排序的时间复杂度为:O(N log2N)。