归并排序

原文

概述

归并算法采用经典的分治(divide-and-conquer)策略,将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
image

思想
先对只有单个元素的子序列通过两两合并(或两个以上)的方式进行排序,形成一个长度稍大一些的有序序列;
然后在此基础上,对两个长度稍大一些的有序序列再进行两两合并,形成一个长度更大的有序序列,重复此动作,直到覆盖整个数组的大小为止。
举例
对于数组[48 34 60 80 75 12 26 48]
image

单趟排序的实现分析

单趟排序的目的:
在数组arr[low...mid]有序和arr[mid...high]有序的前提下,使数组arr[low...high]有序。

单趟归并的过程如下:
准备:
排序前创建有一个和原数组a长度相同的辅助数组aux。

辅助数组aux的任务有两项:比较元素大小, 并在aux中逐个取得有序的元素放入原数组a中 (通过1使aux和a在low-high的位置是完全相同的!这是实现的基础)

过程:

  1. 首先将原数组中的待排序序列拷贝进辅助数组的相同位置中,即将a[low...high]拷贝进aux[low...high]中

  2. 因为aux[low...high]由两段有序的序列:aux[low...mid]和aux[mid...high]组成, 这里称之为aux1和aux2,

  3. 我们要做的就是从aux1和aux2的头部元素开始,比较双方元素的大小。将较小的元素放入原数组a中,并取得较小元素的下一个元素位置index;重复多次直到其中一个数组存放完毕。
    (因为前提是aux1和aux2都是有序的,所以通过这种方法我们能得到更长的有序序列)

  4. 如果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++]; // 右半边当前元素大于等于左半边当前元素,取左半边元素
      }
    }
  }
}

基于递归的归并排序(自顶向下)

image

最关键的是merge_sort(int a [], int low,int high)方法里面的三行代码:

merge_sort(a,low,mid); //对左半边序列递归
merge_sort(a,mid+1,high);//对右半边序列递归
merge(a,low,mid,high);//、单趟合并操作。

递归归并的轨迹图像
image
image

代码

#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......。思路简单,直接看代码吧
循环归并的轨迹图像
image

代码

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;
}

posted @ 2021-12-10 22:23  kingwzun  阅读(101)  评论(0编辑  收藏  举报