一、原理
1. 理解
【分治思想理解:错在以为最小问题是会直接比较两个元素,真正的做法是解部分干脆就不比较,直接划分成单个元素,由并部分比较两个元素及其以上】的确,归并排序的解部分的功能和并部分的功能重复,可以写到一起。
错误理解:
- 分到不能再分才开始解:归并排序一直划分(递归)到排序两个元素为止。
- 解只是解最小问题的解:解只是排序两个元素。
- 并不是求解具体的最小问题的,只是负责将子问题的答案如何合并:负责将已经得到的两个排好的部分排序是并。
正确理解:
- 分到不能再分才开始解:归并排序一直划分(递归)到排序一个元素(不能再划分)为止。
- 解只是解最小问题的解:归并排序的解不做任何事情。
- 并不是求解具体的最小问题的,只是负责将子问题的答案如何合并:负责将已经得到的两个排好的部分排序是并。
2. 伪代码
MERGE-SORT(A, p, r)
:
// 伪代码
// 数组A,开始下标p,结束下标r
MERGE-SORT(A, p, r)
// 保证p<q,还可以划分
if p<r
// 分成两半排序
// 选取q,奇数个数就是中间的值的下标,偶数个数就是中间偏左的值的下标
then q ← ⌊(p + r) / 2)⌋
// 从p到q
MERGE-SORT(A, p, q)
// 从q+1,到r。q已经在它该在的位置上了。
MERGE-SORT(A, q + 1, r)
// 归并部分
MERGE(A, p, q, r)
MERGE(A, p, q, r)
:
// 数组A,开始下标p,中间位置的下标q,结束下标r
MERGE(A, p, q, r)
// 从p到q(包括)共有多少个元素
n1 ← q - p + 1
// 从q+1到r(包括)共有多少个元素
n2 ← r - q
// 创建两个数组L和R,元素个数是n+1个
create arrays L[1..n1+1] and R[1..n2+1]
// L数组是A数组的从p到q(包括)部分
for i ← 1 to n1
do L[i] ← A[p + i - 1]
// R数组是A数组的从q+1到r(包括)部分
for j ← 1 to n2
do R[j] ← A[q + j]
// 正无穷,用于当L、R数组遍历结束时,L[i]、R[j]就是正无穷。
L[n1 + 1] ← ∞
R[n2 + 1] ← ∞
// 将L和R数组中的元素按照特定的方式插入到A数组中。
// L数组的下标,指在L数组最左边(即A数组的p)
i ← 1
// R数组的下标,指在R数组最左边(即A数组的q+1)
j ← 1
// 遍历A数组中的p到r部分。
for k ← p to r
// 将L[i]和R[j]中较小的元素插入到A中
// 如果L[i]小于等于R[j]。这个等于在这里,保证了同值的元素保持原来的先后顺序
do if L[i] ≤ R[j]
// 将更小的元素L[i]插入到A数组中,L数组的下标i右移
then A[k] ← L[i]
i ← i + 1
// 如果R[i]比L[j]更小
// 将更小的元素R[i]插入到A数组中,R数组的下标j右移
else A[k] ← R[j]
j ← j+1
3. 图示
-
MERGE-SORT(A, p, r)
:
考点:递归的顺序。
【①⑪②】像是树的先序遍历,将问题划分成A和B后,不是同时去再划分A和B,而是一直沿着A这样划分下去。
【⑤⑨⑩】只要能合并就立即合并,而不是先去划分。 -
MERGE(A, p, q, r)
:
二、特点
优点:
- 外部排序:可以不用一次全部读入
- 稳定的排序算法:同值的元素保持原来的先后顺序
缺点:
- 需要额外创建的空间代价
时间复杂度:
- 最小规模 c=1
- 划分产生 a=2 个子问题,每个子问题的规模是 1/b=1/2
- 划分时间 D(n)=无(因为直接传参数
⌊(p + r) / 2)⌋
就行) - 合并时间 C(n)=
Θ
(
n
)
\Theta(n)
Θ(n)
三、实现
#include <iostream>
using namespace std;
// ACM中的无穷大常量
#define INF 0x3F3F3F3F
void merge(int A[], int p, int q, int r)
{
// 从p到q(包括)共有多少个元素
int n1 = q - p + 1;
// 从q+1到r(包括)共有多少个元素
int n2 = r - q;
// 创建两个数组L和R,元素个数是n+1个
int *L = new int[n1+1];
int *R = new int[n2+1];
// L数组是A数组的从p到q(包括)部分
for (int i = 0; i < n1; i++)
{
L[i] = A[p + i];
}
// R数组是A数组的从q+1到r(包括)部分
for (int j = 0; j < n2; j++)
{
R[j] = A[q + 1 + j];
}
// 正无穷,用于当L、R数组遍历结束时,L[i]、R[j]就是正无穷。
L[n1] = INF;
R[n2] = INF;
// 将L和R数组中的元素按照特定的方式插入到A数组中。
// i是L数组的下标,指在L数组最左边(即A数组的p)
// j是R数组的下标,指在R数组最左边(即A数组的q+1)
// 遍历A数组中的p到r部分。
for (int i = 0, j = 0, k = p; k <= r; k++)
{
// 将L[i]和R[j]中较小的元素插入到A中
// 如果L[i]小于等于R[j]。这个等于在这里,保证了同值的元素保持原来的先后顺序
if (L[i] <= R[j])
{
A[k] = L[i];
i++;
}
// 如果R[i]比L[j]更小
// 将更小的元素R[i]插入到A数组中,R数组的下标j右移
else
{
A[k] = R[j];
j++;
}
}
// 释放数组
delete[] L,R;
}
// 下标从p到q(包括q)
void merge_sort(int A[], int p, int r)
{
// 保证p<q,还可以划分
if (p < r)
{
int q = (int)((p + r) / 2);
// 或者 int q = floor((p + r) / 2); 需要#include <math.h>
// 从p到q
merge_sort(A, p, q);
// 从q+1,到r。q已经在它该在的位置上了。
merge_sort(A, q + 1, r);
// 归并部分
merge(A, p, q, r);
}
}
int main()
{
int A[] = {8, 4, 5, 7, 1, 3, 6, 2};
int length = sizeof(A) / sizeof(int);
merge_sort(A, 0, length - 1);
for (int i = 0; i < length; i++)
{
cout << A[i] << ' ';
}
return 0;
}