有序数组合并算法学习笔记(JavaScript 实现)

40 阅读4分钟

有序数组合并算法学习笔记(JavaScript 实现)

在算法学习中,合并两个有序数组是一个经典问题。它不仅考察我们对数组操作的理解,还涉及空间与时间复杂度的权衡。本文将围绕两种主要解法展开:基础双指针法原地优化三指针法,并重点分析后者如何在不使用额外空间的前提下完成合并。


问题描述

给定两个非递减顺序排列的整数数组 nums1nums2,以及两个整数 mn,分别表示 nums1nums2 中的有效元素个数。
要求:将 nums2 合并到 nums1 中,使 nums1 成为一个包含 m + n 个元素的有序数组。

注意:nums1 的长度为 m + n,后 n 个位置是预留的空位(通常初始化为 0),用于容纳 nums2 的元素。


方法一:双指针从前往后(需额外空间)

最直观的思路是使用两个指针分别指向 nums1nums2 的起始位置,比较当前元素大小,将较小者放入新数组中。

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=53 < 6 → nums1[5]=6, j=1, k=4
i=2, j=1, k=43 < 5 → nums1[4]=5, j=0, k=3
i=2, j=0, k=33 > 2 → nums1[3]=3, i=1, k=2
i=1, j=0, k=22 == 2 → nums1[2]=2, j=-1, k=1
j < 0,循环结束
最终 nums1 = [1, 2, 2, 3, 5, 6]

复杂度分析

  • 时间复杂度:O(m + n) —— 每个元素最多被访问一次
  • 空间复杂度:O(1) —— 仅使用常数个指针变量,原地修改

关键理解点

  1. 为什么从后往前?
    因为 nums1 末尾有空位,从后往前写不会覆盖尚未处理的有效数据。若从前往后,nums1 的元素可能被覆盖而丢失。

  2. 边界条件处理

    • nums2 元素全部处理完(j < 0),nums1 剩余部分已就位。
    • nums1 元素先处理完(i < 0),需将 nums2 剩余元素全部拷贝到 nums1 前部。
  3. 稳定性与通用性
    该方法适用于所有非递减有序数组,且不要求两数组长度相近。


总结

有序数组合并问题看似简单,却蕴含了空间优化指针技巧的核心思想。通过“从后往前”的三指针策略,我们实现了原地、高效、稳定的合并算法。这不仅解决了 LeetCode 第 88 题,也为处理其他“原地修改”类问题提供了范式。

启示:在面对数组操作问题时,若允许修改原数组且存在空闲空间,不妨思考“逆向填充”是否可行——这往往是降低空间复杂度的关键突破口。

掌握此方法后,可进一步拓展至多个有序数组合并归并排序的原地实现等更复杂场景。