排序算法学习整理四(归并)

  好久好久没有更过这种博客了,原本打算是大一下就把排序算法学完全部,顺便更新完博客的。但是被骗去硬件了(我这个蒟蒻,捧着一块15,还在傻傻的乐呵つ﹏⊂)。一拖就给忘了,果然脑子不太好使。

之所以,今天想起写排序算法的博客,是因为再PAT上遇到了一道@#%!的题目(点刚刚地方转跳),用到了插入和归并,写这道题的翻到了自己的插入排序的博客😂。

 

  归入正题,这次讲的是归并排序。归并排序呢是一个非常有意思的排序算法,它与前三种排序不太一样,往常的排序都是,两个循环控制整个数组,进行为序调整,平均时间复杂度为T(n) = O(nlogn)。而归并排序呢,为了彰显自己非同凡响的思路,ta先将数组在逻辑上切成了n部分(n>1),然后一块块的处理,并且ta 的空间复杂度为O(N)。整个思想呢就是大名鼎鼎的 “分治” ,切就是 “分”, 处理就是 “治”。(可能这么说不太准确,接着会有比较官方的定义)顺便科普一下分治:分治法将问题 (divide) 成一些小的问题然后递归求解,而 治(conquer)则将分的阶段得到的各答案"修补"在一起,即分而治之

  来自百度百科的对归并排序的说明:

    归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个         子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。归并排序是一种稳定的排序方法。

 

我个人对上面这段话的解读如下:

  1、归并排序的基础是归并操作(《数据结构》中的链表归并操作)

  2、归并排序采用了分治法

  3、归并排序多次将数组划分为等长的子序列(最大容差为1),并多次归并子序列

  4、归并排序可以有n路归并,书中lef + (right - left) / 2 仅仅是归并排序 n = 2 的一种特殊情况,当然归并n表的函数需要重写。

瞎叨叨这么多,不如看张图来体会一下归并函数是如何排序的

(先来张动态的,如有侵权,请在下方评论)

  

再来张静态的

(图片l来自:https://www.cnblogs.com/chengxiao/p/6194356.html)

  从图中,我们可以清晰的看到,和我们刚刚说的那样。先把数组分成n个等长的子序列,然后一次次的归并(这是二路归并)。很明显每一个长度大于1的子序列都可以作为被分割的序列,继续分割,很明显的递归思想。(其实我特别想说,特像一棵完全二叉树,肯定可以递归实现)递归深度为log2n。动态图展现的思路是错的,但是递归的过程的确如动态图所示。

  归并排序的核心:“分”和“治” 该如何实现呢?

我们先来分析一下“分”

  因为是二路分治,所以每次取数组中间部分。设n为数组长度,我们来推导一下:

    第一次:mid1 = 0 + (n - 0) / 2;

        左边 :0 ~ mid1 ;右边:mid1 + 1 ~ n-1;

    第二次:完了,分成两半了,凉凉,不然,先考虑一边的情况

        mid2 = 0 + (mid1 - 0) / 2;

        左边 :0 ~ mid2  ;右边:mid2  + 1 ~ mid1 ;

    第三次:mid3 = 0 + (mid2 - 0) /2;(同样,先考虑左边)

        左边:0 ~ mid3  ;右边:mid3  + 1 ~  mid2

    ……

  嗯~ o(* ̄▽ ̄*)o,感觉没什么说服力ヽ(*。>Д<)o゜,来看张图吧

    

  不难发现我们的对半的规律是 (子序列的终止地址 - 子序列的起始地址) / 2,那么咱们把开始地址定义为left ,终止边界定义为right,很显然,求mid的公式为就如图中所示

mid = left +(right -left)/ 2;

  

  那么递归的终止条件呢?让我们来想想归并排序子序列可分的基本要求是什么?

    子序列长度大于1,(总不能把一个元素砍成一个个字节来玩吧)。

  那么这又意味着什么呢,left - right > 0; 即 left > right

 

  所以得出的 “分” 这部分的代码为:

 1 void mergeSort(int arr[], int left, int right, int *temp)
 2 { //实现“分”
 3     if (left >= right)
 4     {//停止条件
 5         return;
 6     }
 7 
 8     int mid = left + (right - left) / 2;
 9     mergeSort(arr, left, mid, temp);      //从left ~ mid
10     mergeSort(arr, mid + 1, right, temp);   //从mid+1 ~ right
11     mergeAdd(arr, left, mid, right, temp);    //从这里是 “治” 的函数,分完就治理嘛
12 } 

 

  接着我们来分析一下“治”

从最上面分治的流程图可以很明显的知道,“治” 的核心在于将子序列合并后有序化,那怎么样才能在合并的过程中有序化呢。前面我提到过归并排序的空间复杂度为O(N)。前三大排序空间复杂度为O(1)。嗯?

那里多出来的一组数据大小的空间?莫非ta开了一个一模一样的数组?

  没错,归并的确新开了一个数组,大小为数据大小,用于存储有序化后的子序列。就像 8 和 4 这个两个长度为1的序列就是新创造出来的,一次类推,都是如此,就以流程图为例子,在 "治" 的过程中产生了8 个长度为 1 的子序列,然后通过递归的回溯,和 并称了长度为2,4的子序列,和最终结果长度为8的数组。但是切割数组再交换实实现起来,有点麻烦(主要是懒,好吧,其实嫌这种方法太慢了)。而且在递归中多次动态开辟空间很浪费时间,所以我决定只开辟一次数组,然后将临时数组当作参数传入函数。说真的,切割序列这玩意我觉得给吧,链表排序挺好的。

 

  说了这么多就是想强调一点:我们需要一个临时数组用来存储排序好的子序列,然后塞回给原数组。

那么这一步是如何实现的呢?

  以流程图为例子,最终排序结果为升序:

这里只有最后一步的图,一步步画到最后太麻烦了(/▽\)

 

最后一步需要归并的子序列如下

  第一步:

 

  第二步:

  第三步:

  第四步:

 

  

最后这步复制可以依靠memcpy完成

  这个循环在终止条件为 两个子序列完全遍历结束,但是事实上实现起来的话,应该是一个子序列遍历完毕后立即退出,再写一个循环,把没有遍历完毕的子序列全部直接填入临时数组。

所以呢这块 “治” 的代码因该是这样子的

 1 void mergeAdd(int arr[], int left, int mid, int right, int *temp)
 2 {
 3     //实现“治”
 4     int i = left;
 5     int j = mid + 1;
 6     int k = left; //临时下标,用于将arr索引映射到temp上
 7     while (i <= mid && j <= right)
 8     { 
 9         //选出小的存入temp
10         temp[k++] = arr[i] < arr[j] ? arr[i++] : arr[j++];
11     }
12     while (i <= mid)
13     {
14         temp[k++] = arr[i++];
15     }
16     while (j <= right)
17     {
18         temp[k++] = arr[j++];
19     }
20     //把temp中的内容拷给arr数组中
21     //进行归并的时候,处理的区间是arr[left,right),对应的会把
22     //这部分区间的数组填到tmp[left,right)区间上
23     memcpy(arr + left, temp + left, sizeof(int) * (right - left + 1));
24 }

 

  完整的归并排序代码

 1 void mergeAdd(int arr[], int left, int mid, int right, int *temp)
 2 {
 3     //实现“治”
 4     int i = left;
 5     int j = mid + 1;
 6     int k = left; //临时下标,用于将arr索引映射到temp上
 7     while (i <= mid && j <= right)
 8     { 
 9         //选出小的存入temp
10         temp[k++] = arr[i] < arr[j] ? arr[i++] : arr[j++];
11     }
12     while (i <= mid)
13     {
14         temp[k++] = arr[i++];
15     }
16     while (j <= right)
17     {
18         temp[k++] = arr[j++];
19     }
20     //把temp中的内容拷给arr数组中
21     //进行归并的时候,处理的区间是arr[left,right),对应的会把
22     //这部分区间的数组填到tmp[left,right)区间上
23     memcpy(arr + left, temp + left, sizeof(int) * (right - left + 1));
24 }
25 
26 void mergeSort(int arr[], int left, int right, int *temp)
27 { //实现“分”
28     if (left >= right)
29     { //停止条件
30         return;
31     }
32 
33     int mid = left + (right - left) / 2;
34     mergeSort(arr, left, mid, temp);
35     mergeSort(arr, mid + 1, right, temp);
36     mergeAdd(arr, left, mid, right, temp);
37 }
38 
39 //作为接口函数
40 void MergeSorted(int arr[], int n)
41 {
42     int *temp = malloc(sizeof(int) * n);
43     mergeSort(arr, 0, n - 1, temp);
44     free(temp);
45 }

 

  测试代码:

 1 void MergeSorted(int arr[], int n);
 2 void ArrayPrint(int arr[], int n);
 3 
 4 int main(void)
 5 {
 6     int arr[] = {5, 6, 7, 334, 8, 2, 32, 4, 1, 0, 2};
 7 
 8     MergeSorted(arr, sizeof(arr) / sizeof(arr[0]));
 9     ArrayPrint(arr, sizeof(arr) / sizeof(arr[0]));
10 
11     return 0;
12 }
13 
14 void ArrayPrint(int arr[], int n)
15 {
16     for (int i = 0; i < n; i++)
17     {
18         printf("%d ", arr[i]);
19     }
20 }

 

  到这里归并排序就算讲完了,至于归并排序的非递归实现我就不详细讲了,精心设计以下两个循环的步长就可以轻松完成,再不行用栈呗(*/ω\*),直接贴代码了

 1 void MergeAdd(int arr[], int left, int mid, int right, int *tmp)
 2 {
 3     int i = left;
 4     int j = mid + 1;
 5     int k = left;
 6     
 7         //跑完一个子序列就退出
 8         while (i <= mid && j <= right)
 9     {
10         tmp[k++] = arr[i] < arr[j] ? arr[i++] : arr[j++];
11     }
12 
13         //跑完余下的子序列
14     while (i <= mid)
15     {
16         tmp[k++] = arr[i++];
17     }
18     while (j <= right)
19     {
20         tmp[k++] = arr[j++];
21     }
22 
23     memcpy(arr + left, tmp + left, sizeof(int) * (right - left + 1));
24 }
25 
26 void MergeSort(int arr[], int len, int *tmp)
27 {
28     if (len <= 1)
29     {
30         return;
31     }
32     
33     //定义一个步长gap,初始值为1,相当于每次只合并两个长度为1的元素
34     for (int gap = 1; gap <= len; gap *= 2)
35     {
36         for (int i = 0; i <= len; i += 2 * gap)
37         {//每次的步长都会变1,2,4……(每次写这个就感觉超像希尔排序)
38             int beg = i;
39             int mid = (gap - 1) + i;
40             if (mid >= len)
41             {
42                 mid = len;
43             }
44             int end = mid + gap;
45             if (end >= len)
46             {
47                 end = len;
48             }
49             MergeAdd(arr, beg, mid, end, tmp);
50         }
51     }
52 }
53 
54 void MergeSorted(int arr[], int n)
55 {
56     int *temp = malloc(sizeof(int) * n);
57     MergeSort(arr, n - 1, temp);
58     free(temp);
59 }            

 

  算法不易,诸君共勉!

posted @ 2020-01-20 16:16  秦_殇  阅读(248)  评论(0编辑  收藏  举报