归并排序(使用JavaScript)的递归实现

664 阅读3分钟

1、算法介绍

归并排序(MergeSort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。归并排序的算法时间复杂读为O(NLogN),是一种稳定的排序算法。

2、递归算法的代码实现

/**
 * 归并序列辅助函数
 * @param {Array} arr 需要被排序的数组
 * @param {Number} leftStartPos 当前被合并序列的左索引起始值
 * @param {Number} rightStartPos 当前被合并序列的右索起始引值
 * @param {Number} rightEndPos 当前被归并序列的右索引结束值
 */
function merge(arr, leftStartPos, rightStartPos, rightEndPos) {
  // 记住当前序列在数组序列中的开始位置
  var pos = leftStartPos;
  // 记住当前两个序列的长度和
  var length = rightEndPos - leftStartPos + 1;
  // 左序列的结束位置
  var leftEndPos = rightStartPos - 1;
  // 临时序列,用来保存子序列的合并结果
  var newArr = [];
  while (leftStartPos <= leftEndPos && rightStartPos <= rightEndPos) {
    // 如果A序列的值比B序列的值小,先合并A序列的值,这将会使得数据按从小到大的数据排序
    if (arr[leftStartPos] <= arr[rightStartPos]) {
      newArr[pos++] = arr[leftStartPos++];
    } else {
      newArr[pos++] = arr[rightStartPos++];
    }
  }
  // 两个while循环是不会同时成立的,如果A序列比B序列长,直接合并A序列剩下的元素
  while (leftStartPos <= leftEndPos) {
    newArr[pos++] = arr[leftStartPos++];
  }
  // 两个while循环是不会同时成立的,如果B序列比A序列长,直接合并B序列剩下的元素
  while (rightStartPos <= rightEndPos) {
    newArr[pos++] = arr[rightStartPos++];
  }
  
  /* 由于两个子序列合并完成时,此刻我们已经不知道左边序列开始位置和右边序列开始位置,
     因此,简单的做法是我们可以换一种思考方式,我们将数据从右边复制到左边,
     需要拷贝的总长度是两个序列的长度和 */
  for (var i = 0; i < length; i++, rightEndPos--) {
    arr[rightEndPos] = newArr[rightEndPos];
  }
}

/**
 * 归并排序的辅助函数
 * @param {Array} arr 需要被排序的数组
 * @param {Number} left 数组的左起始索引
 * @param {Number} right 数组的右结束索引
 */
function _mergeSort(arr, left, right) {
  // 如果开始位置和结束位置一样或者比结束位置还要来的大,说明已经没有继续归并的意义了
  if (left >= right) {
    return;
  }
  var center = Math.floor((left + right) / 2);
  //沿着左序列继续归并
  _mergeSort(arr, left, center);
  //沿着右序列继续归并
  _mergeSort(arr, center + 1, right);
  //合并当前已经排序完成的左右序列   
  merge(arr, left, center + 1, right);
}

/**
 * 统一对外接口,为外界保留简单的调用接口
 */
function mergeSort(arr) {
  if (!Array.isArray(arr)) {
    throw new Error("can not sort");
  }
  return _mergeSort(arr, 0, arr.length - 1);
}

3、递归算法的排序思路

归并排序的重点思想便是合并两个有序子序列

3.1 合并两个有序序列

假设我们现在有两个有序子序列a: [1,2,8,14,15], b: [6, 7, 10],合并两个子序列我们需要借助三个变量分别记住a序列当前被合并到的位置posA, b序列当前被合并到的位置posB, 还有当前合并到新数组的位置。

1、起始状态

起始状态.png

2、比较大小,决定合并哪个序列的值到新序列中

因为A[posA]的值是1,比B[posB]的值来的小,所以,我们需要将A[posA]的值插入到新序列中,并且A序列的指针位置后移(posA++),新序列的位置也需要后移(posC++) 根据大小插入.png

3、合并完成一个序列,剩余序列直接插入新序列中

合并完成一个子序列.png

4、完成合并

完成合并.png

3.2 分而治之

3.2.1 Step1

假设我们有如下测试数据:[2, 7, 3, 5, 8, 4, 10, 9, 1] 首先,将我们的数组一分为二,得到两个子序列。 分而治之.png

3.2.2 Step2

我们发现左子序列还可以继续以上操作,于是,我们继续以上操作(沿着左边序列递归)。 继续分治.png 此刻,我们可以看出已经不能继续二分了(当什么时候便不能继续以上操作了呢,当startPos和endPos相等的时候,说明此刻已经无法继续二分了)此刻,我们得到了的两个序列,[2]和[7],那么我们就可以认为这个序列已经是完成排序的序列。因此可以直接合并。

3.2.3 Step3

完成一次排序.png 此刻我们已经完成部分片段的排序了

3.2.4 Step4

完成排序.png

3、总结

归并排序的示例代码是我的一种实现方式,可能和大家实现有一些写法上的区别(文中的所提及的左右序列均是原始数组上的一个片段,通过传入指针的位置确定),各位读者不用拘泥于此,只要掌握其核心原理,您可以尝试实现按自己的思路编码实现,总之,理解第一,切勿囫囵吞枣,死记硬背(如果你无法理解此算法的思想的话,就算背下来了随着时间的推移也会忘记)。

由于笔者水平有限,写作过程中难免出现错误,若有纰漏,请各位读者指正,请联系作者本人,邮箱404189928@qq.com,你们的意见将会帮助我更好的进步。本文乃作者原创,若转载请联系作者本人。