【LeetCode 刷题笔记】:合并两个有序数组——掌握三指针原地合并的核心思想

31 阅读5分钟

【LeetCode 刷题笔记】:合并两个有序数组——掌握三指针原地合并的核心思想

题目来源第 88 题(Merge Sorted Array)
难度:Easy → Medium
标签:数组、双指针、三指针、原地操作
Hot 100 必刷指数:⭐⭐⭐⭐⭐ image.png


在 LeetCode 的数组类高频题中,「合并两个有序数组」是一道看似简单却极具教学意义的经典题。它不仅考察你对有序结构的理解,更深入检验你对空间优化指针移动策略的掌握。

本文将带你:

  1. 理解题目陷阱:nums1 已预留空间但含“垃圾值”;
  2. 分析朴素解法为何浪费空间;
  3. 揭秘三指针从后往前合并的巧妙思路;
  4. 手把手实现 O(1) 额外空间、O(m+n) 时间 的最优解。

🔹 1. 题目解析:不只是“合并”,而是“原地合并”

📌 题目简述

给你两个非递减整数数组 nums1nums2,以及两个整数 mn,分别表示 nums1nums2 中的有效元素个数。

  • nums1 的长度为 m + n,其中前 m 个元素是有效数据,后 n 个位置为 0(预留空间)。
  • 要求:将 nums2 合并到 nums1 中,使 nums1 成为一个非递减的有序数组。
  • 关键限制:必须原地修改 nums1,不能返回新数组。

💡 示例:

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

🔸 2. 朴素解法:双指针从前向后(但需额外空间)

✅ 思路

  • 使用两个指针 ij 分别指向 nums1nums2 的开头;
  • 比较 nums1[i]nums2[j],将较小者放入新数组
  • 最终将新数组拷贝回 nums1

❌ 问题:空间复杂度 O(m+n)

function merge(nums1, m, nums2, n) {
    let i = 0, j = 0;
    const merged = [];
    while (i < m && j < n) {
        if (nums1[i] <= nums2[j]) merged.push(nums1[i++]);
        else merged.push(nums2[j++]);
    }
    while (i < m) merged.push(nums1[i++]);
    while (j < n) merged.push(nums2[j++]);
    for (let k = 0; k < merged.length; k++) {
        nums1[k] = merged[k];
    }
}
  • 优点:逻辑清晰,易于理解;
  • 缺点违背“原地”要求的精神,且 LeetCode 明确希望你利用 nums1 的尾部空间。

💡 小结:从前向后合并 → 需要 O(n) 额外空间


🔧 3. 关键洞察:为什么“从后往前”能省空间?

🤔 核心观察

  • nums1后 n 个位置是空的(0) ,不会被覆盖;
  • 如果我们从大到小合并(即从两个数组的末尾开始),那么写入的位置(nums1 的尾部)始终是安全的,不会覆盖未处理的有效元素。

✅ 三指针策略

  • i = m - 1:指向 nums1 有效部分的最后一个元素;
  • j = n - 1:指向 nums2 的最后一个元素;
  • k = m + n - 1:指向 nums1 的最后一个位置(写入位);

每次比较 nums1[i]nums2[j],将较大者写入 nums1[k],然后对应指针左移。

💡 为什么是“较大者”?因为我们从后往前构建降序填充,最终整体仍是升序!


🔥 4. 三指针原地合并:最优解实现

💻 完整代码(带详细注释)

function merge(nums1, m, nums2, n) {
    // 三指针:i 指向 nums1 有效尾,j 指向 nums2 尾,k 指向 nums1 总尾
    let i = m - 1;
    let j = n - 1;
    let k = m + n - 1;

    // 从后往前合并,直到其中一个数组耗尽
    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--;
    }

    // 如果 nums2 还有剩余(说明 nums1 已空),直接复制
    while (j >= 0) {
        nums1[k] = nums2[j];
        j--;
        k--;
    }

    // 注意:如果 nums1 有剩余,无需操作,因为它们已在正确位置!
}

🧪 测试用例

let nums1 = [1,2,3,0,0,0], m = 3;
let nums2 = [2,5,6], n = 3;
merge(nums1, m, nums2, n);
console.log(nums1); // [1,2,2,3,5,6]

nums1 = [1], m = 1;
nums2 = [], n = 0;
merge(nums1, m, nums2, n);
console.log(nums1); // [1]

nums1 = [0], m = 0;
nums2 = [1], n = 1;
merge(nums1, m, nums2, n);
console.log(nums1); // [1]

✅ 特别注意:nums1 先耗尽时,必须处理 nums2 剩余元素;但若 nums2 先耗尽,则 nums1 剩余元素已在正确位置,无需移动!


⏱️ 复杂度分析

方法时间复杂度空间复杂度
朴素双指针(新数组)O(m+n)O(m+n)
三指针原地合并O(m+n)O(1)
  • 时间:每个元素最多被访问一次;
  • 空间:仅使用常数个指针变量,真正原地操作

🧠 思维升华:从“合并”到“空间复用”的范式转变

维度普通合并(如归并排序)本题(原地合并)
目标生成新有序数组原地修改已有数组
空间假设可申请新空间不能申请新数组
指针方向从前向后从后向前
核心技巧双指针顺序写入三指针逆序写入

🌟 启示

  • 当目标数组尾部有空闲空间时,逆序处理是避免覆盖的关键;
  • 这一思想可扩展至:合并 K 个有序链表(数组变体)原地归并排序优化等场景;
  • “预留空间”不是摆设,而是解题的突破口!

✅ 总结

  • 不要被“0”迷惑nums1n 个 0 是预留位,不是有效数据;
  • 从前向后会覆盖:必须从后往前,利用空闲空间;
  • 三指针是标配ij 读,k 写,分工明确;
  • 边界处理要小心:只需处理 nums2 剩余,nums1 剩余天然就位。

掌握这道题,你就掌握了原地操作空间复用的精髓。它虽是 Easy 难度,却是面试官最爱的“细节题”!

📌 延伸练习

  • LeetCode 912. 排序数组(思考如何用类似思想优化归并)
  • LeetCode 21. 合并两个有序链表(链表版,无空间限制)
  • LeetCode 23. 合并 K 个升序链表(进阶)

刷题不在多,而在精。一道题吃透,胜过十道囫囵吞枣。