LeetCode 88 题深度解析:为什么聪明人都从后往前合并?

100 阅读4分钟

合并两个有序数组:从生活场景到高效算法

如果说算法是程序员的“内功”,那么双指针技巧,无疑是初入算法世界时最先掌握的一套“基础拳法”——简洁、实用,又充满巧思。

在众多 JavaScript 算法题中,有这样一个经典问题,看似平平无奇,却藏着对空间与顺序的精妙考量:88. 合并两个有序数组 - 力扣(LeetCode) 尤其当题目要求“原地修改”,不允许额外开辟新数组时,真正的功夫才刚刚开始。


问题背景:LeetCode 经典题「合并两个有序数组」

假设你有两个整数数组 nums1nums2,它们都已经按非递减顺序排列(即从小到大)。其中,nums1 的长度是 m + n,前 m 个元素是有效数据,后面 n 个位置是预留的空位(值为0或未定义);而 nums2 的长度正好是 n。我们的目标是:nums2 的所有元素合并进 nums1 中,使得最终 nums1 包含全部 m + n 个元素,并且仍然保持有序

例如:

nums1 = [1, 2, 3, 0, 0, 0], m = 3
nums2 = [2, 5, 6],           n = 3
// 合并后应为:
nums1 = [1, 2, 2, 3, 5, 6]

注意:不能创建新数组!必须原地修改 nums1。这就对空间效率提出了挑战。


初学者思路:从前向后双指针?

很多初学者的第一反应是:既然两个数组都已排序,那我用两个指针分别从头开始比较,把小的放进结果数组。这确实可行,但有个致命问题:如果直接往 nums1 前面写入,会覆盖掉还没处理的原始数据!

想象一下:你正在整理两叠书,左边一叠是你自己的(nums1),右边一叠是借来的(nums2)。你想把它们合并成一叠放在左边的书架上。如果你从最前面开始放,就得先把原来的书挪走,否则新书会压住旧书,导致信息丢失。这显然很麻烦。

所以,从前向后操作,在原地修改时不可行


聪明做法:从后往前,三指针登场!

既然“前面不能动”,那我们就从后面开始填!因为 nums1 的末尾有 n 个空位,正好可以用来存放最大的元素,而且不会影响前面的有效数据。

这就是三指针法的核心思想:

  • 指针 i:指向 nums1 中最后一个有效元素(索引 m - 1
  • 指针 j:指向 nums2 的最后一个元素(索引 n - 1
  • 指针 k:指向 nums1 的最后一个位置(索引 m + n - 1

我们从两个数组的“尾巴”开始比较,谁大,就把谁放到 k 的位置,然后对应的指针向前移动。这样,每一步都在安全区域操作,绝不会覆盖未处理的数据。

来看代码实现:

function merge(nums1, m, nums2, n) {
  let i = m - 1;     // nums1 有效部分的末尾
  let j = n - 1;     // nums2 的末尾
  let k = m + n - 1; // nums1 整体的末尾(待填充位置)

  // 只要两个数组都还有元素,就继续比较
  while (i >= 0 && j >= 0) {
    if (nums1[i] > nums2[j]) {
      nums1[k] = nums1[i]; // 把 nums1 的大元素放到后面
      i--;                 // i 向前移
    } else {
      nums1[k] = nums2[j]; // 把 nums2 的大元素放到后面
      j--;                 // j 向前移
    }
    k--; // k 总是向前移,准备放下一个最大值
  }

  // 如果 nums2 还有剩余元素(说明 nums1 已经处理完)
  while (j >= 0) {
    nums1[k] = nums2[j];
    j--;
    k--;
  }
  // 注意:如果 nums1 有剩余,其实不用处理,因为它们已经在正确位置了!
}

这段代码精巧而高效,时间复杂度是 O(m + n)空间复杂度是 O(1) —— 没有使用任何额外数组,完全原地操作。


为什么不需要处理 nums1 剩余的情况?

细心的朋友可能会问:代码里只处理了 nums2 剩余的情况,那如果 nums1 还有元素没处理完怎么办?

答案是:不需要处理!

因为 nums1 本身就是目标数组。如果 j < 0(即 nums2 已全部放入),而 i >= 0,说明剩下的 nums1[0...i] 元素本来就比所有已放入的元素小,而且它们已经在 nums1 的前半部分——位置本来就是对的!

举个例子:

nums1 = [4, 5, 6, 0, 0, 0], m = 3
nums2 = [1, 2, 3],           n = 3
// 合并后应为
nums1 = [1, 2, 3, 4, 5, 6]

合并过程中,nums2 的 3、2、1 会被依次放到末尾,而 nums1 的 4、5、6 根本不需要移动,它们天然就在正确位置。


总结:优雅与效率的结合

通过这个“合并有序数组”的问题,我们不仅学会了一种高效的算法技巧,更体会到了逆向思维的力量。很多时候,正向思考会遇到障碍(比如数据覆盖),但换个方向——从后往前——问题迎刃而解。

三指针法之所以优秀,是因为它:

  • 充分利用了输入数据的特性(已排序 + 预留空间)
  • 避免了额外空间开销
  • 逻辑清晰,边界处理简洁