归并排序的实现

1.概述

归并排序由冯·诺伊曼在1945年首次提出,是一种典型的分治思想:将问题拆分,递归处理,然后合并。归并排序的基础思路是,当一个数组的前后两部分都是有序的话,那么可以在O(n)时间内合并,使整个数组有序。归并排序的算法复杂度,在最好、最坏和平均的情况下,都是O(nLogn)。

2.算法

归并排序有两种思路:自顶向下和自底向上。

自顶向下就是先将数组作为整体,设数组长度为n,则先分成两个n/2的数组,递归调用归并排序,然后再合并。

按照这个思路,可以先写出以下递归代码:

1
2
3
4
5
6
7
8
void mergeSort(vector<int> &nums, int i, int j){
if (i < j){
int mid = (i + j) / 2;
mergeSort(nums, i, mid); // 使[i, mid]有序
mergeSort(nums, mid + 1, j); // 使(mid, j]有序
merge(nums, i, mid, j); // 将有序的两部分[i, mid], (mid, j]合并起来
}
}

merge的过程也很简单:

  1. 分配一个临时数组,空间大小和待合并的数组相同:j-i+1
  2. 当左右两部分数组都有值时,对比两个数组中最小的值,将该值存放到临时数组中
  3. 当一个子数组的数据全部存放到临时数组后,将另一个子数组的数据依次放到临时数组中
  4. 将临时数组的数据替换到原数组中

在这个过程需要分配额外的空间,其大小和数组长度一致,所以归并排序的空间复杂度为O(n)

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void merge(vector<int>& nums, int left, int mid, int right){
vector<int> tmp(right - left + 1); // 分配临时数组
int i = left, j = mid + 1, k = 0; // i,j是左右两个子数组的起点,将移动到各自的终点:mid和right
while(i <= mid && j <= right){ // 当两个子数组都有数据时,进行对比
if (nums[i] <= nums[j]){ // 将小的值存放到tmp中
tmp[k++] = nums[i++];
}else{
tmp[k++] = nums[j++];
}
}

while (i <= mid) { // 如果左边数组还有值,放到tmp中
tmp[k++] = nums[i++];
}

while (j <= right) { // 如果右边数组还有值,放到tmp中
tmp[k++] = nums[j++];
}

for (int p = 0; p < k; p++) { // 将tmp的值复制到原数组
nums[left + p] = tmp[p];
}
}

归并排序也可以采用自底向上的思路,即先将数组n分成一个个长度为1的子数组,然后两两合并;再分成长度为2的子数组,合并;直到分成长度为n/2的子数组,再进行合并,达到最终有序。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void mergeSort(vector<int> &nums){
int len = nums.size();
int left = 0, right = len - 1; // 左右边界
for (int width = 1; width < len; width *= 2){ // 分组的边长,依次是1,2,4,8,...
for (int i = 0; i < len - width; i += width * 2){ // 每个小数组的长度为width, 每次合并两个数组,i代表第1个数组的起点
int j = i + width * 2 - 1; // j是第二个数组的右边界
if (j > len - 1){
j = len - 1; // 不能超过整个数组的边界
}
int mid = i + width - 1; // mid是第一个数组的右边界
merge(nums, i, mid, j); // 合并
}
}
}

3.C++11代码

  • 递归版本
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
75
76
77
#include<vector>
#include<iostream>
using namespace std;

void merge(vector<int> &nums, int left, int mid, int right){
vector<int> tmp(right - left + 1);
int i = left, j = mid + 1, k = 0;

while(i <= mid && j <= right){
if (nums[i] > nums[j]){
tmp[k++] = nums[j++];
}else{
tmp[k++] = nums[i++];
}
}

while(i <= mid){
tmp[k++] = nums[i++];
}

while(j <= right){
tmp[k++] = nums[j++];
}

for (i = 0; i < k; i++){
nums[left + i] = tmp[i];
}
}

void mergeSort(vector<int> &nums, int i, int j){
if (i >= j){
return ;
}

int mid = (i + j) / 2;
mergeSort(nums, i, mid); // 对左边进行排序
mergeSort(nums, mid + 1, j); // 对右边进行排序

merge(nums, i, mid, j); // 现在[i, mid]和[mid+1, j]都是有序的,将这两部分合并
}

int main(){
// 测试用例
vector<vector<int>> test_case = {
{2, 2},
{1, 2},
{2, 1},
{2, 2, 2, 2, 2},
{1, 2, 3, 4, 5},
{5, 4, 3, 2, 1},
};

// 加一些随机数组
srand(time(0));
for (int i = 0; i < 1000; i++){
int len = rand() % 100 + 1;
vector<int> nums(len);
for (int j = 0; j < len; j++){
nums.push_back(rand() % 100);
}
test_case.push_back(nums);
}

for (auto nums : test_case){
mergeSort(nums, 0, nums.size() - 1);

for (int i = 1; i < nums.size(); i++){
if (nums[i - 1] > nums[i]){
cout << "error" << endl;
return 0;
}
}
}

cout << "all is ok" << endl;
return 0;
}
  • 非递归版本
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
75
76
77
78
79
80
81
#include<vector>
#include<iostream>
using namespace std;

void mergeSort(vector<int> &nums){
int len = nums.size();
vector<int> tmp(len);

for(int width = 1; width < len; width *= 2){ // witdh是每个小数组的大小,1,2,4,8, ...
for (int left = 0; left < len - width; left += 2 * width){ // left是每一对子数组的起点
// 处理每一对子数组,此时每个子数组的左右两边都是有序的
int mid = left + width - 1; //左数组的边界(因为left < len - width,所以mid不会大于len - 1)
int right = left + width * 2 - 1; //右数组的边界(不能超过len-1)
if (right > len - 1){
right = len - 1;
}
// 开始合并过程,此时[left, mid],(mid,right]都是有序的
int k = left;
int i = left;
int j = mid + 1;
while(i <= mid && j <= right){
if (nums[i] > nums[j]){
tmp[k++] = nums[j++];
}else{
tmp[k++] = nums[i++];
}
}

while(i <= mid){
tmp[k++] = nums[i++];
}

while(j <= right){
tmp[k++] = nums[j++];
}
}

//
for (int i = 0; i < len; i++){
nums[i] = tmp[i];
}
}
}


int main(){
// 测试用例
vector<vector<int>> test_case = {
{2, 2},
{1, 2},
{2, 1},
{2, 2, 2, 2, 2},
{1, 2, 3, 4, 5},
{5, 4, 3, 2, 1},
};

// 加一些随机数组
srand(time(0));
for (int i = 0; i < 1000; i++){
int len = rand() % 100 + 1;
vector<int> nums(len);
for (int j = 0; j < len; j++){
nums.push_back(rand() % 100);
}
test_case.push_back(nums);
}

for (auto nums : test_case){
mergeSort(nums);

for (int i = 1; i < nums.size(); i++){
if (nums[i - 1] > nums[i]){
cout << "error" << endl;
return 0;
}
}
}

cout << "all is ok" << endl;
return 0;
}

4.复杂度分析

归并排序其实可以看作是二叉树的后序遍历,树的深度是log2n,每一层的merge,其比较的总次数都是数组长度n,因此总体的时间复杂度是O(nlogn)。

5.对比

归并排序和快速排序都是常见的基于比较的排序算法,它们的时间复杂度都为 O(nlogn) 。
归并排序的最好、最坏和平均时间复杂度都为O(nlogn),并且它具有稳定性。
归并排序的主要缺点是需要开辟额外的空间,这可能在处理大规模数据时限制了其使用。

快速排序的优势在于它是一种原址排序算法,不需要额外的空间开销,它的平均速度比归并排序更快,特别是对于大规模数据集。但是,快速排序在最坏情况下的运行时间仍然不理想,而且它不具有稳定性,可能会改变相同元素的原始相对位置。
因此,选择归并排序或快速排序取决于具体问题和数据特征。在排序需要保持稳定性的问题上,归并排序是最好的选择;在其他问题上,特别是处理大规模数据时,快速排序通常更为高效。

参考资料


归并排序的实现
https://blog.supersource.top/merge_sort/
作者
看热闹的咸鱼
发布于
2023年4月29日
许可协议