归并排序(Merge Sort)
标签
稳定排序、非原地排序、比较排序
基本思想
归并排序属于比较类非线性时间排序,号称比较类排序中性能最佳者,在数据中应用中较广。
归并排序是分治策略的一个典型的应用。所谓分治策略(Divide and Conquer),即将问题分(divide)成一些小的问题以期递归求解,而治(conquer)的阶段则将分的阶段得到的各答案“融合”在一起,即分而治之。落实到归并排序中,就是将整个序列先递归划分为子序列(子序列起始长度为1),然后将相邻的子序列两两合并,合并的过程即是有序化的过程。如此重复,子序列间不断合并得到更长的有序子序列,直到整个序列合并完成,则此时序列完全有序。
拆分阶段
拆分可以用递归实现,也可以用迭代。
递归的话,自上而下。先拆分,大事化小的思想。目的是将长度为$n$的待排序序列:$a_0a_1\dots a_{n - 1}$,先一分为二,拆成$a_0a_1\dots a_{\frac{n - 1}{2} - 1}$和$a_{\frac{n - 1}{2}}\dots a_{n - 1}$两个子序列,再分别将这两个子序列继续一分为二,如此直到子序列长度为1,即完成拆分,进入合并阶段。
而迭代的话,自下而上。直接从最小子序列(长度为1)开始有序化合并,逐步往上,直至整个序列合并完成有序化。
递归 vs. 迭代
合并阶段
显然,合并阶段需要申请额外的临时存储空间(数组)。
如上图所示,每次合并的过程即是对两个已有序子序列进行合并再排序的过程。
通常我们使用两个游标分别指示两个子序列当前待排序元素的位置,另有一个游标指示临时数组的当前元素存储位置。三个游标的初始位置均在头部。实际操作时,每次对两个子序列中游标指示位置的元素进行比较,将较小者(升序排列)存入临时数组中(游标指示位置),存储元素对应的两个游标均后移一位。如此操作,直至子序列之一的游标到达末部,则将另一个子序列中剩余元素直接依次全部存入临时数组中,即可。
如此,临时数组中存储的即是两个子序列合并后的有序序列,再将其复制目标数组中,一次合并排序就完成了。
算法描述
- 步骤1:把长度为$n$的输入序列分成两个长度为$n/2$的子序列;
- 步骤2:对这两个子序列分别采用归并排序;
- 步骤3:将两个排序好的子序列合并成一个最终的排序序列。
动图演示
时间复杂度
归并排序一共需要进行$O(\log n)$层归并操作,每层的归并操作的总时间复杂度是$O(n)$,因此总体的时间复杂度为$O(n\log n)$。
最坏,最好和平均时间复杂度都是$Θ(n\log n)$。
空间复杂度
和其他排序有所不同,为了实现归并操作,每次合并都需要开辟额外的空间来临时保存合并后的排序结果,总共需要开辟$n$个元素的空间,所以归并排序的空间复杂度为$O(n)$。
算法示例(C)
根据实现思路不同,总的来说,归并排序的代码实现也分为递归和迭代两类。递归式自顶向下,迭代是自底向上。
下面先讲讲两类方法的共同部分,也是核心调用——有序化合并函数Merge。
Merge函数
背景
Merge的前提是两个子序列已有序。
Merge函数的输入为一个待排序数组a[low...high],则它实际上由两个已有序的子序列:a[low...mid]和a[mid+1...high]组成。为方便描述,我们分别称之为a1和a2,则目标是a1,a2的合并有序化。
实现步骤
- 设置两个游标用于元素比较(在aux中进行):变量 i 和 j ,分别代表左游标和右游标,开始时分别指向aux[low]和aux[mid + 1];
- 设置游标变量 k 用于指示在a中放置元素的位置(在a中进行),k在开始时候指向a[low];
- 总体上来说 i, j, k 的趋势都是从左向右移动的;
- 左半边用尽(取右半边的元素)
- 右半边用尽(取左半边的元素)
- 左半边元素小于等于右半边(取左半边的元素)
- 右半边元素大于左半边(取右半边的元素)
代码实现(C)
void my_merge(int *a, int low, int mid, int high) { for (int p = low; p <= high; p++) aux[p] = a[p]; //aux为全局变量 int i = low, j = mid + 1, k = low; while (i <= mid && j <= high) { if (aux[i] <= aux[j]) { a[k++] = aux[i++]; } else { a[k++] = aux[j++]; } } while (i <= mid) { a[k++] = aux[i++]; } while (j <= high) { a[k++] = aux[j++]; } return; }
递归方法(自顶向下)
图解Merge函数调用
从上到下,是各个Merge的先后调用顺序。1最先调用,15最后调用。
merge_sort_up_to_down(a, low, mid); //1 merge_sort_up_to_down(a, mid + 1, high); //2 my_merge(a, low, mid, high); //3
【首先,在第一层递归的时候,先进入的是第一行的merge_sort方法,然后紧接着又进入了第二层递归的第一行merge_sort方法, 如此继续,由(a, low,mid)的参数列表可知其递归的趋势是一直向左移动的,直到最后一层递归,所以最先执行第一行merge_sort的对象是a[0]和a[1](上图编号1),然后执行的是最后一层递归的第二行代码,这时候merge_sort的对象是a[2]和a[3](上图编号2),再然后执行第三行,对已经有序的a[0]、a[1]和a[2]、a[3]进行my_merge(上图编号3)。之后返回上一层执行第二行,同样的递归向下再返回,再执行第三行,再往上返回一层。如此,递归的深度不断变浅, 直到最后对整个数组的左右两半进行my_merge,完成最终的有序化合并。】
完整代码(C)
#include <stdio.h> #include <stdlib.h> int *aux; void my_merge(int *a, int low, int mid, int high) { for (int p = low; p <= high; p++) aux[p] = a[p]; int i = low, j = mid + 1, k = low; while (i <= mid && j <= high) { if (aux[i] <= aux[j]) { a[k++] = aux[i++]; } else { a[k++] = aux[j++]; } } while (i <= mid) { a[k++] = aux[i++]; } while (j <= high) { a[k++] = aux[j++]; } return; } void merge_sort_up_to_down(int *a, int low, int high) { if (low >= high) return; int mid = low + (high - low) / 2; merge_sort_up_to_down(a, low, mid); merge_sort_up_to_down(a, mid + 1, high); my_merge(a, low, mid, high); return; } void merge_sort(int *a, int len) { aux = (int *)malloc(sizeof(int) * len); merge_sort_up_to_down(a, 0, len - 1); free(aux); return ; } int main() { /* 归并排序(升序)*/ int num[10] = {5, 1, 8, 4, 7, 2, 3, 9, 0, 6}; int length = sizeof(num) / sizeof(num[0]); merge_sort(num, length); for (int i = 0; i < length; i++) printf("%d ", num[i]); return 0; }
递归的三种优化思路
优化一. 小规模数组使用插入排序
用不同的方法处理小规模问题能改进大多数递归算法的性能,因为递归会使小规模问题中方法调用太过频繁,所以改进对它们的处理方法就能改进整个算法。 因为插入排序非常简单, 因此一般来说在小数组上比归并排序更快。 这种优化能使归并排序的运行时间缩短10%到15%;
快速排序的递归优化思路与之类似,对于小规模数组,在归并和快速排序算法中都可以替换成插入排序,以提升效率。
具体做法就是把递归停止的判断语句 if (low >= high) return; 改成
if (low + M>=high) { // 数组长度小于M的时候 insert_sort(a, low, high); // 切换到插入排序 return; }
优化二:Merge函数可跳过情况判断
void merge_sort_up_to_down(int *a, int low, int high) { // 终止递归的条件 if (low >= high) return; int mid = low + (high - low) / 2; merge_sort_up_to_down(a, low, mid); merge_sort_up_to_down(a, mid + 1, high); if(a[mid] <= a[mid+1]) return; // 避免不必要的合并调用 merge(a, low, mid, high); // 单次合并 }
优化三:去除原数组序列到辅助数组的拷贝
每次调用Merge方法时候,我们都把a对应的序列拷贝到辅助数组aux中来:
for (int p = low; p <= high; p++) aux[p] = a[p];
void merge_sort_up_to_down(int *a, int *aux, int low, int high) { if (low >= high) return; int mid = low + (high - low) / 2; merge_sort_up_to_down(aux, a, low, mid); //每层进行角色交换 merge_sort_up_to_down(aux, a, mid + 1, high); my_merge(a, aux, low, mid, high); return; } void merge_sort(int *a, int len) { int *aux = (int *)malloc(sizeof(int) * len); for (int p = 0; p < len; p++) //只拷贝递归前的这一次 aux[p] = a[p]; merge_sort_up_to_down(a, aux, 0, len - 1); free(aux); return ; }
【注意:因为最外部的 merge_sort 和 my_merge 的参数顺序是相同的, 所以,无论递归过程中拷贝数组和原数组的角色如何替换,对最后一次调用的my_merge而言(将整个数组左右半边合为有序的操作),最终被排为有序的一定是原数组,而不是拷贝数组!】
完整优化代码(C)
#include <stdio.h> #include <stdlib.h> int *aux; void my_merge(int *a, int *aux, int low, int mid, int high) { int i = low, j = mid + 1, k = low; while (i <= mid && j <= high) { if (aux[i] <= aux[j]) { a[k++] = aux[i++]; } else { a[k++] = aux[j++]; } } while (i <= mid) { a[k++] = aux[i++]; } while (j <= high) { a[k++] = aux[j++]; } return; } void merge_sort_up_to_down(int *a, int *aux, int low, int high) { if (low + M>=high) { // 优化一:数组长度小于M的时候 insert_sort(a, low, high); // 切换到插入排序 return; } int mid = low + (high - low) / 2; merge_sort_up_to_down(aux, a, low, mid); //优化三:去掉递归内拷贝 merge_sort_up_to_down(aux, a, mid + 1, high); if(a[mid] <= a[mid+1]) return; // 优化二:避免不必要的合并调用 my_merge(a, aux, low, mid, high); // 单次合并 return; } void merge_sort(int *a, int len) { aux = (int *)malloc(sizeof(int) * len); for (int p = 0; p < len; p++) aux[p] = a[p]; merge_sort_up_to_down(a, aux, 0, len - 1); free(aux); return ; } int main() { /* 归并排序(升序) */ int num[10] = {5, 1, 8, 4, 7, 2, 3, 9, 0, 6}; int length = sizeof(num) / sizeof(num[0]); merge_sort(num, length); for (int i = 0; i < length; i++) printf("%d ", num[i]); return 0; }
迭代循环(自底向上)
思路比较简单:
完整代码(C)
#include <stdio.h> #include <stdlib.h> #include <algorithm> int *aux; void my_merge(int *a, int low, int mid, int high) { int i = low, j = mid + 1, k = low; for (int p = low; p <= high; p++) aux[p] = a[p]; while (i <= mid && j <= high) { if (aux[i] <= aux[j]) { a[k++] = aux[i++]; } else { a[k++] = aux[j++]; } } while (i <= mid) { a[k++] = aux[i++]; } while (j <= high) { a[k++] = aux[j++]; } return; } void merge_sort_down_to_up(int *a, int length) { aux = (int *)malloc(sizeof(int) * length); for (int size =1; size < length; size = size+size){ for(int low = 0; low< length - size; low += size+size) { my_merge(a, low, low + size - 1, std::min(low + size + size - 1, length - 1)); //最后一个子序列可能不够一个size } } return; } int main() { /* 归并排序(升序) */ int num[10] = {5, 1, 8, 4, 7, 2, 3, 9, 0, 6}; int length = sizeof(num) / sizeof(num[0]); merge_sort_down_to_up(num, length); for (int i = 0; i < length; i++) printf("%d ", num[i]); return 0; }
(整理自网络)
参考资料:
https://www.cnblogs.com/chengxiao/p/6194356.html