归并排序

归并排序

一、算法描述

1.1 核心思想

  • 二分,将数据二等均分,然后分别排序,再合并2个排好序的数据
  • 递归,一直二等均分数据,直到无法分割后,才开始递归合并返回
  • 整个二分和合并的过程类似于一棵二叉树,从下往上合并数据,先对子树排序,再合并成根节点

1.2 细节解释

比如说,数组 [5, 7, 2, 4, 3, 6, 1]

首先是一直二等均分数据,直到无法分割为止:

1
2
 ___     ___     ___     ___     ___     ___     ___
| 5 | | 7 | | 2 | | 4 | | 3 | | 6 | | 1 |

然后从下往上开始合并排序返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 ___     ___     ___     ___     ___     ___     ___
| 5 | | 7 | | 2 | | 4 | | 3 | | 6 | | 1 | -- 原始数组
| | | | | | |
|_______| |_______| |_______| |
| | | |
V V V V
___ ___ ___ ___ ___ ___ ___
| 5 | 7 | | 2 | 4 | | 3 | 6 | | 1 | -- 第一层合并
| | | |
|_______________| |___________|
| |
V V
___ ___ ___ ___ ___ ___ ___
| 2 | 4 | 5 | 7 | | 1 | 3 | 6 | -- 第二层合并
| |
|_____________________________|
|
V
___ ___ ___ ___ ___ ___ ___
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | -- 第三层合并

归并排序采用的是分治思想,将小的有序数据逐渐合并成大的有序数据。

将 2 个有序数组合并成 1 个数组时,一般采用双指针,类似这样:

1
2
3
4
5
6
7
8
9
  lp                              rp
| |
V V
___ ___ ___ ___ ___ ___ ___
| 2 | 4 | 5 | 7 | | 1 | 3 | 6 | -- 当前数组


___ ___ ___ ___ ___ ___ ___
| | | | | | | | -- 额外空间

判断双指针指向元素的大小,小的放到额外空间中,然后移动指针:

1
2
3
4
5
6
7
8
9
  lp                                  rp
| |
V V
___ ___ ___ ___ ___ ___ ___
| 2 | 4 | 5 | 7 | | 1 | 3 | 6 | -- 当前数组


___ ___ ___ ___ ___ ___ ___
| 1 | | | | | | | -- 额外空间

通过不断判断和移动双指针,最终可以完成对 2 个有序数组的合并:

1
2
3
4
5
6
7
8
9
      lp                              rp
| |
V V
___ ___ ___ ___ ___ ___ ___
| 2 | 4 | 5 | 7 | | 1 | 3 | 6 | -- 当前数组


___ ___ ___ ___ ___ ___ ___
| 1 | 2 | | | | | | -- 额外空间
1
2
3
4
5
6
7
8
9
      lp                                 rp
| |
V V
___ ___ ___ ___ ___ ___ ___
| 2 | 4 | 5 | 7 | | 1 | 3 | 6 | -- 当前数组


___ ___ ___ ___ ___ ___ ___
| 1 | 2 | 3 | | | | | -- 额外空间
1
2
3
4
5
6
7
8
9
          lp                              rp
| |
V V
___ ___ ___ ___ ___ ___ ___
| 2 | 4 | 5 | 7 | | 1 | 3 | 6 | -- 当前数组


___ ___ ___ ___ ___ ___ ___
| 1 | 2 | 3 | 4 | | | | -- 额外空间
1
2
3
4
5
6
7
8
9
              lp                          rp
| |
V V
___ ___ ___ ___ ___ ___ ___
| 2 | 4 | 5 | 7 | | 1 | 3 | 6 | -- 当前数组


___ ___ ___ ___ ___ ___ ___
| 1 | 2 | 3 | 4 | 5 | | | -- 额外空间
1
2
3
4
5
6
7
8
9
              lp                              rp
| |
V V
___ ___ ___ ___ ___ ___ ___
| 2 | 4 | 5 | 7 | | 1 | 3 | 6 | -- 当前数组


___ ___ ___ ___ ___ ___ ___
| 1 | 2 | 3 | 4 | 5 | 6 | | -- 额外空间
1
2
3
4
5
6
7
8
9
                  lp                          rp
| |
V V
___ ___ ___ ___ ___ ___ ___
| 2 | 4 | 5 | 7 | | 1 | 3 | 6 | -- 当前数组


___ ___ ___ ___ ___ ___ ___
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | -- 额外空间

利用额外空间合并完成之后,再把排好序的数据回写到原数组即可。

二、算法实现

二分排序代码,将数据分割成2部分,分别排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sort(int[] arr, int start, int end) {
if (start + 1 >= end) {
return;
}

// 计算中点
int mid = start + (end - start) / 2;
// 对左边进行排序
sort(arr, start, mid);
// 对右边进行排序
sort(arr, mid, end);
// 合并左右两边的数据
merge(arr, start, mid, end);
}

合并排好序的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
merge(int[] arr, int start, int mid, int end) {
int n = end - start;
int[] tempArr = new int[n];
int i = start, j = mid, k = 0;
while (i < mid && j < end) {
// 优先排左边的,保证稳定性
if (arr[i] <= arr[j]) {
tempArr[k++] = arr[i++];
} else {
tempArr[k++] = arr[j++];
}
}
// 左边剩余元素
while (i < mid) {
tempArr[k++] = arr[i++];
}
// 右边剩余元素
while (j < end) {
tempArr[k++] = arr[j++];
}
// 注意复制到原数组时,起点是 start
System.arraycopy(tempArr, 0, arr, start, n);
}

三、算法分析

3.1 时间复杂度

  • 最好时间复杂度:O(nlogn)
  • 最坏时间复杂度:O(nlogn)
  • 平均时间复杂度:O(nlogn)

3.2 空间复杂度

  • 空间复杂度:O(n)
  • 非原地算法,需要额外空间

3.3 稳定性

  • 稳定排序算法

四、适用场景

  • 空间比较充足
  • 利用空间换时间提升性能

附录

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
66
67
68
69
70
71
72
73
74
/**
* 归并排序
* <p>
* 时间复杂度:O(nlogn)
* <p>
* 空间复杂度:O(1)
* <p>
* 稳定性:稳定
*
* @author weijiaduo
* @since 2022/9/1
*/
public class MergeSort implements Sort {

@Override
public void sort(int[] arr) {
sort(arr, 0, arr.length);
}

/**
* 排序
*
* @param arr 数组
* @param start [start, end)
* @param end [start, end)
*/
private void sort(int[] arr, int start, int end) {
if (start + 1 >= end) {
return;
}

// 计算中点
int mid = start + (end - start) / 2;
// 对左边进行排序
sort(arr, start, mid);
// 对右边进行排序
sort(arr, mid, end);
// 合并左右两边的数据
merge(arr, start, mid, end);
}

/**
* 合并排序数组
*
* @param arr 数组
* @param start [start, end)
* @param mid [start, mid) 和 [mid, end)
* @param end [start, end)
*/
private void merge(int[] arr, int start, int mid, int end) {
int n = end - start;
int[] mergeArr = new int[n];
int i = start, j = mid, k = 0;
while (i < mid && j < end) {
// 优先排左边的,保证稳定性
if (arr[i] <= arr[j]) {
mergeArr[k++] = arr[i++];
} else {
mergeArr[k++] = arr[j++];
}
}
// 左边剩余元素
while (i < mid) {
mergeArr[k++] = arr[i++];
}
// 右边剩余元素
while (j < end) {
mergeArr[k++] = arr[j++];
}
// 注意复制到原数组时,起点是 start
System.arraycopy(mergeArr, 0, arr, start, n);
}

}
作者

jiaduo

发布于

2022-09-01

更新于

2023-10-06

许可协议