如何实现归并排序?
作者:行百里er
博客:https://chendapeng.cn (opens new window)
提示
这里是 行百里er 的博客:行百里者半九十,凡事善始善终,吾将上下而求索!
# 归并排序
归并排序是 分而治之
的排序算法。
划分步骤很简单:将当前数组(元素个数为 N)分成两半,如果 N 是偶数,则将其完全平等划分为两个部分,或者如果 N 是奇数,则一边稍大于一个元素,然后 递归 地对这两半进行排序。
# 递归写法
归并排序递归写法的思想是,设定一个函数,函数实现的目的是 让int[] arr在L ~ R位置上有序 ,处理过程是从 L ~ R 上找一个中间位置M,递归调用该函数, 让int[] arr的L ~ M上有序,M+1 ~ R上有序 ,每一次不能往下递归了,便调用 归并
的方法将左右两边的数组合并成一个数组,到最后整个数组便有序了。
因此,归并排序使用递归方法实现的方法是: 整体是递归,左边排好序 + 右边排好序 + merge 让整体有序 。
伪代码理解这一过程:
将每个元素拆分成大小为1的部分
递归地合并相邻的两个数组分区
i = 左侧开始项 到 右侧最后项 的遍历
如果左侧第一个值 <= 右侧第一个值
拷贝左侧第一项的值
否则: 拷贝右侧部分第一项的值
将元素拷贝进原来的数组中
2
3
4
5
6
7
8
9
10
11
12
13
代码实现:
public class MergeSort {
public static void main(String[] args) {
int[] arr = {18, 15, 13, 17, 6, 20, 15, 9};
System.out.println("排序前:" + Arrays.toString(arr));
mergeSort(arr);
System.out.println("排序后:" + Arrays.toString(arr));
}
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int L, int R) {
if (L == R) {
return;
}
int M = L + ((R - L) >> 1);
System.out.println("递归调用 L--M--R:" + L + "--" + M + "--" + R);
//左边数组递归
process(arr, L, M);
//右边数组递归
process(arr, M + 1, R);
merge(arr, L, M, R);
}
public static void merge(int[] arr, int L, int M, int R) {
System.out.println("开始归并 arr[" + L + "~" + M + "]和arr[" + (M + 1) + "~" + R + "]两部分数组");
//申请一个和arr长度一样的辅助数组
int[] help = new int[R - L + 1];
//比较两组数组,谁小先拷贝谁到辅助数组,拷贝之后移动数组指针
//定义数组指针,LP表示左部分数组指针,RP表示右部分数组指针,i表示辅助数组的指针
int LP = L;
int RP = M + 1;
int i = 0;
//左右两边数组均不能越界
while (LP <= M && RP <= R) {
help[i++] = arr[LP] <= arr[RP] ? arr[LP++] : arr[RP++];
}
//任何一边的数组要越界了,就把该部分的数写到help数组
while (LP <= M) {
help[i++] = arr[LP++];
}
while (RP <= R) {
help[i++] = arr[RP++];
}
//写回到原数组
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
}
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
小技巧:
- 将一个int类型的数乘以2,可以使用位运算
<<
左移1位- int类型的数除以2,位运算
>>
右移1位别问为什么,问就是
位运算就是快
!
运行结果:
排序前:[18, 15, 13, 17, 6, 20, 15, 9]
递归调用 L--M--R:0--3--7
递归调用 L--M--R:0--1--3
递归调用 L--M--R:0--0--1
开始归并 arr[0~0]和arr[1~1]两部分数组
递归调用 L--M--R:2--2--3
开始归并 arr[2~2]和arr[3~3]两部分数组
开始归并 arr[0~1]和arr[2~3]两部分数组
递归调用 L--M--R:4--5--7
递归调用 L--M--R:4--4--5
开始归并 arr[4~4]和arr[5~5]两部分数组
递归调用 L--M--R:6--6--7
开始归并 arr[6~6]和arr[7~7]两部分数组
开始归并 arr[4~5]和arr[6~7]两部分数组
开始归并 arr[0~3]和arr[4~7]两部分数组
排序后:[6, 9, 13, 15, 15, 17, 18, 20]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
递归函数调用过程,我画了个简图以助理解:
拿代码中的数组分析,过程大概就是这样子滴:
# 非递归写法
任何递归写法都能转换成非递归写法。
直接上代码:
public static void mergeSort2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
//数组长度
int N = arr.length;
//定义每部分参与比较数组的长度,初始长度为1
int mergeSize = 1;
//只要mergeSize小于N
while (mergeSize < N) {
int L = 0;
while (L < N) {
int M = L + mergeSize - 1;
if (M >= N) {
break;
}
int R = Math.min(M + mergeSize, N - 1);
merge(arr, L, M, R);
L = R + 1;
}
// 为什么需要这个?主要是为了防止溢出,int的最大值是21亿多(2^31-1),
// 假如此时mergeSize是20亿,运行下面mergeSize*2的时候就会溢出
if (mergeSize > N / 2) {
break;
}
mergeSize <<= 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
其中的merge方法,还是前面递归方式调用的merge:
public static void merge(int[] arr, int L, int M, int R) {
System.out.println("开始归并 arr[" + L + "~" + M + "]和arr[" + (M + 1) + "~" + R + "]两部分数组");
//申请一个和arr长度一样的辅助数组
int[] help = new int[R - L + 1];
//比较两组数组,谁小先拷贝谁到辅助数组,拷贝之后移动数组指针
//定义数组指针,LP表示左部分数组指针,RP表示右部分数组指针,i表示辅助数组的指针
int LP = L;
int RP = M + 1;
int i = 0;
//左右两边数组均不能越界
while (LP <= M && RP <= R) {
help[i++] = arr[LP] <= arr[RP] ? arr[LP++] : arr[RP++];
}
//任何一边的数组要越界了,就把该部分的数写到help数组
while (LP <= M) {
help[i++] = arr[LP++];
}
while (RP <= R) {
help[i++] = arr[RP++];
}
//写回到原数组
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
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
运行结果:
排序前:[18, 15, 13, 17, 6, 20, 15, 9]
开始归并 arr[0~0]和arr[1~1]两部分数组
开始归并 arr[2~2]和arr[3~3]两部分数组
开始归并 arr[4~4]和arr[5~5]两部分数组
开始归并 arr[6~6]和arr[7~7]两部分数组
开始归并 arr[0~1]和arr[2~3]两部分数组
开始归并 arr[4~5]和arr[6~7]两部分数组
开始归并 arr[0~3]和arr[4~7]两部分数组
排序后:[6, 9, 13, 15, 15, 17, 18, 20]
2
3
4
5
6
7
8
9
这里只是归并排序的非递归写法,思想也是分而治之!关键还是 merge 方法。
针对代码中的数组 int[] arr={18, 15, 13, 17, 6, 20, 15, 9}
,其排序过程动图演示:
# 归并排序的时间复杂度
Level 0:$2 ^ 0 = 1$ 次调用 merge()
和 $N / 2 ^ 1$ 个元素,时间:$O(2 ^ 0 * 2 * N / 2 ^ 1)= O(N)$
Level 1:$2 ^ 1 = 2$ 次调用 merge()
与 $N / 2 ^ 2$ 个元素,$O(2 ^ 1 * 2 * N / 2 ^ 2)= O(N)$
Level 2:$2 ^ 2 = 4$ 次调用 merge()
与 $N / 2 ^ 3$ 个元素,$O(2 ^ 2 * 2 * N / 2 ^ 3)= O(N)$
...
有 log(N)
个层,每层都执行 O(N)
次工作,因此总体时间复杂度为 $O(NlogN)$。
# 拓展:递归计算时间复杂度公式
递归,计算时间复杂度有一个Master公式:
形如:
$$
T(N) = a * T(N/b) + O(N^d)(其中的a、b、d都是常数)
$$
的递归函数,可以直接通过 Master
公式来确定时间复杂度。
如果 log(b,a) < d,复杂度为O(N^d)
如果 log(b,a) > d,复杂度为O(N^log(b,a))
如果 log(b,a) == d,复杂度为O(N^d * logN)
我们的归并排序可以用下面的公式来计算: $$ T(N) = 2*T(N/2) + O(N^1) $$ 根据master可知推导出时间复杂度为 $O(N×logN)$
另外,merge
过程需要辅助数组,所以额外空间复杂度为 $O(N)$ 。
归并排序的实质是把比较行为变成了有序信息并传递,比 $O(N^2)$ 的排序快。
首发公众号 行百里er ,欢迎老铁们关注阅读指正。