归并排序

归并排序

核心思想

归并排序,英语 Merge Sort,这是一种基于 分治法 的排序算法。它的基本思想是将一个大的数组分成若干个小的数组,直到每个小数组只有一个元素为止。然后再将这些小数组合并成一个有序的数组

归并排序工作流程:

  1. 分割:将原始数组分成两个部分,递归的将每一个部分的数组继续分割,直到每个部分只包含一个元素。

image-20250211162946409

  1. 合并:将分割好的部分,两两合并,合并的时候保持顺序,最终合并成一个有序的数组。
image-20250211163545350

“拆分”非常好理解,每次砍一半。

关键是“合并”,究竟是如何合并成有序的?

合并过程

那么这里以最后一步为例:[4, 5, 7, 8][1, 2, 3, 6] 进行合并

  1. 定义两个指针,第一个指向第一个有序数组的第一个元素;第二个指向第二个有序数组的第一个元素。需要创建一个临时数组,临时数组的长度 = 两个数组长度之和。两个有序数组的第一个元素进行比较,谁小,谁就放入到临时数组的一个元素位置

    image-20250211164620643

    放入临时数组之后,j 右移指向第二个有序数组的第二个元素。

  2. 继续 i 和 j 对应的元素进行比较,4 和 2 比较,仍然是 2 比较小,将 2 放入到 temp 数组的第二个元素的位置。

    image-20250211164749800

    接下来 j 继续往右边移动,指向第二个有序数组的第三个元素。

  3. 继续 i 和 j 对应的元素进行比较,4 和 3 比较,仍然是 3 比较小,将 3 放入到 temp 数组的第三个元素的位置。

    image-20250211164901388

    接下来 j 继续往右边移动,指向第二个有序数组的第四个元素。

  4. 继续 i 和 j 对应的元素进行比较,这一次就是 4 和 6 进行比较,这一次是 4 是比较小,将 4 放入到 temp 数组的第四个元素的位置

    image-20250211165208876

    接下来就是 i 继续往右边移动,指向第一个有序数组的第二个元素。

  5. 继续 i 和 j 对应的元素进行比较,这一次是 5 和 6 进行比较,这一次是 5 是比较小,将 5 放入到 temp 数组的第五个元素的位置

    image-20250211165347427

    接下来就是 i 继续往右边移动,指向第一个有序数组的第三个元素。

  6. 接下来继续上面的操作,这一次是 7 和 6 进行比较,6 比较小,将 6 放入到 temp 数组的第六个元素的位置

    image-20250211165657533

    这一次应该是 j 往右边移动。但是这一次 j 已经无法往右边移动。说明第二个有序数组所有的元素已经遍历结束。

  7. 只需要将第一个有序数组的剩余元素,全部放入到 temp 数组里面即可

    image-20250211170235947

  8. 最后,再将 temp 这个临时数组里面所有的元素拷贝到原数组中,合并结束

    image-20250211170506634

代码实现

// 这个方法负责合并
function merge(left, right) {
  let result = []; // 临时数组
  let leftIndex = 0; // 左数组的索引,默认指向第一个元素
  let rightIndex = 0; // 右数组的索引,默认指向右数组的第一个元素

  while (leftIndex < left.length && rightIndex < right.length) {
    // 里面就判断究竟是左边数组的元素小还是右边数组元素小
    // 将小的元素放入到临时数组里面
    if (left[leftIndex] <= right[rightIndex]) {
      // 说明左边的元素更小
      // 将左边元素放入到临时数组里面
      result.push(left[leftIndex]);
      leftIndex++;
    } else {
      // 说明右边元素更小
      // 将右边元素放入到临时数组里面
      // 先运行表达式再计算值,即先result.push(right[rightIndex])再rightIndex++,作用同上
      result.push(right[rightIndex++]);
    }
  }

  // 代码来到这里,说明 left 或者 right 总之有一边已经结束了
  // 将另外一个数组剩余的所有元素放入到临时数组里面
  if (leftIndex < left.length) {
    // 说明左边数组有剩余
    result = result.concat(left.slice(leftIndex));
  }

  if (rightIndex < right.length) {
    // 说明右边数组有剩余
    result = result.concat(right.slice(rightIndex));
  }

  return result;
}

// 这个方法主要是负责拆
function mergeSort(arr) {
  if (arr.length < 2) {
    // 如果进入此分支,说明数组不能够再拆了,该数组已经是单独的一个元素了
    // 直接返回该数组
    // 这个其实就是递归的出口
    return arr;
  }

  // 如果没有进入上面的 if,那么就需要拆分数组,就是对半分
  const mid = Math.floor(arr.length / 2);

  // 根据上面计算出来的中间值,将整个数组分为左半部分和右半部分
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));

  // 下一步,就是合并左右两个子数组
  return merge(left, right);
}

const arr = [38, 27, 43, 3, 9, 82, 10];
const sortedArr = mergeSort(arr);
console.log(sortedArr);

复杂度

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n),因为在归并排序中,需要额外的空间来存储临时数组用于合并操作。

稳定性

归并排序是一种 稳定 排序。

如果遇到相等的元素,是优先将左边数组的元素放入临时数组。

if (left[leftIndex] <= right[rightIndex]) {
  // 说明左边的元素更小
  // 将左边元素放入到临时数组里面
  result.push(left[leftIndex]);
  leftIndex++;
} else {
  // 说明右边元素更小
  // 将右边元素放入到临时数组里面
  result.push(right[rightIndex++]);
}

关键就是 left[leftIndex] <= right[rightIndex],因此在左右数组元素相等的情况下,优先放入左边数组的元素。

假设修改一下 left[leftIndex] < right[rightIndex],这么一改,就变成优先取右边数组的元素放入临时数组,就会导致不稳定。


-EOF-


归并排序
http://localhost:8090/archives/gui-bing-pai-xu
作者
Administrator
发布于
2026年01月30日
许可协议