有序数组合并:从双指针到空间优化的算法艺术

46 阅读4分钟

有序数组合并:从双指针到空间优化的算法艺术

在算法世界中, “合并两个有序数组” 是一道经典而富有启发性的问题。它不仅考察对数组操作的理解,更体现了算法设计中对 时间复杂度空间复杂度 的权衡智慧。本文将深入剖析该问题的两种解法,并以此为切入点,系统讲解如何评价一个算法的优劣。


问题描述

给定两个非递减顺序排列的整数数组 nums1nums2,以及它们的有效元素个数 mn
要求:将 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 + 5O(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)

后向合并是本题的最优解:线性时间 + 常数空间。


为什么这个优化如此重要?

在实际工程中:

  • 内存是宝贵资源,尤其在嵌入式、移动端或大规模数据处理场景
  • 原地操作可避免内存分配与拷贝开销,提升性能
  • 面试中,能否想到“从后往前”体现了对数据结构特性的深刻理解

总结:算法思维的核心

  1. 观察数据特性:有序 + 末尾预留空间 → 启发从后向前
  2. 权衡时空复杂度:在满足时间要求下,尽可能降低空间开销
  3. 边界条件处理nums2 有剩余需处理,nums1 则不用
  4. 代码简洁性:最优解往往逻辑清晰、代码简短

🧠 启示:优秀的算法不是“写得复杂”,而是“想得透彻”。

通过这道看似简单的题目,我们不仅掌握了双指针技巧,更深入理解了如何科学评价和优化算法。这正是算法训练的真正价值所在。


延伸思考

  • 如果两个数组都不可修改,如何合并?
  • 若数组无序,先排序再合并是否最优?(提示:考虑 O((m+n) log(m+n)) vs O(m log m + n log n + m + n)

掌握这些思维,你离写出高效、优雅的代码又近了一步!