堆排序也是时间复杂度为O(N*logN)的排序算法,排序包括堆化和排序两个过程。前一篇博客我们分析了堆的两个重要操作,也分析了Java PriorityQueue的源码。新增元素时会进行从下往上的堆化过程,而移除堆顶元素时将叶子节点的最后一个元素顶替堆顶元素,然后执行从上下到的堆化操作。而堆排序中的建堆操作就是将元素挨个添加到堆中,即从上到下的堆化操作。前面分析了同样的数据,满足堆的条件可以有多种情况,可以认为排好序后的是堆的唯一一种情况。如下图(中间的情况就是排序好的堆):
堆排序过程:
1、建堆
即挨个将元素添加到堆中,执行从下到上的堆化操作;当然我们也可以将元素直接添加到堆顶从上往下进行堆化,并且一般就是使用从上往下的堆化方式。堆本身是完全二叉树,堆化的时间复杂度与树的高度有关,看似时间复杂度为O(log₂N)。但是堆还有一个重要的特性就是整个数组的后半段都是叶子节点是不需要排序的(即 2/n+1 到 n 是叶子节点),所以其添加一个元素的时间复杂度是O(N)。繁琐的推导公式就不进行推演了,我数学也忘的差不多了。
2、排序
在堆对的情况下进行排序,即吧上面其他情况的堆变成排序好的堆。我们知道大顶堆的话,堆顶元素是最大的,移除对顶元素后,其余元素会再选出最大的元素放在堆顶(即从上往下的堆化过程)。所以排序的思路就是,将堆顶最大的元素不是删除而是移动到数组的最末尾,那么次大的元素会放入堆顶,一直执行这种操作,知道堆中只有一个元素。那么整个数组中就是排好序的数组。
而排序的过程需要将所有元素从堆中获取出来,会执行N遍log₂N,所以堆排序建堆的过程时间复杂度为O(N),排序的过程时间复杂度为O(N*logN),所以堆排序整体的时间复杂度为O(N*logN)。但是与同样时间复杂度的快速排序相比,性能要低一些。因为在建堆的时候,不论队列本身的有序度如何,(哪怕是满有序度)都会直接将所有元素打乱进行建堆操作。只是对排序也有自己的使用场景,比如我们需要对数据求Top N值时顺便将值进行了排序操作,更多对的使用场景下篇博客分享。
并且整个过程都在数组中处理,只是我们把堆的元素个数一直在递减,即堆在数组中的最大下标一直在递减。所以整个堆排序是原地排序,不需要申请额外的空间。
只是上面见堆的过程中可能涉及到和堆顶元素互换的过程,所以堆排序非稳定的排序。
堆排序的代码实现如下,只要理解了前面一片博客的从上往下和从下往上的堆化过程,已经PriorityQueue的源码的堆化过程,就好写了。
public class HeapSort {
public static void main(String[] args) {
int[] array = new int[]{12, 34, -34, 454, 657, 33, 89, 67, 68, 99, -23, 34};
System.out.println("排序前:" + Arrays.toString(array));
heapSort(array);
System.out.println("排序后:" + Arrays.toString(array));
}
/**
* 堆排序
*
* @param array 带排序的数组
*/
public static void heapSort(int[] array) {
if (array.length <= 1) {
return;
}
// 1、建堆
buildHeap(array);
// 2、排序
int k = array.length - 1;
while (k > 0) {
// 将堆顶元素(最大)与最后一个元素进行交换
swap(array, 0, k);
// 将剩余元素重新堆化,堆顶元素变成最大元素
heapify(array, --k, 0);
}
}
/**
* 交换数组中 两个下标位置的值
* @param array 待排序数组
* @param i 需要交换的下标 1
* @param k 需要交换的下标 2
*/
private static void swap(int[] array, int i, int k) {
int tmp = array[i];
array[i] = array[k];
array[k] = tmp;
}
/**
* 堆化操作
*
* @param array 需要排序的数组
* @param i
* @param i1
*/
private static void heapify(int[] array, int n, int i) {
while (true) {
// 记录最大位置
int maxPos = i;
int leftChild = i * 2 + 1;
// 与左子节点(leftChild)比较,获取最大值位置
if (leftChild <= n && array[i] < array[leftChild]) {
maxPos = leftChild;
}
// 与右子节点进行比较,获取最大值的位置
int rightChild = i * 2 + 2;
if (rightChild <= n && array[maxPos] < array[rightChild]) {
maxPos = rightChild;
}
// 如果上面左子和右子节点都比自己小则结束
if (maxPos == i) {
break;
}
// 与子节点交换位置
swap(array, i, maxPos);
// 交换完子节点后,继续往下找
i = maxPos;
}
}
/**
* 建堆
* @param array 待排序的数组
*/
private static void buildHeap(int[] array) {
// i的起始位置为: (array.length - 1) / 2, 是最后一个叶子节点的父节点
// 也就是最后一个非叶子节点,依次堆化直到根节点
for (int i = (array.length - 1) / 2; i >= 0; i--) {
heapify(array, array.length - 1, i);
}
}
}