排序算法下篇
# 数据结构-堆
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

上图中的大顶堆,我们对堆中的结点按层进行编号,映射到数组中就是下面这个样子:

根据完全二叉树的性质,我们可以推导出大顶堆的特点为:arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2] (i 对应第几个节点,i从0开始编号)
同理可以推导出小顶堆的特点为:arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2]
# 堆排序
# 1. 基本思想
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
# 2. 算法描述
- 将待排序序列构造成一个大顶堆
- 此时,整个序列的最大值就是堆顶的根节点
- 将其与末尾元素进行交换,此时末尾就为最大值
- 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了

可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了
# 3. 代码实现
/**
* 堆排序
* @param arr
*/
public static void heapSort(int arr[]) {
int temp;
//将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆
for(int i = arr.length / 2 -1; i >=0; i--) {
adjustHeap(arr, i, arr.length);
}
for(int j = arr.length-1;j >0; j--) {
// 将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
temp = arr[j];
arr[j] = arr[0];
arr[0] = temp;
// 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序
adjustHeap(arr, 0, j);
}
}
/**
* 将 以 i 对应的非叶子结点的树调整成大顶堆
* @param arr 待调整的数组
* @param i 表示非叶子结点在数组中索引
* @param length 表示对多少个元素继续调整, length是在逐渐的减少
*/
private static void adjustHeap(int arr[], int i, int length){
int temp = arr[i];//先取出当前元素的值,保存在临时变量
// k = i * 2 + 1 k 是 i结点的左子结点
for(int k = i * 2 + 1; k < length; k = k * 2 + 1) {
// 说明左子结点的值小于右子结点的值
if(k+1 < length && arr[k] < arr[k+1]) {
k++; // k 指向右子结点
}
// 说明子结点大于父结点
if(arr[k] > temp) {
arr[i] = arr[k]; //把较大的值赋给当前结点
i = k; // i 指向 k,继续循环比较
}else{
break;
}
}
//当for 循环结束后,我们已经将以i 为父结点的树的最大值,放在了 最顶(局部)
arr[i] = temp;//将temp值放到调整后的位置
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 4. 思考总结
堆排序的时间复杂度为O(nlogn),由于堆排序对原始记录的排序状态并不敏感,因此它无论是最好、最坏和平均时间复杂度均为O(nlogn),这在性能上显然要远远好过于冒泡、简单选择、直接插入的O(n^2^)的时间复杂度了。
空间复杂度上,它只有一个用来交换的暂存单元,也非常的不错,不过由于记录的比较与交换是跳跃式进行,因此堆排序也是一种不稳定的排序方法。
另外,由于初始构建堆所需的比较次数较多,因此,它并不适合待排序序列个数较少的情况。
| 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 |
|---|---|---|---|
| O(nlogn) | O(nlogn) | O(nlogn) | O(1) |
# 归并排序
# 1. 基本思想
归并排序(MergeSort)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序思想示意图:

注意仔细观察它的形状,你会发现,它像极了一棵倒置的完全二叉树,通常涉及到完全二叉树结构的排序算法,效率一般都不低的。
再来看看治阶段,我们需要将两个已经有序的子序列合并成一个有序序列,比如上图中的最后一次合并,要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列,合并为最终序列[1,2,3,4,5,6,7,8],来看下实现步骤:

# 2. 算法描述
- 把长度为n的输入序列分成两个长度为n/2的子序列
- 对这两个子序列分别采用归并排序
- 将两个排序好的子序列合并成一个最终的排序序列

# 3. 代码实现
/**
* 归并排序
* @param arr 排序的原始数组
* @param left 左边有序序列的初始索引
* @param right 右边索引
* @param temp 做中转的数组
*/
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if(left < right) {
int mid = (left + right) / 2; //中间索引
//向左递归进行分解
mergeSort(arr, left, mid, temp);
//向右递归进行分解
mergeSort(arr, mid + 1, right, temp);
//合并
merge(arr, left, mid, right, temp);
}
}
/**
* 合并的方法
* @param arr 排序的原始数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
* @param temp 做中转的数组
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 初始化i, 左边有序序列的初始索引
int j = mid + 1; //初始化j, 右边有序序列的初始索引
int t = 0; // 指向temp数组的当前索引
while (i <= mid && j <= right) {
//如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素,将左边的当前元素,填充到 temp数组
if(arr[i] <= arr[j]) {
temp[t] = arr[i];
t += 1;
i += 1;
}else{
//反之,将右边有序序列的当前元素,填充到temp数组
temp[t] = arr[j];
t += 1;
j += 1;
}
}
// 把有剩余数据的一边的数据依次全部填充到temp
while( i <= mid) {
temp[t] = arr[i];
t += 1;
i += 1;
}
while( j <= right) {
temp[t] = arr[j];
t += 1;
j += 1;
}
// 将temp数组的元素拷贝到arr
t = 0;
int tempLeft = left;
while(tempLeft <= right) {
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 4. 思考总结
和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度,并且是一种稳定的排序算法。
由于归并排序在归并过程中需要与原始记录序列同样数量的存储空间存放归并结果以及递归时深度为log2n的栈空间,因此空间复杂度为O(n+logn)。
也就是说,归并排序是一种比较占用内存,但却效率髙且稳定的算法。
# 总结
迄今为止,我们一共介绍了七种排序算法:

事实上,目前还没有十全十美的排序算法,有优点就会有缺点,即使是快速排序法,也只是在整体性能上优越,它也存在排序不稳定、需要大量辅助空间、对少量数据排序无优势等不足。因此我们就来从多个角度来剖析一下提到的各种排序的长与短:

注:n: 数据规模;k: “桶”的个数;In-place: 占用常数内存,不占用额外内存;Out-place: 占用额外内存
从算法的简单性来看,我们将7种算法分为两类:
- 简单算法:冒泡、简单选择、直接插入
- 改进算法:希尔、堆、归并、快速
从平均情况来看,显然最后3种改进算法要胜过希尔排序,并远远胜过前3种简单算法。
从最好情况看,反而冒泡和直接插入排序要更胜一筹,也就是说,如果你的待排序序列总是基本有序,反而不应该考虑4种复杂的改进算法。
从最坏情况看,堆排序与归并排序又强过快速排序以及其他简单排序。
从稳定性来看,归并排序独占鳌头,我们前面也说过,对于非常在乎排序稳定性的应用中,归并排半是个好算法。
从待排序记录的个数上来说,待排序的个数n越小,采用简单排序方法越合适。反之,n越大,采用改进排序方法越合适。
选择排序在3种简单排序中性能最差,其实也不完全是,比如,如果记录的关键字本身信息量比较大,此时表明其占用存储空间很大,这样移动记录所花费的时间也就越多。此时简单选择排序就变得非常有优势,原因也就在于,它是通过大量比较后选择明确记录进行移动,有的放矢。
总之,从综合各项指标来说,经过优化的快速排序是性能最好的排序算法,但是不同的场合我们也应该考虑使用不同的算法来应对它。