归并排序
归并排序的时间复杂度是 O(n log n),不论是最坏、最好还是平均情况,因此它是一个非常高效的排序算法。
归并排序的空间复杂度是 O(n),因为它需要额外的空间来存储合并过程中的临时数组。
示意图

实现
- 先拆分(Divide):不断将数组拆成左右两半,直到每个子数组只剩一个元素。
1. 递归法(Top-down)
先将数组拆分到不可分割为止,然后再进行组合、排序,直到资料全部排序完成。
function mergeSort_TopBottom(arr){ //如果数组元素只有1个或0个,则认为是有序数组 if(arr.length <=1) return arr; //将数组对半劈开,直到只有1个元素为止 const middle = Math.floor(arr.length/2); const leftArr = mergeSort(arr.slice(0,middle)); const rightArr = mergeSort(arr.slice(middle)); return merge(leftArr,rightArr); }使用递归可以很清楚的描述算法过程,缺点是函数频繁地自我调用。长度为n的数组最终会调用mergeSort_TopBottom()函数 2n-1次,这意味着一个长度超过1500的数组会在Firefox上发生栈溢出错误。因此实际开发中应该使用迭代来实现同样的功能
2. 迭代法(Bottom-up)
迭代法不是将整个数组分成不同的部分,而是使用不同大小的间隔在数组上循环。最小以2为单位拆分,进行排序,再组合成下一个单位4,以此类推,直到排序完成。 如图所示:
// 迭代版归并排序 function mergeSortIterative(arr) { let n = arr.length; let step = 1; // 外层循环控制步长,逐渐增大合并的子数组的大小 while (step < n) { for (let i = 0; i < n; i += step * 2) { // 合并相邻的两个子数组 let left = arr.slice(i, i + step); let right = arr.slice(i + step, i + step * 2); arr.splice(i, step * 2, ...merge(left, right)); // 合并后放回原数组 } step *= 2; // 每次步长翻倍 } return arr; }
代码关键点说明:
外层循环负责每次 增大合并的子数组的大小。我们首先从最小的子数组开始(每个子数组只有一个元素),然后逐步增大步长,直到整个数组被合并成一个有序数组。
-
第一次外层循环时,
step = 1,我们会合并相邻的两个单元素子数组。 -
第二次外层循环时,
step = 2,我们会合并相邻的两个子数组(每个子数组有两个元素)。 -
第三次外层循环时,
step = 4,我们会合并相邻的两个子数组(每个子数组有四个元素)。 -
依此类推,直到最终整个数组合并成一个有序数组。
内层循环负责 逐个处理子数组。每次循环都需要获取两个子数组,每个子数组的长度是
step,合并后的子数组大小是step * 2,下一个需要读取的子数组的开始下标就是:i = i+ step*2
slice的第二个参数是结束索引的位置(千万不要记成是长度) -
- 再合并(Merge):从最小子数组开始,逐步将其合并成有序数组。
- 依次比较左右两个数组中对应位置的元素,将较小的元素插入到 tempArr 数组。
- 最后,剩余的元素(如果有的话)会被直接加入到
result中。
function merge(left,right){ const temArr = []; // 不停的比较剩下未排序的两个数组 while(left.length && right.length){ if(left[0]<=right[0]) temArr.push(left.shift()); else temArr.push(right.shift()); } // 左边或右边可能还有剩余的元素,剩余的元素肯定比之前的元素大,合并即可 return temArr.concat(left,right); }

浙公网安备 33010602011771号