合并两个有序数组是算法面试中的经典问题,题目要求我们将两个已经按非递减顺序排列的数组合并到第一个数组中,同时保持合并后的数组依然有序。这个问题看似简单,但其中蕴含着几个关键点需要我们特别注意:
原地操作要求:题目明确要求合并结果必须存储在nums1中,不能返回新数组。nums1的长度已经预先设置为m+n,后半部分用0填充。
边界条件处理:需要考虑m=0或n=0的情况,即其中一个数组为空时的特殊处理。
时间复杂度考量:不同的解法在时间复杂度上有显著差异,我们需要分析各种方法的优劣。
提示:在实际面试中,面试官通常会期待你首先提出基础解法,然后逐步优化,最后给出最优解。这个过程能展示你的问题解决能力和算法思维。
这是最直观的解法,思路简单直接:
java复制public void merge(int[] nums1, int m, int[] nums2, int n) {
for (int i = 0; i < n; i++) {
nums1[i + m] = nums2[i];
}
Arrays.sort(nums1);
}
时间复杂度分析:
空间复杂度:O(1),没有使用额外空间
优缺点:
在基础解法中,我们需要特别注意几种边界情况:
java复制public void merge(int[] nums1, int m, int[] nums2, int n) {
if (n == 0) return;
if (m == 0) {
System.arraycopy(nums2, 0, nums1, 0, n);
return;
}
// 正常处理逻辑...
}
注意:在实际编码中,使用System.arraycopy()比手动循环复制更高效,也更能体现Java编程的专业性。
为了利用输入数组已经有序的特性,我们可以使用双指针技术:
java复制public void merge(int[] nums1, int m, int[] nums2, int n) {
int[] nums1Copy = new int[m];
System.arraycopy(nums1, 0, nums1Copy, 0, m);
int p1 = 0; // 指向nums1Copy
int p2 = 0; // 指向nums2
int p = 0; // 指向nums1
while (p1 < m && p2 < n) {
nums1[p++] = nums1Copy[p1] < nums2[p2] ? nums1Copy[p1++] : nums2[p2++];
}
// 处理剩余元素
if (p1 < m) System.arraycopy(nums1Copy, p1, nums1, p, m - p1);
if (p2 < n) System.arraycopy(nums2, p2, nums1, p, n - p2);
}
时间复杂度:O(m+n),我们只需要遍历两个数组各一次
空间复杂度:O(m),需要nums1的副本
优缺点:
更巧妙的解法是从后往前处理,这样就不需要额外空间:
java复制public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = m - 1; // nums1有效部分末尾
int p2 = n - 1; // nums2末尾
int p = m + n - 1; // nums1末尾
while (p1 >= 0 && p2 >= 0) {
nums1[p--] = nums1[p1] > nums2[p2] ? nums1[p1--] : nums2[p2--];
}
// 如果nums2还有剩余元素
System.arraycopy(nums2, 0, nums1, 0, p2 + 1);
}
时间复杂度:O(m+n)
空间复杂度:O(1)
关键点:
实际编码技巧:在写这种指针操作时,建议先在纸上画出数组和指针位置的变化过程,这样能帮助理清思路,避免边界错误。
| 解法 | 时间复杂度 | 空间复杂度 | LeetCode运行时间 |
|---|---|---|---|
| 直接排序 | O((m+n)log(m+n)) | O(1) | ~4ms |
| 双指针(前→后) | O(m+n) | O(m) | ~0ms |
| 双指针(后→前) | O(m+n) | O(1) | ~0ms |
常规情况:
java复制nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
// 预期输出: [1,2,2,3,5,6]
nums1为空:
java复制nums1 = [0], m = 0
nums2 = [1], n = 1
// 预期输出: [1]
nums2为空:
java复制nums1 = [1], m = 1
nums2 = [], n = 0
// 预期输出: [1]
交叉情况:
java复制nums1 = [1,3,5,0,0,0], m = 3
nums2 = [2,4,6], n = 3
// 预期输出: [1,2,3,4,5,6]
包含重复元素:
java复制nums1 = [1,2,2,0,0], m = 3
nums2 = [2,3], n = 2
// 预期输出: [1,2,2,2,3]
数组越界:
元素覆盖:
剩余元素处理不当:
打印指针位置:
java复制System.out.println("p1=" + p1 + ", p2=" + p2 + ", p=" + p);
可视化数组状态:
java复制System.out.println(Arrays.toString(nums1));
边界测试:
python复制def merge(nums1, m, nums2, n):
p1, p2, p = m-1, n-1, m+n-1
while p1 >= 0 and p2 >= 0:
if nums1[p1] > nums2[p2]:
nums1[p] = nums1[p1]
p1 -= 1
else:
nums1[p] = nums2[p2]
p2 -= 1
p -= 1
nums1[:p2+1] = nums2[:p2+1]
cpp复制void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {
int p1 = m - 1, p2 = n - 1, p = m + n - 1;
while (p1 >= 0 && p2 >= 0) {
nums1[p--] = nums1[p1] > nums2[p2] ? nums1[p1--] : nums2[p2--];
}
while (p2 >= 0) {
nums1[p--] = nums2[p2--];
}
}
如果要求去重怎么办?
如果数组合并后需要保持严格递增?
如果是合并k个有序数组?
在实际开发中,合并有序数组的思想可以应用于:
掌握这类基础算法问题,不仅有助于面试,更能培养我们解决实际工程问题的思维能力。建议在理解这些解法后,尝试自己从头实现几次,直到能够流畅写出无bug的代码为止。