归并排序算法

1.划分问题:把序列分成元素个数尽量相等的两半。

2.递归求解:把两半元素分别排序。

3.合并问题:把两个有序表合并成一个。

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

算法代码

void merge_sort(int *A, int x, int y, int *T) {
if (y - x > 1) //等价于x+1<y,即左半部分的右边界还在右半部分的左边界左边
{
int m = x + (y - x) / 2;
//划分
int p = x, q = m, i = x;
merge_sort(A, x, m, T);
//递归求左解
merge_sort(A, m, y, T);
//递归求右解
while (p < m || q < y)
{
if (q >= y || (p < m && A[p] <= A[q])) T[i++] = A[p++];
//从左半数组复制到临时空间
else T[i++] = A[q++];
//从右半数组复制到临时空间
}
for (i = x; i < y; ++i) A[i] = T[i];
//从辅助空间复制回A数组
}
}

动图演示

排序算法 —— 归并排序_递归

实战演练

#include <iostream>

using namespace std;
int A0[15] = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
int T0[15];

void merge_sort(int *A, int x, int y, int *T) {
if (y - x > 1) //等价于x+1<y,即左半部分的右边界还在右半部分的左边界左边
{
int m = x + (y - x) / 2;
//划分
int p = x, q = m, i = x;
merge_sort(A, x, m, T);
//递归求左解
merge_sort(A, m, y, T);
//递归求右解
while (p < m || q < y)
{
if (q >= y || (p < m && A[p] <= A[q])) T[i++] = A[p++];
//从左半数组复制到临时空间
else T[i++] = A[q++];
//从右半数组复制到临时空间
}
for (i = x; i < y; ++i) A[i] = T[i];
//从辅助空间复制回A数组
}
}

int main() {
for (int m = 0; m < 15; ++m) {
cout << A0[m] << ' ';
}
cout << endl;
merge_sort(A0, 0, 15, T0);
for (int n = 0; n < 15; ++n) {
cout << A0[n] << ' ';
}
cout << endl;
return 0;
}
3 44 38 5 47 15 36 26 27 2 46 4 19 50 48
2 3 4 5 15 19 26 27 36 38 44 46 47 48 50

算法精讲

首先,只要有一个序列非空,就要继续合并(while (p < m || q < y)),因此在比较时不能直接比较A[p]和A[q],因为可能其中一个序列为空,从而A[p]或者A[q]代表的是一个实际不存在的元素。

~如果第二个排序为空(此时第一个序列一定非空),复制A[p];

~否则(第二个序列非空),当且仅当第一个序列也非空,且A[p]<=A[q]时,才复制A[p]。

算法分析

归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

逆序对问题

给一列数a​1​,a​2​,……,a​n​,求它的逆序对数,即有多少个有序对(i,j),使得i<j但a​i​>a​j​。n可以高达10​6​。

分析

分治三步法:

“划分问题”:把序列分成元素个数尽量相等的两半;

“递归求解”:统计i和j均在左边或者右边的逆序对个数;

“合并问题”:统计i在左边,但j在右边的逆序对个数。

关键在于合并:如何求出i在左边,而j在右边的逆序对数目?

统计的常见技巧是“分类”:只要对于右边的每个j,统计左边比它大的元素的个数f(i),则所有f(j)之和便是答案。

归并排序可以“顺便”完成f(j)的计算:由于合并操作是从小到大进行的,当右边的A[j]复制到T中时,左边还没来得及复制到T的那些数就是左边所有比A[j]大的数,此时在累加器中加上左边元素个数m-p即可(左边所剩的元素在区间[p,m)中,因此元素个数为m-p)。

代码

#include <iostream>
using namespace std;
int cnt=0,T0[15];
int A0[15] = {3, 44, 38, 5, 47, 15, 36, 26, 27, 2, 46, 4, 19, 50, 48};
void Reverse(int *A,int x,int y,int *T)
{
if (y>x+1)
{
int m=x+(y-x)/2;
int p=x,q=m,i=x;
Reverse(A,x,m,T);
Reverse(A,m,y,T);
while (p<m||q<y)
{
if (q>=y||(p<m&&A[p]<=A[q]))
T[i++]=A[p++];
else
{
T[i++]=A[q++];
cnt+=m-p;
}
}
for (int i = x; i < y; ++i)
A[i]=T[i];
}
}
int main()
{
Reverse(A0, 0, 15, T0);
cout<<"cnt="<<cnt<<endl;
return 0;
}