归并排序 详解

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(N),归并排序需要一个与原数组相同长度的数组做辅助来排序
  • 稳定性:归并排序是稳定的排序算法,temp[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];这行代码可以保证当左右两部分的值相等的时候,先复制左边的值,这样可以保证值相等的时候两个元素的相对位置不变。

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

从下图可以看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。

图示过程

(1) 归并排序的流程

(2) 合并两个有序数组的流程

 

归并排序有两种实现方式: 基于递归的归并排序和基于循环的归并排序。(也叫自顶向下的归并排序和自底向上的归并排序)

这两种归并算法虽然实现方式不同,但还是有共同之处的:

1. 无论是基于递归还是循环的归并排序, 它们调用的核心方法都是相同的:完成一趟合并的算法,即两个已经有序的数组序列合并成一个更大的有序数组序列 (前提是两个原序列都是有序的!)

2. 从排序轨迹上看,合并序列的长度都是从小(一个元素)到大(整个数组)增长的

C++

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

void MergeSort (int arr [], int low,int high) {
    if(low>=high) { return; } // 终止递归的条件,子序列长度为1
    int mid =  low + (high - low)/2;  // 取得序列中间的元素
    MergeSort(arr,low,mid);  // 对左半边递归
    MergeSort(arr,mid+1,high);  // 对右半边递归
    merge(arr,low,mid,high);  // 合并
  }


void merge(int arr[],int low,int mid,int high){
    //low为第1有序区的第1个元素,i指向第1个元素, mid为第1有序区的最后1个元素
    int i=low, j=mid+1, k=0;  //mid+1为第2有序区第1个元素,j指向第1个元素
    int *temp=new int[high-low+1]; //temp数组暂存合并的有序序列
    while(i<=mid&&j<=high){
        if(arr[i]<=arr[j]) //较小的先存入temp中
            temp[k++]=arr[i++];
        else
            temp[k++]=arr[j++];
    }
    while(i<=mid)//若比较完之后,第一个有序区仍有剩余,则直接复制到t数组中
        temp[k++]=arr[i++];
    while(j<=high)//同上
        temp[k++]=arr[j++];
    for(i=low,k=0;i<=high;i++,k++)//将排好序的存回arr中low到high这区间
      arr[i]=temp[k];
    delete []temp;//释放内存,由于指向的是数组,必须用delete []
}

基于递归归并排序的优化方法

优化一:对小规模子数组使用插入排序

用不同的方法处理小规模问题能改进大多数递归算法的性能,因为递归会使小规模问题中方法调用太过频繁,所以改进对它们的处理方法就能改进整个算法。因为插入排序非常简单, 因此一般来说在小数组上比归并排序更快。 这种优化能使归并排序的运行时间缩短10%到15%。

怎么切换呢?只要把作为停止递归条件的

  if(low>=high) { return; }

改成

 if(high - low <= 10) { // 数组长度小于10的时候
      InsertSort(int arr[], int low,int high) // 切换到插入排序
      return;
 }

就可以了,这样的话,这条语句就具有了两个功能:

1. 在适当时候终止递归

2. 当数组长度小于M的时候(high-low <= M), 不进行归并排序,而进行插排

 

优化二: 测试待排序序列中左右半边是否已有序

通过测试待排序序列中左右半边是否已经有序, 在有序的情况下避免合并方法的调用。

因为a[low...mid]和a[mid...high]本来就是有序的,存在a[low]<a[low+1]...<a[mid]和a[mid+1]<a[mid+2]...< a[high]这两种关系, 如果判断出a[mid]<=a[mid+1]的话,我们就认为数组已经是有序的并跳过merge() 方法。

void sort (int a[], int low,int high) {
    if(low>=high) {
      return;
    } // 终止递归的条件
    int mid =  low + (high - low)/2;  // 取得序列中间的元素
    sort(a,low,mid);  // 对左半边递归
    sort(a,mid+1,high);  // 对右半边递归
    if(a[mid]<=a[mid+1]) return; // 避免不必要的归并
    merge(a,low,mid,high);  // 单趟合并
  }

优化三:去除原数组序列到辅助数组的拷贝

在上面介绍的基于递归的归并排序的代码中, 我们在每次调用merge方法时候,我们都把arr对应的序列拷贝到辅助数组temp中去。

递归调用的每个层次交换输入数组和输出数组的角色,从而不断地把输入数组排序到辅助数组,再将数据从辅助数组排序到输入数组,节省数组复制的时间。

注意, 外部的sort方法和内部sort方法接收的a和aux参数刚好是相反的

在这里我们做了两个操作:

  • 在排序前拷贝一个和原数组元素完全一样的辅助数组(不再是创建一个空数组了!)
  • 在递归调用的每个层次交换输入数组和输出数组的角色

因为外部sort和merge的参数顺序是相同的,所以,无论递归过程中辅助数组和原数组的角色如何替换,对最后一次调用的merge而言(将整个数组左右半边合为有序的操作), 最终被排为有序的都是原数组,而不是辅助数组!

void MergeSortCore(int* data, int* copy, int first, int last);
void MergeSort(int* data, int length)
{
    if(data == NULL || length <= 0) return;
 
    int* copy = new int[length];
    for(int i = 0; i < length; i++)
        copy[i] = data[i];
 
    MergeSortCore(data, copy, 0, length - 1);
 
    for(int i = 0; i < length; i++)
        data[i] = copy[i];
    delete[] copy;
    return;
}
 
void MergeSortCore(int* data, int* copy, int first, int last)
{
    if(first == last)
        copy[first] = data[first];
    else
    {
        int mid = first + (last - first)/2;
        MergeSortCore(copy, data, first, mid);
        MergeSortCore(copy, data, mid + 1, last);
 
        //merge    
        int i = first, j = mid + 1;
        int k = first;
        while(i <= mid && j <= last)
        {
            if(data[i] <= data[j]) copy[k++] = data[i++];
            else copy[k++] = data[j++];
        }
 
        while(i <= mid) copy[k++] = data[i++];
        while(j <= last) copy[k++] = data[j++];
    }
}

注意:优化结果虽然差不多,但是当其数组接近有序的时候,速度有了可观的提升。

二、基于循环的归并排序(自底向上)

void sort(int a []){
    int N = a.size();
    for (int size = 1; size < N; size *= 2){
      //  low+size=mid+1,为第二个分区第一个元素,它 < N,确保最后一次合并有2个区间
      for(int low = 0; low + size < N;low += 2 * size) {
        mid = low + size - 1;
        high = low + 2 * size - 1;
        if(high > N-1) high = N - 1;
        merge(a,low, mid, high);
      }
    }
  }

 

参考

原文地址:https://www.cnblogs.com/shona/p/12466353.html