数据结构与算法系列之一:八大排序之归并排序



[toc]

归并排序

前言

  建议先看排序综述,传送门:数据结构与算法系列之一:八大排序综述

简介

  归并排序(英语:Merge sort,或mergesort),是创建在归并操作上的一种有效的排序算法,效率为 ${\displaystyle O(n\log n)}$ 。1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。

步骤

  归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作。归并排序算法依赖归并操作。

递归法(Bottom-up)
原理如下(假设序列共有 ${\displaystyle n}$ 个元素):

  • 将序列每相邻两个数字进行归并操作,形成 ${\displaystyle ceil(n/2)}$ 个序列,排序后每个序列包含两/一个元素。
  • 若此时序列数不是1个则将上述序列再次归并,形成 ${\displaystyle ceil(n/4)}$个序列,每个序列包含四/三个元素。
  • 重复步骤2,直到所有元素排序完毕,即序列数为1。

迭代法(Top-down)

  • 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列。
  • 设定两个指针,最初位置分别为两个已经排序序列的起始位置。
  • 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置。
  • 重复步骤3直到某一指针到达序列尾。
  • 将另一序列剩下的所有元素直接复制到合并序列尾。

演示

  wikipedia的大数据规模演示:


merge from wikipedia

  wordzzzz的小数据规模演示:


bubblesort from wordzzzz

代码

递归版

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
/*
* 归并排序递归版
*/

template <typename T>
void Merge(T *array, T *reg, int left, int mid, int right) {
int left1 = left, right1 = mid;
int left2 = mid + 1, right2 = right;
int k = left;
while (left1 <= right1 && left2 <= right2)
reg[k++] = array[left1] < array[left2] ? array[left1++] : array[left2++];
while (left1 <= right1)
reg[k++] = array[left1++];
while (left2 <= right2)
reg[k++] = array[left2++];
for (k = left; k <= right; k++)
array[k] = reg[k];
}

template <typename T>
void MergeSortRecursive(T *array, T *reg, int left, int right) {
if (left >= right)
return;
int mid = left + ((right - left) >> 1);
MergeSortRecursive(array, reg, left, mid); //左序列排序
MergeSortRecursive(array, reg, mid + 1, right); //右序列排序
Merge(array, reg, left, mid, right); //合并左右序列
}

template <typename T>
void MergeSort(T *array, const int length) {
if (array == NULL)
throw invalid_argument("Array must not be empty");
if (length <= 0)
return;
T* reg = (T*)malloc(sizeof(T) * length);

if (reg == NULL)
{
fputs("Error: out of memory\n", stderr);
abort();
}
MergeSortRecursive(array, reg, 0, length - 1);
delete[] reg;
}

迭代版

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
/*
* 归并排序迭代版
*/

template<typename T>
void MergeSortIteration(T *array, const int length) {
if (array == NULL)
throw invalid_argument("Array must not be empty");
if (length <= 0)
return;

T* regB = (T*)malloc(sizeof(T) * length);

if (regB == NULL)
{
fputs("Error: out of memory\n", stderr);
abort();
}
for (int seg = 1; seg < length; seg += seg) { //步长,每次翻倍
for (int left = 0; left < length; left += seg + seg) {
int low = left, mid = min(left + seg, length), high = min(left + seg + seg, length);//因为可能会超出length
int k = low;
int left1 = low, right1 = mid;
int left2 = mid, right2 = high;
while (left1 < right1 && left2 < right2) //这里的表达式没有等号,都是左闭右开区间
regB[k++] = array[left1] < array[left2] ? array[left1++] : array[left2++];
while (left1 < right1)
regB[k++] = array[left1++];
while (left2 < right2)
regB[k++] = array[left2++];
}
for (int i = 0; i < length; i++) //更新array
array[i] = regB[i];
}
delete[] regB;
}

算法复杂度

  • 数据结构 数组
  • 最坏时间复杂度 ${\displaystyle O(n\log n)}$
  • 最优时间复杂度 ${\displaystyle O(n)}$
  • 平均时间复杂度 ${\displaystyle O(n\log n)}$
  • 空间复杂度 ${\displaystyle O(n)}$

  比较操作的次数介于 ${\displaystyle (n\log n)/2}$ 和 ${\displaystyle n\log n-n+1}$ 。 赋值操作的次数是 ${\displaystyle (2n\log n)}$ 。归并算法的空间复杂度为: ${\displaystyle \Theta (n)}$。

分析

  对于归并排序有几点说明:

  • 和快速排序一样,归并排序在小数组上面的表现不如插入排序。
  • 辅助数组是一个共用的数组。如果在每个归并的过程中都申请一个临时数组会造成比较大的时间开销。
  • 归并的过程需要将元素复制到辅助数组,再从辅助数组排序复制回原数组,会拖慢排序速度。

  归并排序有以下几点优化方法:

  • 和快速排序一样,对于小数组可以使用插入排序或者选择排序,避免递归调用。(代码见下面的归并排序递归版优化)
  • 在merge()调用之前,可以判断一下a[mid]是否小于等于a[mid+1]。如果是的话那么就不用归并了,数组已经是有序的。原因很简单,既然两个子数组已经有序了,那么a[mid]是第一个子数组的最大值,a[mid+1]是第二个子数组的最小值。当a[mid]<=a[mid+1]时,数组整体有序。
  • 为了节省将元素复制到辅助数组作用的时间,可以在递归调用的每个层次交换原始数组与辅助数组的角色。(代码见下面的归并排序迭代版优化)

递归版优化

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
/*
* 归并排序递归版合并函数
*/

template <typename T>
void Merge(T *array, T *reg, int left, int mid, int right) {
int left1 = left, right1 = mid;
int left2 = mid + 1, right2 = right;
int k = left;
while (left1 <= right1 && left2 <= right2)
reg[k++] = array[left1] < array[left2] ? array[left1++] : array[left2++];
while (left1 <= right1)
reg[k++] = array[left1++];
while (left2 <= right2)
reg[k++] = array[left2++];
for (k = left; k <= right; k++)
array[k] = reg[k];
}

/*
* 归并排序递归版递归函数优化
*/

template <typename T>
void MergeSortRecursive1(T *array, T *reg, int left, int right) {
if (left >= right)
return;
if (right - left <= M) //序列长度小于阈值就采用插入排序
InsertSort(array, left, right);
else{
int mid = left + ((right - left) >> 1);
MergeSortRecursive1(array, reg, left, mid); //左序列排序
MergeSortRecursive1(array, reg, mid + 1, right); //右序列排序
Merge(array, reg, left, mid, right); //合并左右序列
}
}

template <typename T>
void MergeSort(T *array, const int length) {
if (array == NULL)
throw invalid_argument("Array must not be empty");
if (length <= 0)
return;
T* reg = (T*)malloc(sizeof(T) * length);

if (reg == NULL)
{
fputs("Error: out of memory\n", stderr);
abort();
}
MergeSortRecursive1(array, reg, 0, length - 1);
delete[] reg;
}

迭代版优化

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
/*
* 归并排序迭代版优化
*/

template<typename T>
void MergeSortIteration1(T *array, const int length) {
if (array == NULL)
throw invalid_argument("Array must not be empty");
if (length <= 0)
return;

T* regA = array;
T* regB = (T*)malloc(sizeof(T) * length);

if (regB == NULL)
{
fputs("Error: out of memory\n", stderr);
abort();
}
for (int seg = 1; seg < length; seg += seg) { //步长,每次翻倍
for (int left = 0; left < length; left += seg + seg) {
int low = left, mid = min(left + seg, length), high = min(left + seg + seg, length);//因为可能会超出length
int k = low;
int left1 = low, right1 = mid;
int left2 = mid, right2 = high;
while (left1 < right1 && left2 < right2) //这里的表达式没有等号,都是左闭右开区间
regB[k++] = regA[left1] < regA[left2] ? regA[left1++] : regA[left2++];
while (left1 < right1)
regB[k++] = regA[left1++];
while (left2 < right2)
regB[k++] = regA[left2++];
}
T* temp = regA; //优化:交换辅助数组与原始数组的角色
regA = regB;
regB = temp;
}
if (regA != array) { //如果regA != array,则说明现在regA是辅助数组
for (int i = 0; i < length; i++) //所以需要拷贝数据到regB,也就是array。
regB[i] = regA[i];
regB = regA; //regB重新指向辅助数组
}
delete[] regB;
}

系列教程持续发布中,欢迎订阅、关注、收藏、评论、点赞哦~~( ̄▽ ̄~)~

完的汪(∪。∪)。。。zzz

0%