LeetCode 88. 合并两个有序数组:从基础到最优的两种解法详解

15 阅读8分钟

在有序数组的操作场景中,“合并两个有序数组”是经典基础题,也是面试高频考点。这道题看似简单,但要写出高效且符合“原地修改”要求的代码,需要仔细梳理边界条件和优化思路。本文将从题目分析入手,先讲解直观的“从前往后”解法,再优化到更高效的“从后往前”解法,带你完整掌握这道题的核心逻辑。

一、题目核心要求拆解

先明确题目给出的条件和限制,避免解题时踩坑:

  • 输入:两个非递减排序的整数数组 nums1、nums2;m 是 nums1 中有效元素个数,n 是 nums2 中元素个数。

  • 关键限制:合并后的结果需存储在 nums1 中(原地修改,不能返回新数组),nums1 初始长度为 m+n,后 n 个元素是占位的 0,需忽略。

  • 输出要求:合并后的 nums1 仍保持非递减顺序。

核心难点:既要保证原地修改不占用额外空间(或极少额外空间),又要处理好两个数组的指针移动和元素插入时的边界问题。

二、解法一:从前往后遍历(直观思路)

这是最容易想到的思路,类似我们手动合并两个有序列表的过程:用两个指针分别遍历 nums1 和 nums2 的有效元素,比较大小后依次插入 nums1 中。

1. 核心思路

  • 双指针初始化:pointer1 指向 nums1 有效元素的起始位置(0),pointer2 指向 nums2 的起始位置(0)。

  • 有效长度维护:nums1 的有效长度会随 nums2 元素的插入动态增加(初始为 m,每插入一个 nums2 元素就加 1),可用 m + pointer2 表示(pointer2 是已插入的 nums2 元素个数)。

  • 比较与插入:

    • 若 nums1[pointer1] ≤ nums2[pointer2],说明 nums1 当前元素更小,pointer1 后移继续遍历。

    • 若 nums1[pointer1] > nums2[pointer2],需要先将 nums1 中 pointer1 及之后的有效元素后移一位,再将 nums2[pointer2] 插入到 pointer1 位置,最后两个指针都后移。

  • 兜底处理:若 nums2 还有剩余元素未插入,直接将剩余元素追加到 nums1 末尾(此时 nums1 前面的有效元素已遍历完)。

2. 完整代码(TypeScript)

/**
 Do not return anything, modify nums1 in-place instead.
 */
function merge(nums1: number[], m: number, nums2: number[], n: number): void {
  // 双指针分别指向nums1和nums2的起始位置
  let pointer1: number = 0, pointer2: number = 0;
  // m + pointer2 表示nums1当前的有效长度(每插入一个nums2元素,有效长度+1)
  while (pointer1 < m + pointer2 && pointer2 < n) {
    // 比较当前两个指针指向的元素,小于等于则nums1指针后移
    if (nums1[pointer1] <= nums2[pointer2]) {
      pointer1++;
    } else {
      // 元素后移:从当前有效长度的最后一位开始,直到pointer1位置
      for (let i = m + pointer2; i > pointer1; i--) {
        nums1[i] = nums1[i - 1];
      }
      // 插入nums2的当前元素
      nums1[pointer1] = nums2[pointer2];
      // 两个指针都后移,准备下一次比较
      pointer1++;
      pointer2++;
    }
  }
  // 若nums2还有剩余元素,直接追加到nums1末尾
  if (pointer2 < n) {
    for (let i = 0; i < n - pointer2; i++) {
      nums1[pointer1 + i] = nums2[pointer2 + i];
    }
  }
};

3. 关键细节与边界处理

  • 循环条件设计:pointer1 < m + pointer2 && pointer2 < n,确保只有当两个数组都有未遍历的有效元素时才进入比较,避免指针越界。

  • 元素后移边界:后移时从 m + pointer2(当前有效长度)开始,而不是 nums1 的末尾(m+n-1),减少无意义的 0 元素移动,提升效率。

  • 剩余元素兜底:当 pointer2 < n 时,说明 nums2 还有元素没插入,此时 nums1 的有效元素已遍历完,直接批量追加即可。

4. 优缺点分析

  • 优点:思路直观,符合人类手动合并的逻辑,容易理解和实现。

  • 缺点:元素后移操作导致时间复杂度较高。最坏情况下(nums2 所有元素都比 nums1 小,需要每次插入都后移 nums1 所有元素),时间复杂度为 O(m*n);空间复杂度为 O(1)(原地修改,无额外空间占用)。

三、解法二:从后往前遍历(最优思路)

解法一的核心问题是“元素后移”导致效率低。那有没有办法避免后移操作?答案是从后往前遍历——利用 nums1 末尾的 n 个 0 占位空间,直接从两个数组的末尾开始比较,将更大的元素放到 nums1 的末尾,彻底省去后移步骤。

1. 核心思路

  • 双指针初始化:与解法一相反,pointer1 指向 nums1 有效元素的末尾(m-1),pointer2 指向 nums2 的末尾(n-1);再用一个 pointer 指向 nums1 的最终末尾(m+n-1),用于存放合并后的元素。

  • 反向比较与填充:

    • 比较 nums1[pointer1] 和 nums2[pointer2] 的大小,将更大的元素放到 pointer 位置。

    • 放入元素后,对应的指针(pointer1 或 pointer2)和填充指针(pointer)都向前移动一位。

  • 终止条件:当 pointer2 < 0 时,说明 nums2 所有元素都已插入,循环终止。此时 nums1 中未遍历的有效元素本身就是有序的,无需再处理。

2. 完整代码(TypeScript)

/**
 Do not return anything, modify nums1 in-place instead.
 */
function merge(nums1: number[], m: number, nums2: number[], n: number): void {
  // pointer1:nums1有效元素末尾指针;pointer2:nums2末尾指针;pointer:nums1最终填充末尾指针
  let pointer1: number = m - 1, pointer2: number = n - 1, pointer = m + n - 1;

  // 只要nums2还有元素未处理,就继续循环
  while (pointer2 >= 0) {
    // 若nums1还有有效元素,且当前元素大于nums2的元素,则将nums1的元素放入最终位置
    if (pointer1 >= 0 && nums1[pointer1] > nums2[pointer2]) {
      nums1[pointer] = nums1[pointer1];
      pointer1--;
    } else {
      // 否则将nums2的元素放入最终位置(包括nums1无有效元素的情况)
      nums1[pointer] = nums2[pointer2];
      pointer2--;
    }
    // 填充指针向前移动
    pointer--;
  }
};

3. 关键细节与边界处理

  • pointer1 越界处理:当 pointer1 < 0 时,说明 nums1 的有效元素已全部处理完,此时直接将 nums2 剩余元素依次放入 pointer 位置即可。

  • 无需后移的核心原因:利用了 nums1 末尾的 0 占位空间,反向填充时不会覆盖未处理的有效元素,因此省去了解法一中的后移步骤。

  • 循环条件简化:只需判断 pointer2 >= 0,因为 nums2 处理完后,合并任务就完成了,nums1 剩余元素本身有序。

4. 优缺点分析

  • 优点:时间复杂度优化到 O(m+n)(最优复杂度),每个元素只需遍历一次;空间复杂度仍为 O(1),原地修改效率极高。

  • 缺点:思路不如解法一直观,需要转换思维(从反向遍历的角度思考),对初学者来说可能需要多梳理几遍指针移动逻辑。

四、两种解法对比总结

对比维度解法一(从前往后)解法二(从后往前)
时间复杂度O(m*n)(最坏情况)O(m+n)(最优)
空间复杂度O(1)O(1)
核心优势思路直观,易理解效率极高,无冗余操作
适用场景初学者理解基础逻辑面试最优解、生产环境

五、常见踩坑点提醒

  • 指针越界:解法一中未正确处理 pointer2 越界时的 undefined 比较,解法二中未判断 pointer1 < 0 导致的数组访问错误。

  • 有效长度错误:解法一中误用 nums1 初始长度 m 作为后移边界,未动态维护有效长度(m+pointer2)。

  • 元素覆盖:解法一中未后移就直接插入元素,导致 nums1 未处理的有效元素被覆盖。

  • 剩余元素遗漏:两种解法都可能忘记处理 nums2 剩余元素的兜底逻辑(解法一已补充,解法二因循环条件无需额外处理)。

六、总结

LeetCode 88 题的核心是“原地合并有序数组”,解题的关键在于指针的设计和边界条件的处理。解法一适合初学者建立基础逻辑,解法二则是最优解,通过反向遍历的技巧避免了冗余的元素后移操作,大幅提升效率。

建议大家先理解解法一的直观思路,再思考“如何优化后移操作”,从而自然过渡到解法二的反向遍历思路。掌握这两种解法后,不仅能解决这道题,还能举一反三应对其他“有序数组操作”类题目(如合并 k 个有序数组、有序数组去重等)。