力扣解题-88. 合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1: 输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 输出:[1,2,2,3,5,6] 解释:需要合并 [1,2,3] 和 [2,5,6] 。 合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2: 输入:nums1 = [1], m = 1, nums2 = [], n = 0 输出:[1] 解释:需要合并 [1] 和 [] 。 合并结果是 [1] 。
示例 3: 输入:nums1 = [0], m = 0, nums2 = [1], n = 1 输出:[1] 解释:需要合并的数组是 [] 和 [1] 。 合并结果是 [1] 。 注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
提示: nums1.length == m + n nums2.length == n 0 <= m, n <= 200 1 <= m + n <= 200 -109 <= nums1[i], nums2[j] <= 109
第一次解答
解题思路
核心方法:合并后整体排序,先将nums2的元素直接填充到nums1的末尾空闲位置,再对整个nums1数组进行排序,逻辑极简但未利用数组“有序”的特性,存在性能浪费。
具体步骤:
- 填充nums2到nums1:遍历nums2的每个元素(共n个),将nums2[i]赋值到nums1[m+i]的位置(nums1的前m位是有效元素,后n位是预留的0,正好用于存放nums2);
- 整体排序:调用
Arrays.sort(nums1)对合并后的nums1进行升序排序,完成非递减排列的要求。
性能劣势说明
该解法虽然实现了核心功能,但未利用题目中“两个数组均为非递减顺序”的关键条件,导致性能较差:
- 时间复杂度较高:排序的时间复杂度为
O((m+n)log(m+n)),而利用有序特性的最优解法可做到O(m+n),对于m+n=200的场景,排序的额外开销会导致耗时增加(4ms仅击败15.63%用户); - 冗余操作:明明两个数组已有序,却重新对所有元素排序,相当于“抛弃已知有序信息,从头排序”,是对题目条件的浪费;
- 内存表现一般:虽然无额外空间开销,但排序过程中JVM的排序算法(双轴快排)会产生少量临时空间,导致内存消耗43.2MB仅击败36.95%用户。
执行耗时:4 ms,击败了15.63% 的Java用户 内存消耗:43.2 MB,击败了36.95% 的Java用户
public void merge(int[] nums1, int m, int[] nums2, int n) {
for (int i = 0; i < n; i++) {
nums1[m+i] = nums2[i];
}
Arrays.sort(nums1);
}
第二次解答
解题思路
核心方法:逆向双指针(从后往前合并),充分利用两个数组“非递减有序”的特性,从最大值开始比较,将较大值依次放入nums1的末尾,避免覆盖未处理的有效元素,时间复杂度优化至O(m+n),是本题的最优解法。
核心原理铺垫
由于nums1的末尾有n个空闲位置,且两个数组均为升序排列,从后往前合并 可避免“正向合并时覆盖nums1前m位有效元素”的问题:
- 正向合并需额外空间暂存nums1的元素(否则会被覆盖),而逆向合并直接利用nums1的空闲末尾,无需额外空间;
- 比较两个数组的“当前最大值”,将更大的数放入nums1的当前最末尾,逐步向前填充,最终得到有序数组。
具体步骤
- 边界判断:若n=0(nums2为空),无需处理,直接返回(避免无效的指针操作);
- 初始化逆向双指针:
p1 = m - 1:指向nums1有效元素的最后一个位置(即nums1的最大值位置);p2 = n - 1:指向nums2的最后一个位置(即nums2的最大值位置);p = m + n - 1:指向nums1的最后一个位置(即合并后元素的写入位置);
- 逆向合并核心循环(
p1 >= 0 && p2 >= 0):- 比较nums1[p1]和nums2[p2]的大小,取较大值写入nums1[p];
- 若nums1[p1] >= nums2[p2]:将nums1[p1]写入nums1[p],p1左移一位(p1--),继续比较nums1的下一个最大值;
- 否则:将nums2[p2]写入nums1[p],p2左移一位(p2--),继续比较nums2的下一个最大值;
- 每次写入后,p左移一位(p--),准备写入下一个元素;
- 处理nums2剩余元素:若循环结束后p2 >= 0(说明nums2还有未合并的元素,且这些元素均小于nums1已合并的所有元素),将nums2[p2]依次写入nums1[p],直到p2 < 0;
- 无需处理nums1的剩余元素:因为nums1的剩余元素本就位于nums1的前半部分,且已是非递减顺序,无需额外操作。
核心优化逻辑说明
- 时间复杂度最优:仅需一次遍历(m+n次比较/赋值操作),时间复杂度为O(m+n),相比排序的O((m+n)log(m+n)),效率提升显著(0ms击败100%用户);
- 空间复杂度O(1):全程在nums1原数组上操作,无额外数组/集合创建,内存消耗降至42.9MB,击败90.89%用户;
- 避免元素覆盖:逆向合并的核心优势是“写入位置在未处理元素的后方”,不会覆盖nums1前m位的有效元素,无需额外空间暂存数据;
- 边界处理完善:通过
if(n!=0)跳过nums2为空的情况,通过最后的p2循环处理nums2剩余元素,覆盖了m=0(nums1无有效元素)等极端场景。
执行耗时:0 ms,击败了100.00% 的Java用户 内存消耗:42.9 MB,击败了90.89% 的Java用户
public void merge(int[] nums1, int m, int[] nums2, int n) {
if(n!=0) {
int p1 = m - 1; // nums1 有效部分的最后一个索引
int p2 = n - 1; // nums2 最后一个索引
int p = m + n - 1; // nums1 总数组的最后一个索引(写入位置)
while (p1 >= 0 && p2 >= 0) {
int a = nums1[p1];
int b = nums2[p2];
if (a >= b) {
nums1[p] = a;
p1--;
} else {
nums1[p] = b;
p2--;
}
p--;
}
//如果nums2的元素还有剩余,全部挪过去
while (p2 >= 0) {
nums1[p] = nums2[p2];
p--;
p2--;
}
}
}
总结
- 第一次解答的“合并后排序”思路极简,但未利用数组“有序”的特性,时间复杂度较高,仅适合快速实现功能的场景;
- 第二次解答的“逆向双指针”是本题的最优解:利用有序特性将时间复杂度优化至O(m+n),且原地操作无额外空间开销,是对题目条件的充分利用;
- 本题的核心解题技巧是逆向思维:避开正向合并的“覆盖风险”,从最大值开始填充,既利用nums1的空闲空间,又保证有序性。