有序数组合并算法学习笔记(JavaScript 实现)
在算法学习中,合并两个有序数组是一个经典问题。它不仅考察我们对数组操作的理解,还涉及空间与时间复杂度的权衡。本文将围绕两种主要解法展开:基础双指针法和原地优化三指针法,并重点分析后者如何在不使用额外空间的前提下完成合并。
问题描述
给定两个非递减顺序排列的整数数组 nums1 和 nums2,以及两个整数 m 和 n,分别表示 nums1 和 nums2 中的有效元素个数。
要求:将 nums2 合并到 nums1 中,使 nums1 成为一个包含 m + n 个元素的有序数组。
注意:
nums1的长度为m + n,后n个位置是预留的空位(通常初始化为 0),用于容纳nums2的元素。
方法一:双指针从前往后(需额外空间)
最直观的思路是使用两个指针分别指向 nums1 和 nums2 的起始位置,比较当前元素大小,将较小者放入新数组中。
function mergeBasic(nums1, m, nums2, n) {
const merged = [];
let i = 0, j = 0;
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) —— 需要额外数组存储结果
虽然逻辑清晰,但题目通常要求原地修改 nums1,且希望空间复杂度为 O(1) 。因此,我们需要更优的方案。
方法二:三指针从后往前(原地合并,空间 O(1))
核心思想
利用 nums1 数组末尾的空闲空间,从后往前填充最大值。这样可以避免覆盖尚未处理的 nums1 前部有效元素。
我们使用三个指针:
i:指向nums1有效部分的最后一个元素(索引m - 1)j:指向nums2的最后一个元素(索引n - 1)k:指向nums1整体的最后一个位置(索引m + n - 1)
每次比较 nums1[i] 和 nums2[j],将较大者放入 nums1[k],然后对应指针前移。
JavaScript 实现
function merge(nums1, m, nums2, n) {
let i = m - 1; // nums1 有效元素末尾
let j = n - 1; // nums2 末尾
let k = m + n - 1; // nums1 总容量末尾
// 从后往前比较并填充
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j]) {
nums1[k] = nums1[i];
i--;
} else {
nums1[k] = nums2[j];
j--;
}
k--;
}
// 如果 nums2 还有剩余元素(说明它们都比 nums1 最小值还小)
while (j >= 0) {
nums1[k] = nums2[j];
j--;
k--;
}
// 注意:如果 i >= 0 而 j < 0,说明 nums1 剩余元素已在正确位置,无需操作
}
为什么不需要处理 nums1 剩余?
因为 nums1 本身就是目标数组,其前 i+1 个元素已经有序且小于等于已放置的元素。当 j < 0 时,nums1[0...i] 已经在正确位置,无需移动。
示例演示
假设:
nums1 = [1, 2, 3, 0, 0, 0], m = 3
nums2 = [2, 5, 6], n = 3
执行过程:
i=2, j=2, k=5 → 3 < 6 → nums1[5]=6, j=1, k=4
i=2, j=1, k=4 → 3 < 5 → nums1[4]=5, j=0, k=3
i=2, j=0, k=3 → 3 > 2 → nums1[3]=3, i=1, k=2
i=1, j=0, k=2 → 2 == 2 → nums1[2]=2, j=-1, k=1
j < 0,循环结束
最终 nums1 = [1, 2, 2, 3, 5, 6]
复杂度分析
- 时间复杂度:O(m + n) —— 每个元素最多被访问一次
- 空间复杂度:O(1) —— 仅使用常数个指针变量,原地修改
关键理解点
-
为什么从后往前?
因为nums1末尾有空位,从后往前写不会覆盖尚未处理的有效数据。若从前往后,nums1的元素可能被覆盖而丢失。 -
边界条件处理
- 当
nums2元素全部处理完(j < 0),nums1剩余部分已就位。 - 当
nums1元素先处理完(i < 0),需将nums2剩余元素全部拷贝到nums1前部。
- 当
-
稳定性与通用性
该方法适用于所有非递减有序数组,且不要求两数组长度相近。
总结
有序数组合并问题看似简单,却蕴含了空间优化和指针技巧的核心思想。通过“从后往前”的三指针策略,我们实现了原地、高效、稳定的合并算法。这不仅解决了 LeetCode 第 88 题,也为处理其他“原地修改”类问题提供了范式。
启示:在面对数组操作问题时,若允许修改原数组且存在空闲空间,不妨思考“逆向填充”是否可行——这往往是降低空间复杂度的关键突破口。
掌握此方法后,可进一步拓展至多个有序数组合并、归并排序的原地实现等更复杂场景。