LeetCode 刷题日记:合并两个有序数组 —— 双指针与逆向三指针的深度剖析

65 阅读5分钟

LeetCode 刷题日记:合并两个有序数组 —— 双指针与逆向三指针的深度剖析

📌 题目链接:leetcode.cn/problems/me…
💡 难度:简单 | 标签:数组、双指针、原地算法
🧑‍💻 语言:JavaScript


🎯 一、题目再理解:不只是“合并”,而是“原地合并”

给定两个非递减排序的整数数组:

  • nums1:长度为 m + n,其中前 m 个元素是有效数据,后 n 个位置是预留的(值任意,通常为 0)。
  • nums2:长度为 n,全部为有效数据。

要求:nums2 合并到 nums1 中,使得 nums1 成为一个长度为 m + n 的非递减有序数组,且必须在 nums1 上原地完成修改,不能返回新数组。

✅ 输入输出示例

输入:
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 的后半部分虽然是空的,但前半部分的有效数据会被覆盖!

举个例子:

nums1 = [4,5,6,0,0,0] (m=3)
nums2 = [1,2,3]       (n=3)

如果 i=0, j=0,发现 nums2[0]=1 < nums1[0]=4,
于是把 1 写入 nums1[0] → nums1 变成 [1,5,6,0,0,0]

但原来的 4 被覆盖了!后续无法恢复。

所以,正向写入会破坏未处理的数据,不可行。

那怎么办?—— 换个方向:从后往前!


🧩 三、解法一:正向双指针 + 辅助数组(逻辑清晰,但非严格原地)

虽然题目强调“原地”,但在学习阶段,先掌握标准的“双指针合并”模式很有必要。这是解决所有有序序列合并问题的基础模板。

📐 算法思想

  • 创建一个临时数组 temp
  • 使用两个指针 i(指向 nums1 有效部分)、j(指向 nums2
  • 比较 nums1[i]nums2[j],将较小者放入 temp
  • 最后将 temp 全部拷贝回 nums1

⏱️ 复杂度分析

  • 时间复杂度:O(m + n) —— 每个元素访问一次
  • 空间复杂度:O(m + n) —— 需要额外数组存储结果

✅ 优点:逻辑直观,不易出错
❌ 缺点:不符合“严格原地”要求,面试中可能被追问优化

💻 JavaScript 实现

var merge = function(nums1, m, nums2, n) {
    let i = 0, j = 0;
    let temp = [];

    // 双指针合并
    while (i < m && j < n) {
        if (nums1[i] <= nums2[j]) {
            temp.push(nums1[i]);
            i++;
        } else {
            temp.push(nums2[j]);
            j++;
        }
    }

    // 处理剩余元素
    while (i < m) {
        temp.push(nums1[i]);
        i++;
    }
    while (j < n) {
        temp.push(nums2[j]);
        j++;
    }

    // 拷贝回 nums1
    for (let k = 0; k < temp.length; k++) {
        nums1[k] = temp[k];
    }
};

🧪 调试小技巧

你可以打印 temp 来验证合并过程是否正确:

console.log("Merged temp:", temp); // 查看中间结果

🚀 四、解法二:逆向三指针(真正原地,面试高分答案)

这才是本题的灵魂解法。它巧妙利用了 nums1 末尾的“空闲空间”,通过从大到小反向填充,避免了数据覆盖。

🧠 核心洞察

nums1 的后 n 个位置是空的 → 我们可以从最后一个位置开始写入最大值,逐步向前推进,永远不会覆盖未处理的有效数据。

📌 三指针定义

指针含义初始值
inums1 有效部分的最后一个元素索引m - 1
jnums2 的最后一个元素索引n - 1
knums1 的物理末尾(待写入位置)m + n - 1

🔁 合并逻辑(关键!)

  • 比较 nums1[i]nums2[j]
  • 较大的放入 nums1[k]
  • 对应指针左移(i--j--),k--
  • 重复直到其中一个数组处理完

❓ 为什么放“较大的”?
因为我们是从最大值开始填,保证最终数组从前往后是非递减的。

🧹 收尾处理

  • 如果 nums2 还有剩余(j >= 0):说明 nums1 已空,把 nums2 剩余全填入
  • 如果 nums1 还有剩余:它们本来就在 nums1 前部,且位置正确,无需移动

⏱️ 复杂度分析

  • 时间复杂度:O(m + n) —— 每个元素最多被访问一次
  • 空间复杂度:O(1) —— 仅用三个指针,完全原地

💻 JavaScript 实现(含详细注释)

var merge = function(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];
            i--;
        } else {
            nums1[k] = nums2[j];
            j--;
        }
        k--; // 写入后,k 左移
    }

    // 如果 nums2 还有剩余元素(nums1 已处理完)
    // 把它们全部复制到 nums1 前部
    while (j >= 0) {
        nums1[k] = nums2[j];
        j--;
        k--;
    }

    // 注意:若 i >= 0(nums1 有剩余),它们已在正确位置,无需操作
};

🖼️ 图解示例(以题目样例为例)

初始状态:

nums1 = [1, 2, 3, 0, 0, 0]
               ↑        ↑
               i=2      k=5

nums2 = [2, 5, 6]
               ↑
               j=2

第1步:nums1[2]=3 < nums2[2]=6nums1[5] = 6j--, k--
第2步:3 < 5nums1[4] = 5
第3步:3 > 2nums1[3] = 3
第4步:2 == 2nums1[2] = 2(取 nums2 的)
第5步:nums1[0]=1 < nums2[0]=2nums1[1] = 2
最后:nums1[0] = 1

结果:[1,2,2,3,5,6]


🆚 五、两种解法对比总结

维度正向双指针 + 辅助数组逆向三指针(原地)
是否原地❌ 需 O(m+n) 额外空间✅ O(1) 空间
时间复杂度O(m + n)O(m + n)
代码难度简单,易理解中等,需理解方向
面试推荐度作为过渡思路强烈推荐,标准答案
适用场景允许额外空间、教学演示严格原地、性能敏感

💬 面试建议
先说出“可以用辅助数组的双指针”,然后主动说:“但考虑到空间效率,我们可以优化为原地的逆向三指针……”
这样既展示基础能力,又体现优化意识!