归并排序( merge sort )是一种采用 分治法( divide and conquer )设计的高效排序算法,时间复杂度为 $O(n\log{n})$ 。它的设计思路很直白,主要分为三步:
- 问题划分:将待排序序列平均划分为两个部分;
- 子问题求解:分别对两个子序列进行排序(递归执行归并排序);
- 结果合并:将两个已经排好顺序的子序列合并成一个完整的有序序列,得到最终结果;
分治法 是计算机科学中一种很重要的算法设计思想,简单概括就是“分而治之”:将一个复杂的问题,分解成两个或多个子问题,再对子问题进行求解,最终合并成原问题的解。
随着问题的划分,子问题规模逐渐变小,求解难度逐渐降低,最终达到可以直接求解的程度。
合并有序序列
归并排序最为关键的一步是最后的 结果合并 ,即:将两个已经排好顺序的子序列合并成一个完整的有序序列。那么,有序序列应该怎么合并呢?
思路其实很简单。先准备一个临时存储空间,用来保存合并后的完整序列,可称为目标序列。合并时只需不断地从两个子序列中取出最小元素,然后依次写入目标序列即可。
由于子序列已经排好顺序,它们各自的最小元素均位于序列 头部 。因此,我们只需比较子序列头部元素的大小,即可找到它们的最小值。具体步骤如下:
- 变量 $i$ 用于记录目标序列下一个待合入元素,指向临时存储空间下一个待写入位置;
- 变量 $j$ 、$k$ 用于遍历两个子序列,开始时分别指向子序列①和②的头部;
- 循环比较两个子序列中,$j$ 、$k$ 指向的头部元素大小,并视情况处理:
- 如果 $j$ 指向的序列①头部元素不大于序列②,将它写入 $i$ 指向的位置,并分别自增 $i$ 、$j$ ,使其分别指向下一个位置;
- 否则意味着 $k$ 指向的序列②头部元素更小,将它写入 $i$ 指向的位置,并分别自增 $i$ 、$k$ ,使其分别指向下一个位置;
- 如果两个子序列中有一个已经合并完毕,则将另一个的剩余部分依次拷贝到目标序列即可;
下面这个动画,很好地演示了合并有序序列的整个过程:
合并有序序列示例代码如下,可结合注释和动画对照理解:
归并排序
掌握了合并有序序列的方法,理解归并排序也就毫无难度了:
- 先检查待排序序列的长度,如果元素个数少于 2 ,则无需处理;
- 将待排序序列平均划分为两部分(问题划分);
- 递归 执行归并排序算法分别对两个子序列进行排序(子问题求解);
- 将两个已经排好序的子序列合并起来,并保存到辅助存储空间(结果合并);
- 将结果从临时存储空间中,拷贝回原序列;
递归算法需要特别注意 退出条件 。在归并排序算法中,随着序列的层层划分,长度不断减半。当长度最终降到 2 以下时,递归调用便结束。
这个动画很好的演示了归并排序的过程,可以帮助理解:
归并排序示例代码如下,可结合注释和动画对照理解:
/*
* merge_sort - 对给定数组进行归并排序
*
* 参数说明:
* - array: 数组首地址;
* - n: 数组长度,即数组元素个数;
* - workspace: 辅助存储空间,长度至少为 n ;
*
* 返回值:无
*/
void merge_sort(int *array, int n, int *workspace) {
// 检查待排序数组长度,小于 2 则无需处理
if (n < 2) {
return
}
// 将待排序数组平均分为两部分,每部分的长度均减半
int n1 = n / 2;
// 递归调用merge_sort函数,对子数组①进行排序
merge_sort(array, n1, workspace);
// 计算子数组②的长度,由于n可能为奇数,n2可能不等于n1
int n2 = n - n1;
// 递归调用merge_sort函数,对子数组②进行排序
merge_sort(array+n1, n2, workspace);
// 将两个已经排好序的子数组,合并到辅助存储空间
merge_two_sorteds(array, n1, array+n1, n2, workspace);
// 将合并结果,从辅助空间拷贝回原数组
for (int i=0; i
def merge_sort(array):
''' 对给定序列(list)进行归并排序
参数说明:
- array: 给定序列,应该是一个list对象。
返回值:无
'''
# 检查待排序序列长度,小于 2 则无需处理
n = len(array)
if n < 2:
return
# 将待排序序列平均分为两部分,每部分的长度均减半
n1 = n // 2
# 将子序列①拷贝出来,并递归调用merge_sort函数对子序列进行排序
# 因list对象特性原因,做法跟动画演示略有区别
# 这里先将子序列从原序列复制出来,排序后再直接合并回原序列
sub1 = array[:n1]
merge_sort(sub1)
# 同样将子序列②拷贝出来,并递归调用merge_sort函数对子序列进行排序
sub2 = array[n1:]
merge_sort(sub2)
# 将两个已经排好顺序的子序列,合并回原序列
merge_two_sorteds(sub1, sub2, array)
递归细节
如果觉得归并排序递归调用很难理解,可以观察下面这个动画,来获得启发。动画右边的树形结构,形象地描述了序列层层分解的过程。红框则标注了当前的执行进度,为你揭开递归调用的神秘面纱:
完整代码
/*
* merge_two_sorteds - 合并两个有序数组
*
* 参数说明:
* - src1: 有序数组①首地址;
* - n1: 有序数组①长度,即数组元素个数;
* - src2: 有序数组②首地址;
* - n2: 有序数组②长度,即数组元素个数;
* - dst: 结果保存数组,长度至少为两个有序数组之和;
*
* 返回值:无
*/
void merge_two_sorteds(int *src1, int n1, int *src2, int n2, int dst) {
// 变量 i 记录下一个待合入位置,初始化为 0 , 指向结果数组待写入位置
// 变量 j k 用于遍历两个待合并数组,均初始化为 0 ,分别指向数组头部
int i=0, j=0, k=0;
// 循环遍历两个待合并数组
// 当它们中有一个合并完毕,则结束循环
while (j
def merge_two_sorteds(src1, src2, result=None):
''' 合并两个有序序列
参数说明:
- src1: 有序序列①,应该是一个list对象。
- src2: 有序序列②,应该是一个list对象。
- result: 用于保存结果,应该是一个list对象或者None。
如果为None,函数将根据src1和src2的长度,分配一个list对象。
返回值:结果序列
'''
# 分别计算两个序列的长度,备用
n1 = len(src1)
n2 = len(src2)
# 如果调用者没有提供结果保存处,自动分配一个
if result is None:
result = [None] * (n1 + n2)
# 变量 i 记录下一个待合入位置,初始化为 0 , 指向结果序列待写入位置
# 变量 j k 用于遍历两个待合并序列,均初始化为 0
i = j = k = 0
# 循环遍历两个待合并序列
# 当它们中有一个合并完毕,则结束循环
while j
【小菜学算法】系列文章首发于公众号【小菜学编程】,敬请关注: