有序数组合并:从双指针到空间优化的算法艺术
在算法世界中, “合并两个有序数组” 是一道经典而富有启发性的问题。它不仅考察对数组操作的理解,更体现了算法设计中对 时间复杂度 与 空间复杂度 的权衡智慧。本文将深入剖析该问题的两种解法,并以此为切入点,系统讲解如何评价一个算法的优劣。
问题描述
给定两个非递减顺序排列的整数数组 nums1 和 nums2,以及它们的有效元素个数 m 和 n。
要求:将 nums2 合并到 nums1 中,使 nums1 成为一个包含 m + n 个元素的有序数组。
⚠️ 注意:
nums1的长度实际为m + n,后n个位置预留为空(通常为 0),用于容纳nums2的元素。
解法一:从前向后 —— 简单但低效
最直观的想法是使用双指针从前向后遍历:
- 指针
i指向nums1[0] - 指针
j指向nums2[0] - 比较
nums1[i]与nums2[j],将较小者放入新数组 - 最终将新数组拷贝回
nums1
function mergeSimple(nums1, m, nums2, n) {
let i = 0, j = 0;
const merged = [];
while (i < m && j < n) {
if (nums1[i] <= nums2[j]) {
merged.push(nums1[i++]);
} else {
merged.push(nums2[j++]);
}
}
// 处理剩余元素
while (i < m) merged.push(nums1[i++]);
while (j < n) merged.push(nums2[j++]);
// 拷贝回 nums1
for (let k = 0; k < m + n; k++) {
nums1[k] = merged[k];
}
}
复杂度分析
- 时间复杂度:
O(m + n)
每个元素仅被访问一次。 - 空间复杂度:
O(m + n)
需要额外数组merged存储结果。
❌ 缺陷:浪费了
nums1末尾预留的空间,且违反题目“原地修改”的隐含要求。
解法二:从后向前 —— 空间最优解
观察发现:nums1 末尾有 n 个空位,不会被覆盖。因此,我们可以从后往前填充,避免数据覆盖问题。
三指针策略
i = m - 1:指向nums1有效部分的最后一个元素j = n - 1:指向nums2的最后一个元素k = m + n - 1:指向nums1的最后一个位置(待填)
每次比较 nums1[i] 与 nums2[j],将较大者放入 nums1[k],然后移动对应指针。
function merge(nums1, m, nums2, n) {
let i = m - 1;
let j = n - 1;
let 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--;
}
// 注意:若 nums1 有剩余,无需操作,已在正确位置
}
为什么不需要处理 nums1 剩余?
因为 nums1 的剩余元素本就在 nums1 的前部,且目标位置就是当前位置,无需移动。
算法评价:时间与空间复杂度
什么是时间复杂度?
时间复杂度描述算法执行时间随输入规模增长的趋势,用大 O 表示法(Big O Notation)表示。
- 不关注具体常数或低阶项,只抓主导项
- 例如:
T(n) = 3n + 5→O(n)
常见时间复杂度(由优到劣):
O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)
什么是空间复杂度?
空间复杂度衡量算法运行过程中临时占用的额外存储空间。
- 输入参数本身不计入(如
nums1,nums2) - 只计算额外申请的变量或数据结构
本题两种解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 是否原地 |
|---|---|---|---|
| 前向合并 | O(m + n) | O(m + n) | ❌ |
| 后向合并(推荐) | O(m + n) | O(1) | ✅ |
✅ 后向合并是本题的最优解:线性时间 + 常数空间。
为什么这个优化如此重要?
在实际工程中:
- 内存是宝贵资源,尤其在嵌入式、移动端或大规模数据处理场景
- 原地操作可避免内存分配与拷贝开销,提升性能
- 面试中,能否想到“从后往前”体现了对数据结构特性的深刻理解
总结:算法思维的核心
- 观察数据特性:有序 + 末尾预留空间 → 启发从后向前
- 权衡时空复杂度:在满足时间要求下,尽可能降低空间开销
- 边界条件处理:
nums2有剩余需处理,nums1则不用 - 代码简洁性:最优解往往逻辑清晰、代码简短
🧠 启示:优秀的算法不是“写得复杂”,而是“想得透彻”。
通过这道看似简单的题目,我们不仅掌握了双指针技巧,更深入理解了如何科学评价和优化算法。这正是算法训练的真正价值所在。
延伸思考:
- 如果两个数组都不可修改,如何合并?
- 若数组无序,先排序再合并是否最优?(提示:考虑
O((m+n) log(m+n))vsO(m log m + n log n + m + n))
掌握这些思维,你离写出高效、优雅的代码又近了一步!