在有序数组的操作场景中,“合并两个有序数组”是经典基础题,也是面试高频考点。这道题看似简单,但要写出高效且符合“原地修改”要求的代码,需要仔细梳理边界条件和优化思路。本文将从题目分析入手,先讲解直观的“从前往后”解法,再优化到更高效的“从后往前”解法,带你完整掌握这道题的核心逻辑。
一、题目核心要求拆解
先明确题目给出的条件和限制,避免解题时踩坑:
-
输入:两个非递减排序的整数数组 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 个有序数组、有序数组去重等)。