手写算法并记住它:归并排序

对于经典算法,你是否也遇到这样的情形:学时以为很清楚,可过阵子就忘了?javascript

本系列文章就尝试解决这个问题。java

研读那些排序算法,细品它们的名字,其实都很贴切。面试

好比归并排序,“归并”二字就是“递归”加“合并”。它是典型的分而治之算法。算法

上图中,先把数组一分为二,而后递归地排序好每部分,最后合并。数组

其中,分和归相对容易些(后面会说),该算法的核心是:如何合并两个已经排好序的数组?markdown

解决办法很容易想到,两权相较取其轻。oop

如上图所示,每次比较取出一个相对小的元素放入结果数组中。post

翻译成代码:spa

let left = [2, 4, 6], i = 0
let right = [1, 3, 5], j = 0
let result = []
while(i < left.length && j < right.length) {
  if (left[i] < right[j]) {
    result.push(left[i])
    i++
  } else {
    result.push(right[j])
    j++
  }
}
console.log(result) // [ 1, 2, 3, 4, 5 ]
复制代码

代码中,i和j分别是两个数组的下标。遍历结束后,某个数组可能会有剩余,所有追加到结果数组中就能够了:翻译

if (i < left.length) {
  result.push(...left.slice(i))
} 
if (j < right.length){
  result.push(...right.slice(j))
}
复制代码

说明:为了清晰表达两者谁均可能剩余,这里没有直接使用if...else。事实上不会出现两者都有剩余状况的(while循环保证的)。另外,这里使用了数组相关API(concat也能够),也能够直接使用循环来作。

并,这个核心问题解决了,接下来咱们来看看分和归。

关于分,只要把数组从中间劈成两半就行:

let m = Math.floor(array.length / 2)
let left = array.slice(0, m)
let right = array.slice(m)
复制代码

至于递归,虽然它不符合线性思惟,但其实也没啥难的。

只要有递归步骤(递归公式),很容翻译成代码的。

咱们再回忆一下归并算法的步骤:

  1. 数组分红两半,left和right
  2. 递归处理left
  3. 递归处理right
  4. 合并两者结果

轻松翻译成代码:

function mergeSort(array) {
  let m = Math.floor(array.length / 2)
  let left = mergeSort(array.slice(0, m))
  let right = mergeSort(array.slice(m))
  return merge(left, right)
} 
复制代码

递归是自身调用自身,不能无限次的调用下去,所以须要有递归出口(初始条件)。

它的递归出口是,当数组元素个数为小于2时,就是已是排好序的,不须要再递归调用了。

所以须要在前面加入代码:

if (array.length < 2) {
  return array
}
复制代码

查看完整代码:codepen

至此,归并排序原理和实现已经说完了。

这里总结一下,归并排序须要额外空间,空间复杂度为O(n),不是本地排序,相等元素是不会交换先后顺序,于是是稳定排序。时间复杂度为O(nlogn),是比较优秀的算法,在面试题中出现的几率也很高。

归并排序和下一篇要讲的快速排序,都是分而治之算法,都须要分、归、并。前者重头戏在于如何去并,然后者重头戏在于如何去分。

归并排序,要作到能分分钟手写出来,是须要掌握其排序原理的。其关键在于,经过比较取小来合并两个已递归排好序的数组。至于递归,只要能说清楚递归步骤和出口,就能很容易写出来,不须要死记硬背的。

但愿有所帮助,本文完。



本系列已经发表文章:

相关文章
相关标签/搜索