1. 题目
题目描述
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素是应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n。
示例
输入:nums1 = 1,2,3,0,0,0, m = 3, nums2 = 2,5,6, n = 3
输出:1,2,2,3,5,6
约束
-
nums1.length == m + n
-
nums2.length == n
-
0 <= m, n <= 200
-
1 <= m + n <= 200
-
-10^9 <= nums1i, nums2j <= 10^9
2. 最佳解题思路描述(双指针从后往前,原地 O (1) 空间)
-
设三个指针:
-
i:指向 nums1 有效数字末尾(i = m - 1) -
j:指向 nums2 末尾(j = n - 1) -
k:指向 nums1 总长度末尾填充位置(k = m + n - 1)
-
-
从后向前比较两个数组有效数字,把更大的值放到
nums1[k],对应指针向前移动; -
循环直到其中一个数组遍历完毕;
-
若 nums2 还有剩余更小数字,全部依次拷贝到 nums1 前面空位;
-
全程原地修改 nums1,不开辟额外数组,空间复杂度 O (1),时间 O (m+n)。
优势
-
无需频繁数组插入 / 删除,避免数组元素批量移动带来的时间损耗;
-
原地操作,不额外开辟数组,空间最优;
-
一次遍历完成合并,效率最高。
3. 我的可优化代码(逻辑可 AC 但性能差)
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int t = 0,t2 = 0;
for(int j=0;j<n;j++){
for(int i=t;i<m+n;i++){
if(nums1[i]>=nums2[j]){
int p = nums2[j];
nums1.insert(nums1.begin() + i, p);
t++;
i--;
break;
}
}
}
while(t<n){
int q = nums2[t];
nums1.insert(nums1.begin()+t+m,q);
t++;
}
nums1.erase(nums1.begin() + n+m, nums1.begin() + n+m+n);
}
};
代码说明
-
逻辑正确性:
外层遍历 nums2 每个数字,内层从上次插入位置向后查找第一个大于等于当前 nums2 j 的位置,执行 insert 插入;
若 nums2 剩余数字全部大于 nums1 所有有效数,直接追加到末尾;
最后 erase 删除末尾原本占位的 n 个 0,最终数组有序,可以通过全部测试用例。
-
存在的缺陷与可优化点:
-
性能极差:
vector::insert会导致插入点后所有元素批量后移,嵌套循环时间复杂度接近 O ((m+n)*n),数据量大时超时; -
额外内存开销:insert 会频繁扩容、拷贝数组元素;
-
冗余操作:最后执行 erase 删除占位 0,多一次数组截断操作;
-
双重循环嵌套,代码冗长,可读性差。
-
4. 最优标准代码(双指针逆序原地合并)
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int i = m - 1; // nums1有效末尾
int j = n - 1; // nums2末尾
int k = m + n - 1; // 填充位置末尾
while(i >= 0 && j >= 0){
if(nums1[i] > nums2[j]){
nums1[k] = nums1[i];
i--;
}else{
nums1[k] = nums2[j];
j--;
}
k--;
}
// nums2剩余数字全部拷贝到前面
while(j >= 0){
nums1[k] = nums2[j];
j--;
k--;
}
}
};
5. 总结
-
我的代码逻辑正确可 AC,但属于暴力插入写法,依靠 vector 插入 API 完成合并,时间、空间性能差,不推荐面试使用;
-
本题最优解法核心:从后往前双指针,利用 nums1 尾部预留空位,避免元素反复移位;
-
关键记忆点:
-
数组插入
insert是重操作,算法题尽量避免频繁嵌套插入; -
两个有序数组合并优先考虑双指针;
-
nums1 尾部有空白时,逆序填充可以完美规避覆盖未读取数据的问题。
-
-
适用场景区分:
-
刷题随便过用你的插入写法;
-
面试、追求性能必须使用逆序双指针版本。
-
6. 相关知识拓展
拓展 1:正向双指针需要额外数组
如果从前向后合并,nums1 前面数字会被覆盖丢失,必须新建临时数组存储结果,空间 O (m+n),不如逆序方案。
拓展 2:vector insert/erase 底层代价
vector 连续内存,中间插入会移动后方全部元素,时间 O (N);尾部插入无移位代价。本题你在数组中间频繁 insert,是性能瓶颈根源。
拓展 3:另一种偷懒写法(库函数,面试不推荐)
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
for(int i=0;i<n;i++) nums1[m+i] = nums2[i];
sort(nums1.begin(),nums1.end());
}
直接把 nums2 复制到 nums1 尾部再整体排序,代码极简,但排序时间 O ((m+n) log (m+n)),效率低于双指针。
拓展 4:复杂度对比
-
我的插入写法:时间 O (n*(m+n)),空间不稳定(vector 扩容);
-
sort 偷懒写法:时间 O ((m+n) log (m+n)),空间 O (1);
-
逆序双指针最优解:时间 O (m+n),空间 O (1)。