算法原理
归并算法体现了分而治之的思想,使用递归实现的主要逻辑为:
将数组不断向下递归形成左右两个子部分,分别对左右两个子部分进行“有序数组合并”。
其中的“有序数组合并”的主要逻辑为:
比较两个数组最左端(最小)的元素,选取较小的元素,放置到合并后的数组中。不断重复上述操作,完成合并。
归并排序也可使用自底向上的非递归思路。上述递归的目的是划分左右两个子部分,事实上也可以控制间隔手动划分:
从数组中依次取2个,执行有序数组合并;之后从数组中每次取4个,执行有序数组合并;之后取8个,以此类推。
算法实现
归并排序的递归实现
对应的有序数组合并的实现如下:
归并排序将一个数组递归划分为[left, mid]
和[mid+1, right]
两个部分。划分到最后时,两个部分均只剩余一个数字,因此可以看作有序数组进行合并。合并后的有序数组继续向上合并,从而完成整体排序过程。
实现过程中需要注意的地方有两处。其一是有序数组合并时的数组标号,需要注意划分后的第二个部分是从mid+1
开始的。其二是两个有序数组中可能有一个已经全部放入合并后数组,因此循环中要对其进行判断。
归并排序的自底向上实现
自底向上的归并排序,主要难度在于剩余元素的处理。上述代码中For
循环内为对于指定间隔的合并,执行For
循环之后,还要考虑数组内的剩余元素。
例如对于长度为7的数组,第一轮完成后剩余1个元素,第二轮完成后剩余3个元素,显然要对剩余的3个元素按照2|1
分组的形式进行合并。观察数组划分的方式,可以得出下述事实:
本轮剩余元素个数 = (上轮间隔数 + 上轮剩余元素个数) or 上轮剩余元素个数
对于剩余元素超过上一轮间隔数的情况,均可按照上一轮间隔数|剩余个数
的分组形式进行排序;如果剩余元素不足上一轮间隔数,无需排序,因为这些元素一定在前面的某轮完成了排序。例如,对于长度为21的元素,第4轮(interval=16
)剩余长度为5,不足上一轮间隔数(interval=8
),那么本轮无需对其进行排序。在第三轮(interval=8
),大于上一轮间隔数(interval=4
),因此要在本轮完成排序。
考虑到上述分析中主要考虑上轮间隔,因此也可以用上轮间隔作为循环和判断依据,从而简化代码逻辑。
算法优化
归并排序有三个可以优化的地方:
- 由于插入排序对于近乎有序的数组有着较好的排序性能,可以在归并排序的规模较小时使用插入排序。注意本条优化只在理论上有效,对于具体的python实现,并没有产生可见的性能差距。
- 判断
array[mid]
和array[mid+1]
的大小关系,如果array[mid] < array[mid+1]
,此时无需额外进行“合并有序数组”操作。
- 合并有序数组的操作中,每次都要创建了临时数组
tmp
,用于保存本轮需要排序的元素。由于每轮需要排序的数组个数最多也就是数组总长度,因此可以事先创建大小为数组总长度的临时数组。
算法测试
本文出现的代码均已经过正确性测试,对于性能进行测试的结果如下:
显然归并排序可以在更短的时间内处理更多的数据。由于自底向上的实现没有使用递归,对于一些情况可能更加适用。
算法复杂度
归并排序向下递归划分的过程一共划分为$ log(N) $层。其中每层需要进行$ O(N) $级别的有序数组合并的操作。(显然,对于总长度为$ k $的两个有序数组,完成合并需要$ O(k) $时间。)因此,总体的时间复杂度为$ O(Nlog(N)) $。
由于有序数组合并过程需要额外的空间,且空间大小为数组总长度,因此空间复杂度为$ O(N) $。另外,存在原地归并算法,不需要额外空间。