合并两个有序数组为什么要从后往前写?如何做到原地合并?
简化版
原地合并两个有序数组时,应该从后往前写。
因为 nums1 前半段保存着有效元素,从前往后写会覆盖还没比较的值;从后往前写则使用预留空间,不会破坏未处理数据。
用 i 指向 nums1 有效末尾,j 指向 nums2 末尾,k 指向总数组末尾,每次放较大的数。
详细版
假设 nums1 长度是 m + n,前 m 个元素有效,后 n 个位置留给合并结果。
设置:
i = m - 1j = n - 1k = m + n - 1
每次比较 nums1[i] 和 nums2[j],把较大的放到 nums1[k],对应指针左移。
如果 nums2 还有剩余,需要继续拷贝;如果 nums1 还有剩余,不用处理,因为它已经在正确位置。
时间复杂度 O(m+n),额外空间 O(1)。
完整版教学
一、为什么从前往后会覆盖数据
原地合并的难点不是合并逻辑,而是 nums1 同时承担输入和输出。若从前往后写,nums1[0] 可能还没来得及和 nums2 比较,就被新值覆盖。覆盖后原始信息丢失,后续无法恢复。
例如 nums1 = [2,5,0,0],nums2 = [1,3]。如果从前写,第一位应该放 1,但会覆盖掉 2,而 2 之后还要参与比较。
记忆钩子:原地数组合并时,哪里有空位,就优先从哪里写。
二、为什么从后往前安全
nums1 后面有 n 个空位,最终合并结果的最大值也应该在最后。我们从后往前放较大的数,写入的是空位或已经处理完的位置,不会覆盖还没用过的有效元素。这个方向正好利用了有序数组“最大值在末尾”的性质。
| 写入方向 | 是否覆盖有效数据 | 是否利用预留空间 |
|---|---|---|
| 从前往后 | 容易覆盖 | 不充分 |
| 从后往前 | 不覆盖 | 充分利用 |
所以三指针从后合并是这题最稳的做法。
三、三指针分别表示什么
三个指针的语义要清楚:
i:nums1 中尚未合并的最后一个有效元素
j:nums2 中尚未合并的最后一个元素
k:nums1 中下一次写入的位置
每轮比较 nums1[i] 和 nums2[j],谁大就放到 nums1[k]。放完后,对应指针左移,k 也左移。循环保持“不变式”:k 右侧已经是最终有序结果。
四、代码模板
实现如下:
function merge(nums1, m, nums2, n) {
let i = m - 1
let j = n - 1
let k = m + n - 1
while (j >= 0) {
if (i >= 0 && nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--]
} else {
nums1[k--] = nums2[j--]
}
}
}
外层只需要 while (j >= 0)。如果 nums1 还有剩余,它们本来就在前面正确位置;如果 nums2 有剩余,必须拷贝进去。
五、带数字推演
以 nums1 = [1,2,3,0,0,0]、m = 3、nums2 = [2,5,6]、n = 3 为例:
i=2(3), j=2(6), k=5 -> 放 6
i=2(3), j=1(5), k=4 -> 放 5
i=2(3), j=0(2), k=3 -> 放 3
i=1(2), j=0(2), k=2 -> 放 2
最后得到 [1,2,2,3,5,6]。
六、常见误区与追问
- 误区:从前往后合并。 会覆盖
nums1中尚未比较的有效元素。 - 误区:循环条件写成 i >= 0 && j >= 0。 这样会漏掉
nums2剩余元素。 - 误区:nums1 剩余也强行拷贝。 nums1 剩余已经在正确位置,不需要额外处理。
- 追问:如果没有预留空间怎么办? 就不能原地从后写,需要新数组或扩容结构。
- 追问:稳定性重要吗? 这类题通常只要求结果有序;若要求稳定,要明确相等时优先放哪个数组的元素。
这些点体现的是对“输入输出共用数组”的理解。
七、加强记忆
合并两个有序数组记成“三指针从后塞大数”。i 看 nums1 有效末尾,j 看 nums2 末尾,k 看最终写入位置。因为 nums1 尾部有空位,最大值也应该在尾部,所以从后写既符合顺序,又避免覆盖。最后只要 nums2 还有元素就继续拷贝,nums1 剩余不用动。