归并排序
概述
归并算法采用经典的分治(divide-and-conquer)策略,将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
思想
先对只有单个元素的子序列通过两两合并(或两个以上)的方式进行排序,形成一个长度稍大一些的有序序列;
然后在此基础上,对两个长度稍大一些的有序序列再进行两两合并,形成一个长度更大的有序序列,重复此动作,直到覆盖整个数组的大小为止。
举例
对于数组[48 34 60 80 75 12 26 48]
单趟排序的实现分析
单趟排序的目的:
在数组arr[low...mid]有序和arr[mid...high]有序的前提下,使数组arr[low...high]有序。
单趟归并的过程如下:
准备:
排序前创建有一个和原数组a长度相同的辅助数组aux。
辅助数组aux的任务有两项:比较元素大小, 并在aux中逐个取得有序的元素放入原数组a中 (通过1使aux和a在low-high的位置是完全相同的!这是实现的基础)
过程:
-
首先将原数组中的待排序序列拷贝进辅助数组的相同位置中,即将a[low...high]拷贝进aux[low...high]中
-
因为aux[low...high]由两段有序的序列:aux[low...mid]和aux[mid...high]组成, 这里称之为aux1和aux2,
-
我们要做的就是从aux1和aux2的头部元素开始,比较双方元素的大小。将较小的元素放入原数组a中,并取得较小元素的下一个元素位置index;重复多次直到其中一个数组存放完毕。
(因为前提是aux1和aux2都是有序的,所以通过这种方法我们能得到更长的有序序列) -
如果aux的两段序列中,其中一段中的所有元素都已"比较"完了, 取得另一段序列中剩下的元素,全部放入原数组a的剩余位置。
- 对于3步骤的具体实现:
-
- 设置两个游标变量,i 和 j,开始时分别指向aux[low]和aux[mid];分别代表左游标和右游标。
-
- 设置游标k用于确定在a中放置元素的位置(在a中进行),k在开始时候指向a[low]
-
- 如果aux1[i]<aux[j];则a[k]=aux1[i]; k++;i++;
-
- 如果aux1[i]>aux[j];则a[k]=aux2[i]; k++;j++;
代码1:
代码1和2效率都一样,用哪个,看个人习惯
/**
* @description: 完成一趟合并
* @param a 输入数组
* @param low,mid,high a[L...R] 是待排序序列, 其中a[L...mid]和 a[mid+1...R]已有序
*/
int aux[1000000];
void merge(int a[],int L,int mid,int R){
for(int k=L;k<=R;k++){
aux[k]=a[k];// 将待排序序列a[low...high]拷贝到辅助数组的相同位置
}
int i=L; // 游标i,开始时指向待排序序列中左半边的头元素
int j=mid+1;// 游标j,开始时指向待排序序列中右半边的头元素
int k=L;
while(i<=mid&&j<=R){
if(aux[i]<=aux[j]) a[k++]=aux[i++];
// 右半边当前元素大于等于左半边当前元素,取左半边元素
else a[k++]=aux[j++];
// 右半边当前元素小于左半边当前元素, 取右半边元素
}
while(i<=mid){
a[k++]=aux[i++]; // 左半边没用尽
}
while(j<=R){
a[k++]=aux[j++];// 右半边没用尽
}
}
代码2:
/**
* @description: 完成一趟合并
* @param a 输入数组
* @param low,mid,high a[low...high] 是待排序序列, 其中a[low...mid]和 a[mid+1...high]已有序
*/
int aux[1000000];
void merge (int a [],int low,int mid,int high) {
for(int k=low;k<=high;k++){
aux[k] = a[k]; // 将待排序序列a[low...high]拷贝到辅助数组的相同位置
}
int i = low; // 游标i,开始时指向待排序序列中左半边的头元素
int j = mid+1; // 游标j,开始时指向待排序序列中右半边的头元素
for(int k=low;k<=high;k++){
if(i>mid){
a[k] = aux[j++]; // 左半边用尽
}else if(j>high){
a[k] = aux[i++]; // 右半边用尽
}else if(aux[j]<aux[i]){
a[k] = aux[j++]; // 右半边当前元素小于左半边当前元素, 取右半边元素
}else {
a[k] = aux[i++]; // 右半边当前元素大于等于左半边当前元素,取左半边元素
}
}
}
}
基于递归的归并排序(自顶向下)
最关键的是merge_sort(int a [], int low,int high)方法里面的三行代码:
merge_sort(a,low,mid); //对左半边序列递归
merge_sort(a,mid+1,high);//对右半边序列递归
merge(a,low,mid,high);//、单趟合并操作。
递归归并的轨迹图像
代码
#include <iostream>
using namespace std;
int a[1000000];
int aux[1000000];
/**
* @description: 完成一趟合并
* @param a 输入数组
* @param low,mid,high a[low...high] 是待排序序列, 其中a[low...mid]和 a[mid+1...high]已有序
*/
void merge(int a[], int low, int mid, int high)
{
for (int k = low; k <= high; k++)
{
aux[k] = a[k]; // 将待排序序列a[low...high]拷贝到辅助数组的相同位置
}
int i = low;
// 游标i,开始时指向待排序序列中左半边的头元素
int j = mid + 1;
// 游标j,开始时指向待排序序列中右半边的头元素
for (int k = low; k <= high; k++)
{
if (i > mid)
{
a[k] = aux[j++]; // 左半边用尽
}
else if (j > high)
{
a[k] = aux[i++]; // 右半边用尽
}
else if (aux[j] < aux[i])
{
a[k] = aux[j++]; // 右半边当前元素小于左半边当前元素, 取右半边元素
}
else
{
a[k] = aux[i++]; // 右半边当前元素大于等于左半边当前元素,取左半边元素
}
}
}
/**
* @description: 基于递归的归并排序算法
*/
void merge_sort(int arr[], int L, int R)
{
if (L >= R)
return;// 终止递归的条件
int mid = (R + L) / 2;// 取得序列中间的元素
merge_sort(arr, L, mid);// 对左半边递归
merge_sort(arr, mid + 1, R); // 对右半边递归
merge(arr, L, mid, R);// 单趟合并
}
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
merge_sort(a, 0, n - 1);
for (int i = 0; i < n; i++)
cout << a[i] << " ";
return 0;
}
改进
基于循环的归并排序(自底向上)
思路:
手动定义归并函数的大小,第一次size=1,第二次size=2......。思路简单,直接看代码吧
循环归并的轨迹图像
代码
int aux [];
void sort(int a []){
int N = a.length;
aux = new int [N];
for (int size =1; size<N;size = size+size){
for(int low =0;low<N-size;low+=size+size) {
merge(a,low,low+size-1,Math.min(low+size+size-1,N-1));
}
}
}
性能分析
名称 | 时间复杂度平均情况 | 空间复杂度 | 稳定性 | 复杂性 |
---|---|---|---|---|
归并排序 | O(nlog2n) | O(n) | 稳定 | 较复杂 |
时间复杂度
归并排序的形式就是一棵二叉树,它需要遍历的次数就是二叉树的深度,而根据完全二叉树的可以得出它的时间复杂度是O(n * log2n)。
空间复杂度
由前面的算法说明可知,算法处理过程中,需要一个大小为n的临时存储空间用以保存合并序列。
算法稳定性
在归并排序中,相等的元素的顺序不会改变,所以它是稳定的算法。(不是原地排序)
归并排序和堆排序、快速排序的比较
若从空间复杂度来考虑:首选堆排序,其次是快速排序,最后是归并排序。
若从稳定性来考虑,应选取归并排序,因为堆排序和快速排序都是不稳定的。
若从平均情况下的排序速度考虑,应该选择快速排序。
应用
求逆数对
归并排序时,
如果第一个数组的第i号元素,大于等于第二个数组的第j个元素,
那么第一个数组的i后面的待排序的所有元素都与j元素形成逆序对。
因为第一个数组i后面的元素都比这个元素大,而且下标都比这个元素的下标小
也就是ans+=(mid-i)+1;
代码:
#include <iostream>
using namespace std;
int a[1000000];
int aux[1000000];
long long ans = 0;
void merge(int a[], int L, int mid, int R)
{
for (int k = L; k <= R; k++)
{
aux[k] = a[k];
}
int i = L;
int j = mid + 1;
int k = L;
while (i <= mid && j <= R)
{
if (aux[i] <= aux[j])
{
a[k++] = aux[i++];
}
else
{
a[k++] = aux[j++];
/**********************************/
ans+=mid-i+1;
/**********************************/
}
}
while (i <= mid)
{
a[k++] = aux[i++];
}
while (j <= R)
{
a[k++] = aux[j++];
}
}
void merge_sort(int arr[], int L, int R)
{
if (L == R)
return;
int mid = (L + R) / 2;
merge_sort(arr, L, mid);
merge_sort(arr, mid + 1, R);
merge(arr, L, mid, R);
}
int main()
{
int n;
cin >> n;
for (int i = 0; i < n; i++)
cin >> a[i];
merge_sort(a, 0, n - 1);
cout << ans << endl;
return 0;
}