归并排序

简介: 归并排序

mergeSort

应用分治法思想,有两种实现方法:

  1. 自顶向下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法)
  2. 自底向上的迭代

算法步骤

UpDown

通过二分法达到log(n)这样一个层级,然后再通过O(n)的归并操作,就形成了nlog(n)的归并排序

  1. 二分操作:将n个元素平均分成两部分,再分别将这两部分进行递归二分操作。直到所有分组都只有1个元素(只有一个元素时,数列显然有序)

  2. 归并操作:从最底层开始逐步合并两个排好序的数列
    我这里采用对原数组进行操作,所以先建立一个辅助数组T temp[r-l+1],通过for循环将原数组数据复制到辅助数组
    又因为两个序列都已经有序,因此只需要两个序列的低位轮流进行比较,输的一方为小值,将这个值赋值到原数组,输的一方继续拿出一个值来比较,直到有一方没有元素后,将另一方的所有元素依次赋值到原数组中即可。此时,原数组为两个序列的有序合并。

下面两个子数组分别进行比较,将合适的元素赋值到上面的大数组

BottomUp

自底向上进行归并操作

  1. size是归并子数组的长度,从1开始直到n,归并一次,size扩大一倍
  2. 从下标i开始,对arr[i,i+size-1]和arr[i+size,i+2*size-1]进行合并(两个子数组中对应的坐标相差size),下轮循环开始是从i+2*size

自顶向下的递归实现

#include <iostream>

#include "insertionSort.h"

usingnamespacestd;

//归并操作

template<typenameT>

void__merge(Tarr[],intleft,intmid,intright){

   Ttemp[right-left+1];//临时数组

   for(inti=left;i<=right;i++)

       temp[i-left]=arr[i];

   //临时数组中的左右指针(左指针是第一个元素,右指针从内部看来也是右部第一个元素,但从全局上看,就是中值+1)

   intcurL=left,curR=mid+1;

   //k是要原数组中即将要赋值的坐标

   for(intk=left;k<=right;k++){

       //这里四选一,所以要在一个if-else体系中,否则就会多次赋值

       //越界判断

       if(curL>mid){

           arr[k]=temp[curR-left];

           curR++;

       }

       elseif(curR>right){

           arr[k]=temp[curL-left];

           curL++;

       }

       //左右低位轮流PK

       //右值为小值,加入到arr中,右边再拿出一个值来比较

       elseif(temp[curL-left]>temp[curR-left]){

           arr[k]=temp[curR-left];

           curR++;

       }else { //左值小于等于右值,左值加入arr中,左边再拿出一个值来比较(b)

           arr[k]=temp[curL-left];

           curL++;

       }

   }

}

//对数组中arr[left]至arr[right]进行归并排序

template<typenameT>

void__mergeSort(Tarr[],intleft,intright){

   

   //1. 二分操作  

   //递归出口:处理的数据集为空

   if(left>=right)

       return ;

   intmid=(left+right)/2;

   __mergeSort(arr,left,mid);

   __mergeSort(arr,mid+1,right);

   

   //2. 归并操作

   __merge(arr,left,mid,right);

}

//归并排序

template<typenameT>

voidmergeSort(Tarr[],intn){

   //注意这里,数组范围是前闭后闭的

   __mergeSort(arr,0,n-1);

   

}

intmain(){

   intn=100000;

   //生成乱序数组

   int*arr=SortTestHelper::generateRandomArray(n,0,n);

   int*arr2=SortTestHelper::copyIntArray(arr,n);

   //测试耗时

   //SortTestHelper::testSort("insertionSort",insertionSort,arr,n);

   SortTestHelper::testSort("mergeSort",mergeSort,arr2,n);

   

   //释放空间

   delete[] arr;

   delete[] arr2;

   return0;

}

优化1:归并操作前的判断

当数组是近乎有序时,归并排序是弱于插入排序的时间复杂度的,可以在归并操作之前对数组进行是否有序判断

//归并操作

   //左边的末尾大于右边首元素才会进行归并操作,如果是小于等于,则说明已经有序,不再进行归并操作

   if(arr[mid]>arr[mid+1])

       __merge(arr,left,mid,right);

当我们要处理的元素中可能出现有序的情况,可以使用上述代码优化

优化2:穿插使用插入排序

对于所有高级排序算法,都可以这么进行优化:

快要递归到底时,可以使用插入排序,当递归到数组数量非常小的时候,可以改为使用插入排序来提高性能

  1. 数组数量较小时,有序的概率较大
  2. 虽然插入排序最差时间复杂度为O(n^2),归并排序时间复杂度为O(nlogn),但是时间复杂度前面是有一个常数的系数的,插入排序的系数比归并排序的系数小,也就是说,当n小到一定程度时,插入排序是要比归并排序要快的

   //二分操作

   //处理数据集为空

//  if(left>=right)

//      return ;

   

   //当数组元素<=15时,更换为插入排序

   if((right-left)<=15){

       insertionSort(arr ,left ,right);

       return ;

   }

需要在insertionSort.h中重载insertionSort函数

//对arr中[l,r]范围内的元素进行插入排序

template<typenameT>

voidinsertionSort(Tarr[],intleft,intright){

   for(inti=left+1;i<=right;i++){

       intj;

       Te=arr[i];

       //注意这里是j>left,而不是j>0

       for(j=i;j>left&&arr[j-1]>e;j--)

               arr[j]=arr[j-1];

       arr[j]=e;

   }

}

test

n=500000

//原始代码

mergeSort_TopDown : 0.173s

//使用优化1

mergeSort_TopDown : 0.158s

//使用优化2

mergeSort_TopDown : 0.144s

//使用优化1&2

mergeSort_TopDown : 0.119s

优化效果一目了然

自底向上的递推实现

//自底向上的归并排序

template<typenameT>

voidmergeSort_BottomUp(Tarr[],intn){

   //size表示归并子数组的长度,归并之后的归并子数组长度要变为原来的2倍,因此每次循环结束size=size*2

   for(intsize=1;size<=n;size=size*2){

       //两个要进行归并的子数组间隔为size,对arr[i,i+size-1]和arr[i+size,i+2*size-1]进行归并,归并之后下次开始是arr[i+2*size],因此每轮循环结束后i=i+2*size

       //第一轮对[0,size-1]和[size,2*size-1]进行归并,第二轮对[2*size,3*size-1]和[3*size,4*size-1]进行归并

       

       //界限处理:

       //1. 因为只有存在两个子数组才能进行归并,那么要求i+size<n,保证了第二个子数组中arr[i+size]存在的合法性,并且避免了第一个子数组中arr[i+size-1]越界

       //2. 遍历到后面,第二个子数组长度可能小于size,那么(2*size-1)>(n-1)造成越界,将其和(n-1)取最小值即可,min(i+2*size-1,n-1)

       for(inti=0;i+size<n;i=i+2*size){

           __merge(arr,i,i+size-1,min(i+2*size-1,n-1));

       }

   }

}

自底向上没有使用数组的重要特性:根据索引获取元素

正因如此,可以非常好的使用O(nlogn)的时间对链表这样的数据结构进行归并排序


目录
相关文章
|
7月前
归并排序详解
归并排序详解
67 1
|
28天前
归并排序
归并排序。
20 2
|
6月前
|
算法 搜索推荐 Java
归并排序就是这么容易
归并排序就是这么容易
42 2
|
7月前
|
存储 算法 搜索推荐
C++归并排序的实现
C++归并排序的实现
|
7月前
|
搜索推荐
归并排序是什么
归并排序是什么
20 归并排序
20 归并排序
44 0
|
搜索推荐 算法 C#
C#——归并排序
C#——归并排序
156 0
|
算法
归并排序及小和问题(中)
归并排序及小和问题(中)
163 1
归并排序及小和问题(中)
|
算法
【2. 归并排序】
归并与快排不同: >快速排序: >- 分界点是随机数组里面的一个`数`来分,使得左边都是<= 这个数,右边 >= 这个数 (`数值`) >- 先分完,在分别递归俩边 > >归并排序: >- 先递归俩边,在进行合并操作 >- 分界点是`整个数组中间的位置`(`下标值`)
99 0
【2. 归并排序】