在算法设计中,合并两个有序数组是一个基础却极具启发性的问题。它不仅出现在归并排序的核心步骤中,也是许多实际场景(如日志合并、数据同步)的简化模型。若仅追求功能实现,我们可以轻松创建一个新数组,用双指针从前向后依次选取较小元素填入。然而,当题目要求将结果直接存入第一个数组(nums1)且不使用额外空间时,问题便从“简单”跃升为“巧妙”——这正是考察算法思维与空间优化能力的关键所在。
常规思路:从前向后,需额外空间
最直观的方法是使用两个指针分别从 nums1 和 nums2 的起始位置开始比较:
let i = 0, j = 0;
const result = [];
while (i < m && j < n) {
if (nums1[i] <= nums2[j]) result.push(nums1[i++]);
else result.push(nums2[j++]);
}
// 处理剩余元素...
这种方法逻辑清晰,时间复杂度为 O(m + n),但需要 O(m + n) 的额外空间来存储结果。若题目明确要求“原地合并”,即利用 nums1 末尾预留的 n 个空位直接写入结果,则此方案不再适用。
关键洞察:从后向前,避免覆盖
观察题设条件:nums1 的长度为 m + n,前 m 个元素有效,后 n 个位置为空。这意味着,如果我们从数组末尾开始填充,就能安全地覆盖那些尚未处理的元素。
于是,我们引入三指针策略:
i指向nums1有效部分的最后一个元素(索引m - 1);j指向nums2的最后一个元素(索引n - 1);k指向nums1的最终末尾(索引m + n - 1)。
function merge(nums1, m, nums2, n) {
let i = m - 1, j = n - 1, k = m + n - 1;
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k--] = nums1[i--];
} else {
nums1[k--] = nums2[j--];
}
}
while (j >= 0) nums1[k--] = nums2[j--];
}
每次比较 nums1[i] 与 nums2[j],将较大者放入 nums1[k],然后相应指针左移。由于我们总是将当前最大值放到最后,且 nums1 后半部分原本为空,因此不会发生数据覆盖。
当 nums1 的元素先处理完(即 i < 0),只需将 nums2 剩余元素依次复制到 nums1 前部;若 nums2 先处理完,则 nums1 剩余部分已在正确位置,无需操作。
空间复杂度的极致优化
该算法的最大优势在于空间复杂度为 O(1) ——仅使用了几个指针变量,未申请任何额外数组。所有操作都在 nums1 上原地完成,完美契合题目约束。同时,时间复杂度仍保持 O(m + n),每个元素仅被访问一次。
这种“从后向前”的逆向思维,是解决原地修改类问题的经典范式。它提醒我们:当正向操作会导致数据破坏时,不妨尝试反向推进,利用已知的“安全区域”作为落脚点。
实际意义与延伸思考
该技巧不仅适用于数组合并,在字符串拼接(如 URL 编码)、内存管理等场景也有广泛应用。例如,某些系统 API 要求输入输出使用同一缓冲区,此时从后向前写入可避免中间结果被覆盖。
此外,这一解法也体现了算法设计中的一个重要原则:充分利用问题的隐含条件。本题中,“nums1 末尾有足够空位”这一细节,正是实现原地合并的关键突破口。忽略它,只能退而求其次;抓住它,便能写出优雅高效的代码。
结语
有序数组合并看似平凡,却蕴含着深刻的算法思想。从前向后的朴素解法,到从后向前的原地优化,不仅是技巧的升级,更是思维方式的转变。它教会我们在面对约束时,如何通过逆向思考、利用空闲资源、避免冗余操作,最终在时间和空间上达到双重最优。掌握这一模式,不仅能应对面试题,更能培养出对数据结构与内存布局的敏锐直觉——而这,正是优秀程序员的核心素养之一。