4.4.1 传说中的堆排序
还记得二叉堆的特性是什么吗?
1. 最大堆的堆顶是整个堆中的最大元素。
2. 最小堆的堆顶是整个堆中的最小元素。
以最大堆为例,如果删除一个最大堆的堆顶(并不是完全删除,而是跟末尾的节点交换位置),经过自我调整,第2大的元素就会被交换上来,成为最大堆的新堆顶。
正如上图所示,在删除值为10的堆顶节点后,经过调整,值为9的新节点就会顶替上来;在删除值为9的堆顶节点后,经过调整,值为8的新节点就会顶替上来……
由于二叉堆的这个特性,每一次删除旧堆顶,调整后的新堆顶都是大小仅次于旧堆顶的节点。那么只要反复删除堆顶,反复调整二叉堆,所得到的集合就会成为一个有序集合,过程如下。
删除节点9,节点8成为新堆顶。
删除节点8,节点7成为新堆顶。
删除节点7,节点6成为新堆顶。
删除节点6,节点5成为新堆顶。
删除节点5,节点4成为新堆顶。
删除节点4,节点3成为新堆顶。
删除节点3,节点2成为新堆顶。
到此为止,原本的最大二叉堆已经变成了一个从小到大的有序集合。之前说过,二叉堆实际存储在数组中,数组中的元素排列如下。
由此,可以归纳出堆排序算法的步骤。
1. 把无序数组构建成二叉堆。需要从小到大排序,则构建成最大堆;需要从大到小排序,则构建成最小堆。
2. 循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆顶。
大体思路明白了,那么该如何用代码来实现呢?
讲二叉堆时,我们写了二叉堆操作的相关代码。现在只要在原代码的基础上稍微改动一点点,就可以实现堆排序了。
4.4.2 堆排序的代码实现
1. /**
2. * “下沉”调整
3. * @param array 待调整的堆
4. * @param parentIndex 要“下沉”的父节点
5. * @param length 堆的有效大小
6. */
7. public static void downAdjust(int[] array, int parentIndex,
int length) {
8. // temp 保存父节点值,用于最后的赋值
9. int temp = array[parentIndex];
10. int childIndex = 2 * parentIndex + 1;
11. while (childIndex < length) {
12. // 如果有右孩子,且右孩子大于左孩子的值,则定位到右孩子
13. if (childIndex + 1 < length && array[childIndex + 1] >
array[childIndex]) {
14. childIndex++;
15. }
16. // 如果父节点大于任何一个孩子的值,则直接跳出
17. if (temp >= array[childIndex])
18. break;
19. //无须真正交换,单向赋值即可
20. array[parentIndex] = array[childIndex];
21. parentIndex = childIndex;
22. childIndex = 2 * childIndex + 1;
23. }
24. array[parentIndex] = temp;
25. }
26.
27.
28. /**
29. * 堆排序(升序)
30. * @param array 待调整的堆
31. */
32. public static void heapSort(int[] array) {
33. // 1. 把无序数组构建成最大堆
34. for (int i = (array.length-2)/2; i >= 0; i--) {
35. downAdjust(array, i, array.length);
36. }
37. System.out.println(Arrays.toString(array));
38. // 2. 循环删除堆顶元素,移到集合尾部,调整堆产生新的堆顶
39. for (int i = array.length - 1; i > 0; i--) {
40. // 最后1个元素和第1个元素进行交换
41. int temp = array[i];
42. array[i] = array[0];
43. array[0] = temp;
44. // “下沉”调整最大堆
45. downAdjust(array, 0, i);
46. }
47. }
48.
49.
50. public static void main(String[] args) {
51. int[] arr = new int[] {1,3,2,6,5,7,8,9,10,0};
52. heapSort(arr);
53. System.out.println(Arrays.toString(arr));
54. }
原来如此,现在明白了!那么堆排序的时间复杂度和空间复杂度各是多少呢?
毫无疑问,空间复杂度是O(1),因为并没有开辟额外的集合空间。t至于时间复杂度,我们来分析一下。
二叉堆的节点“下沉”调整(downAdjust 方法)是堆排序算法的基础,这个调节操作本身的时间复杂度在上一章讲过,是O(log n)。
我们再来回顾一下堆排序算法的步骤。
1. 把无序数组构建成二叉堆。
2. 循环删除堆顶元素,并将该元素移到集合尾部,调整堆产生新的堆顶。
第1步,把无序数组构建成二叉堆,这一步的时间复杂度是O(n) 。
第2步,需要进行n-1次循环。每次循环调用一次downAdjust方法,所以第2步的计算规模是 (n-1)×logn ,时间复杂度为O(nlogn) 。
两个步骤是并列关系,所以整体的时间复杂度是O(nlogn) 。
最后一个问题,从宏观上看,堆排序和快速排序相比,有什么区别和联系呢?
先说说相同点,堆排序和快速排序的平均时间复杂度都是O(nlogn) ,并且都是不稳定排序 。至于不同点,快速排序的最坏时间复杂度是O(n 2 ) ,而堆排序的最坏时间复杂度稳定在O(nlogn) 。
此外,快速排序递归和非递归方法的平均空间复杂度都是O(logn) ,而堆排序的空间复杂度是O(1) 。
好了,关于堆排序算法,我们就介绍到这里。感谢大家!