赞
踩
本篇文章我将主要向大家介绍归并排序的递归实现和非递归实现。
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
我来简单总结一下上面的说法,归并排序实际上是将两个已经有序的子序列合并成一个完整的有序序列。
如下图所示,是两段有序的子序列。
接下来我们来看如何利用归并的思想将这两段有序的子序列合并成一段有序序列,这里以升序为例。
首先需要额外申请一块临时空间来存放排序后的序列,这块临时空间的大小当然就是这两个子序列的大小之和。
归并的步骤是这样的,定义两个指针分别指向两个子序列的起始位置,然后开始比较指针指向的元素。将元素值较小的一个插入到临时空间中,同时让该指针指向下一个元素。依次类推,等到其中一个序列的元素都被插入进去之后,再将另一个数组的剩余元素依次插入到新数组中。
下面来看动态演示。
可以看到,最终临时空间里的内容就是两段子序列的合并。
这就是归并排序的核心思想。
通过上面归并的思想我们可以得出这样一个结论,如果一段序列可以分解为左右两边都有序的话,我们就可以采用归并的思路来对它进行排序。
比如下图中的序列可以看成是由两段有序的子序列构成。
这样我们就以将这个序列的两边归并到一个和当前序列同样大小的临时空间中,最后再将临时空间的内容拷贝回原序列空间即可。
但是我们在实际排序的时候肯定拿不到一段这样的序列,那么我们就要思考如何对一段完全无序的序列进行归并排序呢?这里就要用到分治的思想。
分治算法
:大问题分解小问题,小问题再进一步的分解,直到不可再分解的子问题。
我们可以将一段无序序列不断分解,分解到只剩下两个数的子序列。这时候这两个数就可以看做是两个有序的子序列,从而可以采用归并算法对这两个数进行排序。排过序的序列又成为了新的有序子序列,继续往上排序,直到将原序列分成两个有序的子序列再归并就能完成整个排序的过程了。
归并排序示意图如下:
这里的分治算法需要通过递归调用来实现,下面来看递归实现代码:
void MergeArr(int* a, int begin1, int end1, int begin2, int end2, int* tmp) { int left = begin1, right = end2; int index = begin1; while (begin1 <= end1 && begin2 <= end2) { if (a[begin1] < a[begin2]) tmp[index++] = a[begin1++]; else tmp[index++] = a[begin2++]; } while (begin1 <= end1) tmp[index++] = a[begin1++]; while (begin2 <= end2) tmp[index++] = a[begin2++]; // 把归并好的再tmp的数据在拷贝回到原数组 for (int i = left; i <= right; ++i) a[i] = tmp[i]; } // 时间复杂度O(N*logN) // 空间复杂度O(N) void _MergeSort(int* a, int left, int right, int* tmp) { if (left >= right) return; int mid = (left + right) / 2; _MergeSort(a, left, mid, tmp); _MergeSort(a, mid + 1, right, tmp); // 归并[left,mid][mid+1,right]有序 MergeArr(a, left, mid, mid + 1, right, tmp); } // 归并排序递归实现 void MergeSort(int* a, int n) { assert(a); int* tmp = malloc(sizeof(int) * n); _MergeSort(a, 0, n - 1, tmp); free(tmp); }
代码分析:
首先现在排序函数中创建一块和待排数组同样大的空间。
一般我们调用的排序函数只传两个参数,一个是数组地址,一个是元素个数。但是排序函数的参数设计不适合递归调用,所以这里我们自己设计一个排序子函数来进行递归调用
。
子函数在调用的时候一共接收4个参数,一个是待排数组,一个是临时数组,还有两个分别是待排数组的首尾下标。
进入子函数中之后,先分割子区间,通过左下标和右下标拿到中间下标,然后再将数组从中间下标分割成两个子数组,将子数组的下标继续传给递归调用的函数。
这里注意,左右两个子序列的区间在传参时我们将其设定为闭区间,分别是[left, mid]
和[mid + 1, right]
.
等到函数接收的左下标大于等于右下标时,说明待排数组只有一个元素或者0个,不需要进行排序,因此结束函数调用。
归并排序的算法在另一个函数中进行,该函数接收6个参数,分别为待排数组,两个子序列的左右下标和临时数组。
归并的思想和我上面演示的基本一致,但还是有一些区别。代码中是将待排序的数组的每个元素插入到临时数组中对应的位置
,因为两块数组是同样大的,所以不会出现越界的情况。这样做的目的是为了方便将排好序的数据重新放回到原数组中。
以上就是递归实现归并的全部过程。
下面我们再来看归并的非递归实现。
非递归的核心思想和递归是一样的,只不过非递归是是用循环来代替递归。
如上图所示,一开始先以一个元素为一组两两之间进行归并。这样第一趟排完序之后,数组每两个元素都是有序的,这样就可以再以两个元素为一组两两之间进行归并,依次类推,由于是二倍增大,所以最后一趟排序中左右两个子序列必有一个大于等于数组的一半,排完之后数组有序。
这里我们在设置循环的时候需要控制两点:
1.每组序列的个数。
2.每趟排序子序列之间归并的次数。
实现代码如下:
// 归并排序非递归实现 void MergeSortNonR(int* a, int n) { assert(a); int* tmp = malloc(sizeof(int) * n); int gap = 1; //每组元素个数 while (gap < n) { for (int i = 0; i < n; i += 2 * gap) { // [i,i+gap-1] [i+gap, i+2*gap-1] int begin1 = i, end1 = i + gap - 1; int begin2 = i + gap, end2 = i + 2 * gap - 1; // 1、合并时只有第一组,第二组不存在,就不需要合并 if (begin2 >= n) break; // 2、合并时第二组只有部分数据,需要修正end2边界 if (end2 >= n) end2 = n - 1; MergeArr(a, begin1, end1, begin2, end2, tmp); } gap *= 2; } free(tmp); }
代码分析:
这里定义一个变量gap从1开始控制子序列的间隔,定义变量i从0开始表示待排数组起始位置。
第一段子序列的范围是[i,i+gap-1]
,第二段子序列的范围是[i+gap, i+2*gap-1]
.第一段子序列和第二段子序列合并之后开始合并第三段和第四段子序列,此时只需改变i的值即可,给i的值加上2倍的gap
,就可以让i指向第三段序列的起始位置,然后重复上面的操作归并第三段和第四段的序列。
这里需要注意的是,结束条件可能会分为多种情况。
第一种:按照正常情况结束.
归并的时候每一组子序列都有对应的子序列,这个时候当i>n的时候,说明所有子序列都已归并完成,可以结束一趟循环。
第二种:第一段序列没有对应的序列
如下图所示:
一二段序列归并完之后,第三段序列并没有对应的序列与之归并,这个时候就可以提前结束循环,不用再进行归并
这里还有一种特殊情况:第二段序列只有部分数据
如下图所示:
当归并第三组和第四组的时候,第四组的元素不足两个,而这个时候如果还按照上面的情况进行归并的话,就会出现越界的情况。因此这里需要修改end的边界,让end指向数组的最后一个元素,下标为n-1.
以上是内循环的分析过程,外循环通过控制gap不断增加归并子序列的大小,这里以二倍增加,所以每一趟循环结束之后给gap的值乘以2.
当gap的值大于等于n的时候,说明归并结束。
以上就是非递归实现归并的全部思路。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。