|
| 1 | +--- |
| 2 | +title: 堆排序 |
| 3 | +date: 2015/03/08 |
| 4 | +categories: |
| 5 | +- algorithm |
| 6 | +tags: |
| 7 | +- algorithm |
| 8 | +- sort |
| 9 | +--- |
| 10 | + |
| 11 | +# 堆排序 |
| 12 | + |
| 13 | +## 要点 |
| 14 | + |
| 15 | +在介绍堆排序之前,首先需要说明一下,堆是个什么玩意儿。 |
| 16 | + |
| 17 | +**堆**是一棵**顺序存储**的**完全二叉树**。 |
| 18 | + |
| 19 | +其中每个结点的关键字都**不大于**其孩子结点的关键字,这样的堆称为**小根堆**。 |
| 20 | + |
| 21 | +其中每个结点的关键字都**不小于**其孩子结点的关键字,这样的堆称为**大根堆**。 |
| 22 | + |
| 23 | +举例来说,对于 n 个元素的序列 {R0, R1, ... , Rn} 当且仅当满足下列关系之一时,称之为堆: |
| 24 | + |
| 25 | +- **Ri <= R2i+1 且 Ri <= R2i+2 (小根堆)** |
| 26 | + |
| 27 | +- **Ri >= R2i+1 且 Ri >= R2i+2 (大根堆)** |
| 28 | + |
| 29 | +其中 i=1,2,…,n/2 向下取整; |
| 30 | + |
| 31 | + |
| 32 | + |
| 33 | +如上图所示,序列 R{3, 8,15, 31, 25} 是一个典型的小根堆。 |
| 34 | + |
| 35 | +堆中有两个父结点,元素 3 和元素 8。 |
| 36 | + |
| 37 | +元素 3 在数组中以 R[0] 表示,它的左孩子结点是 R[1],右孩子结点是 R[2]。 |
| 38 | + |
| 39 | +元素 8 在数组中以 R[1] 表示,它的左孩子结点是 R[3],右孩子结点是 R[4],它的父结点是 R[0]。可以看出,它们**满足以下规律**: |
| 40 | + |
| 41 | +设当前元素在数组中以 **R[i]** 表示,那么, |
| 42 | + |
| 43 | +- 它的**左孩子结点**是:**R[2\*i+1]**; |
| 44 | + |
| 45 | +- 它的**右孩子结点**是:**R[2\*i+2]**; |
| 46 | + |
| 47 | +- (3) 它的**父结点**是:**R[(i-1)/2]**; |
| 48 | + |
| 49 | +- R[i] <= R[2*i+1] 且 R[i] <= R[2i+2]。 |
| 50 | + |
| 51 | + |
| 52 | +## 算法思想 |
| 53 | + |
| 54 | +- 首先,按堆的定义将数组R[0..n]调整为堆(这个过程称为创建初始堆),交换R[0]和R[n]; |
| 55 | + |
| 56 | +- 然后,将R[0..n-1]调整为堆,交换R[0]和R[n-1]; |
| 57 | + |
| 58 | +- 如此反复,直到交换了R[0]和R[1]为止。 |
| 59 | + |
| 60 | + |
| 61 | +以上思想可归纳为两个操作: |
| 62 | + |
| 63 | +1. 根据初始数组去**构造初始堆**(构建一个完全二叉树,保证所有的父结点都比它的孩子结点数值大)。 |
| 64 | + |
| 65 | +2. 每次**交换第一个和最后一个元素,输出最后一个元素**(最大值),然后把剩下元素**重新调整**为大根堆。 |
| 66 | + |
| 67 | + |
| 68 | +当输出完最后一个元素后,这个数组已经是按照从小到大的顺序排列了。 |
| 69 | + |
| 70 | +先通过详细的实例图来看一下,如何构建初始堆。 |
| 71 | + |
| 72 | +设有一个无序序列 { 1, 3,4, 5, 2, 6, 9, 7, 8, 0 }。 |
| 73 | + |
| 74 | + |
| 75 | + |
| 76 | +构造了初始堆后,我们来看一下完整的堆排序处理: |
| 77 | + |
| 78 | +还是针对前面提到的无序序列 { 1,3, 4, 5, 2, 6, 9, 7, 8, 0 } 来加以说明。 |
| 79 | + |
| 80 | + |
| 81 | + |
| 82 | +相信,通过以上两幅图,应该能很直观的演示堆排序的操作处理。 |
| 83 | + |
| 84 | +**核心代码** |
| 85 | + |
| 86 | +```java |
| 87 | +public void HeapAdjust(int[] array, int parent, int length) { |
| 88 | + int temp = array[parent]; // temp保存当前父节点 |
| 89 | + int child = 2 * parent + 1; // 先获得左孩子 |
| 90 | + |
| 91 | + while (child < length) { |
| 92 | + // 如果有右孩子结点,并且右孩子结点的值大于左孩子结点,则选取右孩子结点 |
| 93 | + if (child + 1 < length && array[child] < array[child + 1]) { |
| 94 | + child++; |
| 95 | + } |
| 96 | + |
| 97 | + // 如果父结点的值已经大于孩子结点的值,则直接结束 |
| 98 | + if (temp >= array[child]) |
| 99 | + break; |
| 100 | + |
| 101 | + // 把孩子结点的值赋给父结点 |
| 102 | + array[parent] = array[child]; |
| 103 | + |
| 104 | + // 选取孩子结点的左孩子结点,继续向下筛选 |
| 105 | + parent = child; |
| 106 | + child = 2 * child + 1; |
| 107 | + } |
| 108 | + |
| 109 | + array[parent] = temp; |
| 110 | +} |
| 111 | + |
| 112 | +public void heapSort(int[] list) { |
| 113 | + // 循环建立初始堆 |
| 114 | + for (int i = list.length / 2; i >= 0; i--) { |
| 115 | + HeapAdjust(list, i, list.length); |
| 116 | + } |
| 117 | + |
| 118 | + // 进行n-1次循环,完成排序 |
| 119 | + for (int i = list.length - 1; i > 0; i--) { |
| 120 | + // 最后一个元素和第一元素进行交换 |
| 121 | + int temp = list[i]; |
| 122 | + list[i] = list[0]; |
| 123 | + list[0] = temp; |
| 124 | + |
| 125 | + // 筛选 R[0] 结点,得到i-1个结点的堆 |
| 126 | + HeapAdjust(list, 0, i); |
| 127 | + System.out.format("第 %d 趟: \t", list.length - i); |
| 128 | + printPart(list, 0, list.length - 1); |
| 129 | + } |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +## 算法分析 |
| 134 | + |
| 135 | +**堆排序算法的总体情况** |
| 136 | + |
| 137 | +| 参数 | 结果 | |
| 138 | +| --------- | --------- | |
| 139 | +| 排序类别 | 选择排序 | |
| 140 | +| 排序方法 | 堆排序 | |
| 141 | +| 时间复杂度平均情况 | O(nlog2n) | |
| 142 | +| 时间复杂度最坏情况 | O(nlog2n) | |
| 143 | +| 时间复杂度最好情况 | O(nlog2n) | |
| 144 | +| 空间复杂度 | O(1) | |
| 145 | +| 稳定性 | 不稳定 | |
| 146 | +| 复杂性 | 较复杂 | |
| 147 | + |
| 148 | +### 时间复杂度 |
| 149 | + |
| 150 | +堆的存储表示是**顺序的**。因为堆所对应的二叉树为完全二叉树,而完全二叉树通常采用顺序存储方式。 |
| 151 | + |
| 152 | +当想得到一个序列中第 **k** 个最小的元素之前的部分排序序列,最好采用堆排序。 |
| 153 | + |
| 154 | +因为堆排序的时间复杂度是 **O(n+klog2n)**,若 **k ≤ n/log2n**,则可得到的时间复杂度为 **O(n)**。 |
| 155 | + |
| 156 | +### 算法稳定性 |
| 157 | + |
| 158 | +堆排序是一种**不稳定**的排序方法。 |
| 159 | + |
| 160 | +因为在堆的调整过程中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径, |
| 161 | + |
| 162 | +因此对于相同的关键字就可能出现排在后面的关键字被交换到前面来的情况。 |
| 163 | + |
| 164 | +## 示例代码 |
| 165 | + |
| 166 | +[我的 Github 测试例](https://github.com/dunwu/algorithm-notes/blob/master/codes/src/test/java/io/github/dunwu/algorithm/sort/SortStrategyTest.java) |
| 167 | + |
| 168 | +样本包含:数组个数为奇数、偶数的情况;元素重复或不重复的情况。且样本均为随机样本,实测有效。 |
0 commit comments