- 时间复杂度 O(N*log(N))
- 空间复杂度 O(n)
归并排序(英语:Merge sort,或mergesort),是创建在归并操作上的一种有效的排序算法,效率为 {displaystyle O(nlog n)} {displaystyle O(nlog n)}(大O符号)。1945年由约翰·冯·诺伊曼首次提出。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,且各层分治递归可以同时进行。
动画演示
核心代码 ( 使用自顶向下 )
//合并两个数组,使得新数组有序
const __merge = (leftArr, rightArr) => {
const newArr = [];
while (leftArr.length && rightArr.length) {
if (leftArr[0] < rightArr[0]) {
newArr.push(leftArr.shift());
} else {
newArr.push(rightArr.shift());
}
}
//若rightArr比leftArr短,则将leftArr剩下的有序数字依次push
while (leftArr.length) {
newArr.push(leftArr.shift());
}
//若leftArr比rightArr短,则将rightArr剩下的有序数字依次push
while (rightArr.length) {
newArr.push(rightArr.shift());
}
return newArr;
}
//递归使用归并排序
const __mergeSort = (arr) => {
if (arr.length === 1) {
return arr;
}
const middle = Math.floor(arr.length / 2),
leftArr = arr.slice(0, middle),
rightArr = arr.slice(middle);
const sortedLeftArr = __mergeSort(leftArr);
const sortedRightArr = __mergeSort(rightArr);
return __merge(sortedLeftArr, sortedRightArr);;
}
思考
-
测试实例使用50000的完全随机整数进行排序,结果显示归并排序比插入排序速度快。但是如果我们在一个数据完全有序的环境下,我们知道插入排序的时间复杂度会将为O(n),所以这种情况下归并排序的速度还会比插入排序的速度快吗?
测试代码如下
//在50000条完全有序的数据下,归并排序与插入排序速度比较 let n = 50000; const arr = SortTestHelper.generateSortedArray(n); const arr2 = SortTestHelper.cloneArr(arr); SortTestHelper.testSort('inserction sort', insertionSort, arr, n); mergeSort(arr2);
测试结果,归并排序耗时为 100ms-120ms,而插入排序耗时在 0ms-1ms之间。
归并排序的优化
- 优化方案一
回顾上面的归并排序的核心代码
//递归使用归并排序,对arr[l.....r]的范围进行排序
const __mergeSort = (arr) => {
if (arr.length === 1) {
return arr;
}
const middle = Math.floor(arr.length / 2),
leftArr = arr.slice(0, middle),
rightArr = arr.slice(middle);
const sortedLeftArr = __mergeSort(leftArr);
const sortedRightArr = __mergeSort(rightArr);
return __merge(__mergeSort(leftArr), __mergeSort(rightArr));
这里我们递归调用__mergeSort
,在对两个部分进行归并排序之后,我们没有管两个部分的顺序如何就直接调用__merge
函数。但是我们设想一下此时 sortedLeftArr
的最后一个元素值如果比sortedRightArr
第一个元素的值小,那么说明 sortedLeftArr
整体是比 sortedRightArr
小的( 因为归并过程__mergeSort
能确保当前的归并数组是有序的 ),那么我们就不需要__merge
,对于数组的合并我们使用concat
即可
//`sortedLeftArr`的最后一个元素值如果比`sortedRightArr`第一个元素的值小
//则这个arr就是有序的,不需要 __merge。
// __mergeSort函数确保sortedLeftArr和sortedRightArr是有序的
sortedLeftArr = [1,2,3,4,5,6]
sortedRightArr = [7,8,9,10]
优化后代码
//递归使用归并排序
const __mergeSort = (arr) => {
if (arr.length === 1) {
return arr;
}
const middle = Math.floor(arr.length / 2),
leftArr = arr.slice(0, middle),
rightArr = arr.slice(middle);
const sortedLeftArr = __mergeSort(leftArr);
const sortedRightArr = __mergeSort(rightArr);
//优化版本
//判断 sortedLeftArr的最后一个元素与sortedRightArr的第一个元素的大小
//若前者大,则只需要归并
//否则只需要concat返回,因为 [1,2,3,4,5] [6,7,8,9,10]
if (sortedLeftArr[sortedLeftArr.length - 1] > sortedRightArr[0]) {
return __merge(sortedLeftArr, sortedRightArr);
} else {
return sortedLeftArr.concat(sortedRightArr);
}
}
实测同样50000条完全有序的整数下,优化后的归并排序耗时约在 40ms-50ms
- 优化方案二
可以在归并排序到某个部分时候调用插入排序以加快排序的速度。但是上述归并排序的实现是基于二分法切割数组,最后一次比较两个归并好的数组的值大小。
结论
- 即使优化过后的归并排序也无法完全像插入排序一样可以在时间复杂度上转为 O(n),因为归并排序本身受logN的限制
- 归并排序实现的优化方案不同,例如判断两部分的归并好的数组的尾部和头部元素进而判断当前数组是否有序。以及加入插入排序加快归并的速度