归并排序在不同数据类型下的实验

 归并排序在不同数据类型下的实验

  前言:学校课程要求对不同排序算法进行分析,因对递归感到恐惧,但战胜恐惧的最好方法就是面对恐惧(奥利给!),于是我选择了归并排序算法。

  本人菜鸡一枚,若在此文中有说的不对之处,还望大佬批评指正,感激不尽!

  本文部分内容参考:https://www.cnblogs.com/penghuwan/p/7940440.html

  大家也可以去看看那位dalao写的文章,真的是非常透彻!!

(1)概念:

  归并排序是一种先通过递归,将一个大的序列数组分解为每一个单独的序列元素后,再两两合并为一个更大的序列,逐层进行,最后得到一个有序的序列的排序算法。

(2)设计思路:

  依据归并排序的概念,我们不难将归并排序分为三个部分:对左侧序列递归分解,对右侧数列递归分解,合并操作(详见图1)。同时,在递归中,只有当每一个待合并的序列都有序时,我们才能确保最后的序列也同样有序。因此,只有当递归到序列元素数量为1时,方可退出递归,开始进行合并操作。

1 if(left>=right)
2 {
3     return ;
4 }
5 int mid=left+(right-left)/2;
6 _mergeSort(arr,temparr,left,mid,assignCnt,compareCnt);   
7 _mergeSort(arr,temparr,mid+1,right,assignCnt,compareCnt);
8 merge(arr,temparr,left,mid,right,assignCnt,compareCnt);

图1. 归并排序三大板块及递归出口

  在单趟合并过程前,为了节省空间,我们事先创建了一个与原序列相同的辅助序列(详见图4),并将其传入。在单趟合并过程中,借助传入的参数,我们能够得到需要合并的范围,通过比较该范围内辅助序列中的元素并将其依次赋值给原数列,我们便能够将原序列的某一部分进行排序。通过不断合并、排序,我们最后便能够得到一个有序的序列。

(3)理论分析:

  在稳定性方面,由于在每次合并过程中,等值元素总是遵循依次放入原序列的规则,维持了等值元素在合并前后的存放顺序,因此,在理论上,归并排序是一种稳定的排序算法。在时间复杂度方面,归并排序算法是一种高效的算法,通过用分治的思想,用空间换时间。通过数学计算,我们可以发现归并排序的时间复杂度为O(nlogn)。

 1     设N个元素归并排序所需时间为T(n) 
 2     令T(1)=1
 3     则T(n)=2*T(n/2)+n    第一层
 4 
 5     T(n/2)=2*T(n/4)+n/2
 6 
 7     T(n)=4*T(n/4)+2*n     第二层
 8 
 9     T(n/4)=2*T(n/8)+n/4
10 
11     T(n)=8*T(n/8)+3*n     第三层
12 
13     .........
14 
15     T(n)=n*T(1)+log2n*n=n+n*log2n

图2、归并排序时间复杂度推导

(4)优化改进:

  1、在小规模数据时终止递归而用插入排序替代。在处理小规模数据时,归并排序由于还要开辟栈区,费时费空间,而插入排序效率更高,因此,在递归到小规模数据时利用插入排序算法替代更深层次的递归,可以减少栈的使用,在防止栈溢出的同时降低空间复杂度(详见图3)。

if (left + 10 >= right)
     {
         insertSort(arr, left, right, compareCnt, assignCnt);//小规模数据利用插入算法替代归并算法可以减少栈的使用,防止栈溢出的同时减少空间复杂度
         return;
     }

3. 小规模数据采用插入排序

  2、对完全顺序进行特判。在创建临时数组前利用bool型变量对完全顺序的数据进行特判,经过试验后发现该特判对其他数据消耗的时间几乎没有影响,而对完全顺序的数据可以大幅度改进,利大于弊(详见图4)。

template <typename T> void mergeSortPlus(T* arr, int size, ULL& compareCnt, ULL& assignCnt)
 {
     bool a = true;//对完全顺序类型数据进行特判,比较发现并不会占用其他数据类型过多时间,利大于弊
     for (int i = 0; i < size - 1; i++) {
         if (arr[i] > arr[i + 1]) { a = false; }
         compareCnt++;
     }
     if (a) { return; }

     T* temparr = new T[size];//在该阶段时创建临时数组并将原数组的元素拷贝
     for (int i = 0; i < size; i++)
     {
         temparr[i] = arr[i];
     }
     _mergeSortPlus(arr, temparr, 0, size - 1, compareCnt, assignCnt);

     if (temparr != NULL) delete[] temparr;//释放内存
     temparr = NULL;
 }

图4完全顺序数据采用插入排序

  3、在递归过程中通过交换原序列与辅助数组参数的传入,去除单趟合并中对原数组到辅助数组的拷贝。因为mergeSortPlus和_mergePlus的参数顺序是相同的, 所以,无论递归过程中辅助数组和原数组的角色如何替换,对最后一次调用的_mergePlus而言,,最终被排为有序的都是原数组,而不是辅助数组(详见图5)!举个例子,假设要进入下一层递归,我们需要交换辅助序列和原序列,而下一层递归刚好触发了结束条件,于是return了,因此返回上一层递归,而在返回时,原序列和辅助序列自然也就再次交换回来,同理可得多层递归。因此,不论你交换了多少次,最后的递归打开后的参数只与最外层的参数相匹配。

ps:采用了这个优化方案会导致另一个if判断的优化方案失效(详见图5)

 template <typename T> void _mergeSortPlus(T* arr, T* temparr, int left, int right, ULL& compareCnt, ULL& assignCnt)
 { 
    if (left + 10 >= right)
     {
         insertSort(arr, left, right, compareCnt, assignCnt);
         return;
     }
     int mid = left + (right - left) / 2;
     _mergeSortPlus(temparr, arr, left, mid, compareCnt, assignCnt);      //交换临时数组和原数组节省拷贝时间
     _mergeSortPlus(temparr, arr, mid + 1, right, compareCnt, assignCnt);

     //if(temparr[mid]<=temparr[mid+1]&&arr[mid]<=arr[mid+1]) return;//若左右都已排序并左侧均比右侧小,则视为已排序,跳过合并步骤
     //ps:若使用临时数组互换的操作则if操作失效,因为不知道排序完成的是那个数组,而若另外添加函数来进行判断,则会耗费更多的时间与空间,综合比较后,决定取消此操作。
     _mergePlus(arr, temparr, left, mid, right, compareCnt, assignCnt);   //因为外部_mergeSortPlus和_mergePlus的参数顺序是相同的, 所以,无论递归过程中辅助数组和
                                                                          //原数组的角色如何替换,对最后一次调用的_mergePlus而言,,最终被排为有序的都是原数组,而不是辅助数组!  
 }

5. 交换参数去除辅助数组的拷贝

(5)实际运行&分析:

  经过分析发现,经过了上述优化后,归并排序的效率平均提升了大约40%,而在对完全顺序类型的数据进行排序时,由于特判的存在,优化率达到惊人的100%(详见表1)。同时,我们发现,在优化前后的归并排序算法中,归并排序对完全逆序数据的处理效率要超过正态分布和均匀分布数据,并且赋值和比较的次数也小于他们两个。并且,在数据数量不断增大时,归并排序也展现出了优异的处理能力,并没有发生随着数据量增大消耗时间呈几何倍增长的趋势(详见表1、2)。而在稳定性方面,归并排序也正如之前理论分析的那样,呈现出了稳定的特征,未找到不稳定的特例(详见表3)。

  由以上的发现,我们可以知道:归并排序适合应用于处理大数据或需要确保数据的稳定性的场景,在面对“混乱度”较低的数据时,效率会更高。

表1. 优化前后整形数据排序时间(Release配置版)(秒)

数据类型:

正态分布

均匀分布

    

优化前

优化后

优化前

优化后

1024

0.001

0

0

0

2048

0

0

0

0

4096

0

0

0.001

0

8192

0.001

0

0.001

0

16384

0

0.001

0.001

0.002

32768

0.003

0.002

0.003

0.002

65536

0.006

0.004

0.005

0.003

数据类型:

完全顺序

完全逆序

 

优化前

优化后

优化前

优化后

1024

0

0

0

0

2048

0

0

0

0

4096

0

0

0

0

8192

0

0

0.001

0

16384

0.002

0

0.001

0

32768

0.002

0

0.002

0.001

65536

0.003

0

0.004

0.002

 

表2. 整形数据比较和赋值操作次数(Release配置版)(次)(以优化后为例)

数据类型:

正态分布

均匀分布

 

比较次数

赋值次数

比较次数

赋值次数

1024

25123

23752

25074

23686

2048

56987

50952

56914

50974

4096

120796

115325

120691

115404

8192

268877

244793

268395

244926

16384

565905

543776

564083

543313

32768

1239629

1143273

1236229

1142694

65536

2590324

2502024

2584262

2501654

数据类型:

完全顺序

完全逆序

 

比较次数

赋值次数

比较次数

赋值次数

1024

13823

12800

21887

23552

2048

31743

26624

48895

50176

4096

65535

61440

101887

112640

8192

147455

126976

224255

237568

16384

303103

286720

464895

524288

32768

671743

589824

1011711

1097728

65536

1376255

1310720

2088959

2392064

表3. 从结构体数据看稳定性65536大小(Release配置版)(分)

倒序输出前十名数据

 学 号   总分 语  数  外  理  化

00051320 413  88  84  72  84  85

00058292 412  74  86  84  85  83

00047306 411  84  86  82  77  82

00027406 411  77  93  71  86  84

00039516 409  75  93  76  78  87

00034325 409  81  76  90  82  80

00001693 408  73  85  85  86  79

00062585 407  90  76  80  78  83

00055381 407  76  83  77  79  92

00053188 407  73  86  78  78  92

比较次数:1521734    赋值次数:1540800

 (6)不足与改进:

  1、在插入排序时,若采用用二分插入排序或是更好的优化方法,可以使运行速度更快。

  2、关于完全逆序数据类型为什么运行时间比正态分布和均匀分布数据类型短这一问题仍在思考。

  ......

  还有很多毛病,希望大佬帮忙批评指正!

源码:

 typedef unsigned long long ULL;
//优化后-------------------------------------------------------------------------------------------------------------------------------------------------
 template <typename T>  void _mergePlus(T* arr, T* temparr, int left, int mid, int right, ULL& compareCnt, ULL& assignCnt)
 {
     int i = left;
     int j = mid + 1;
     //此处的临时数组拷贝,通过在递归过程中交换临时数组和原数组得以省略,节约时间
     for (int k = left; k <= right; k++) {
         if (i > mid) {
             arr[k] = temparr[j++]; // 左侧用完,把右侧依次放入
         }
         else if (j > right) {
             arr[k] = temparr[i++]; // 右侧用完,把左侧依次放入
         }
         else if (temparr[j] < temparr[i]) {
             arr[k] = temparr[j++]; // 右边小于左边,把右侧元素放入
             compareCnt++;
         }
         else {
             arr[k] = temparr[i++]; // 左边小于右边,把左侧元素放入
             compareCnt++;
         }
         assignCnt++;
     }
 }
//拆分,需要在调用前创建一个大小与arr相同的空数组,用来零时存放
 template <typename T> void _mergeSortPlus(T* arr, T* temparr, int left, int right, ULL& compareCnt, ULL& assignCnt)
 {
     if (left + 10 >= right)
     {
         insertSort(arr, left, right, compareCnt, assignCnt);          //小规模数据利用插入算法替代归并算法可以减少栈的使用,防止栈溢出的同时减少空间复杂度
         return;
     }
     int mid = left + (right - left) / 2;
     _mergeSortPlus(temparr, arr, left, mid, compareCnt, assignCnt);      //交换临时数组和原数组节省拷贝时间
     _mergeSortPlus(temparr, arr, mid + 1, right, compareCnt, assignCnt);

     //if(temparr[mid]<=temparr[mid+1]&&arr[mid]<=arr[mid+1]) return;     //若左右都已排序并左侧均比右侧小,则视为已排序,跳过合并步骤
     //ps:若使用临时数组互换的操作则if操作失效,因为不知道排序完成的是那个数组,而若另外添加函数来进行判断,则会耗费更多的时间与空间,综合比较后,决定取消此操作。


     _mergePlus(arr, temparr, left, mid, right, compareCnt, assignCnt);    //因为外部_mergeSortPlus和_mergePlus的参数顺序是相同的, 所以,无论递归过程中辅助数组和
                                                                         //原数组的角色如何替换,对最后一次调用的_mergePlus而言,,最终被排为有序的都是原数组,而不是辅助数组!  
 }
 //调用函数
 template <typename T> void mergeSortPlus(T* arr, int size, ULL& compareCnt, ULL& assignCnt)
 {
     bool a = true;//对完全顺序类型数据进行特判,比较发现并不会占用其他数据类型过多时间,利大于弊
     for (int i = 0; i < size - 1; i++) {
         if (arr[i] > arr[i + 1]) { a = false; }
         compareCnt++;
     }
     if (a) { return; }
     
     T* temparr = new T[size];
     for (int i = 0; i < size; i++)//在该阶段时创建临时数组并将原数组的元素拷贝
     {
         temparr[i] = arr[i];
     }
     _mergeSortPlus(arr, temparr, 0, size - 1, compareCnt, assignCnt);
     if (temparr != NULL) delete[] temparr; //释放内存
     temparr = NULL;
 }



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值