目录

题目描述

面试题 10.01. 合并排序的数组

image-20230312174406394

题意分析

两个升序数组 A、B,其中 A 的实际长度是 m + n,但只有前 m 个位置装着有效数据,后 n 个是预留的空位。要求把 B 的 n 个元素合并进 A,使 A 整体保持升序。函数没有返回值,说明必须原地修改 A,不能构造新数组再返回。

「A 末尾预留了恰好 n 个空位」是题目最重要的暗示。出题人特意把数组开大而不是只给 m 个元素,就是在提示:合并结果的总容量已经确定,最终每个位置放什么也是确定的,可以直接往目标位置写。

两个数组都已有序,这个条件意味着不需要任何排序或查找——每一步只要比较两个数组当前的候选元素,就能确定谁该占据下一个位置。这是线性合并成立的全部依据。

边界上要考虑:m 可以为 0,此时 A 全是空位,等价于把 B 整体复制过去;n 可以为 0,此时什么都不用做;两个数组可能有大量相等元素,比较时要保证不丢不重;B 的元素可能全部小于 A 的最小值,也可能全部大于 A 的最大值,这两种极端情况会分别耗尽某一侧的指针。

解法:从后向前合并

核心思路

两个数组已经升序,当前未合并元素中的最大值一定是 A[i]B[j] 中较大的那个。若从前往后写入 A,会覆盖 A 中尚未读取的有效元素;而 A 的空位都在尾部,所以从后往前填充可以原地完成合并。

i = m - 1j = n - 1 分别指向两组未处理元素的末尾,k = m + n - 1 指向下一个写入位置。循环维护两个不变量:

  • A[k+1..m+n-1] 已经是最终答案中最大的那一段,且顺序正确。
  • 当 B 还有元素时,k - i = j + 1 > 0,所以写入位置始终在 A 的未读位置右侧,不会覆盖 A[i]

每轮把较大的候选写入 A[k],相应读指针与 k 同时左移。不需要等待 A 全部耗尽:一旦 B 已经写完,A 剩余元素原本就在正确的前缀中;若 A 先耗尽,则继续把 B 写入即可。

正确性说明:当 A、B 都有剩余元素时,根据升序性质,A[i]B[j] 的较大值就是当前未处理元素的最大值;A 耗尽后,B[j] 就是唯一候选。把它放在位置 k 会保持已完成后缀的不变量。循环在 B 耗尽时结束,此时后缀已经就位,A 的剩余前缀无需移动,所以整个 A 有序且包含两数组全部元素。

解题步骤

  • 初始化 i = m - 1j = n - 1k = m + n - 1
  • 只要 j >= 0,B 中仍有元素必须写入 A。
  • i >= 0A[i] > B[j],把 A[i] 放到 A[k];否则放入 B[j]。相等时取哪一侧都正确,这里统一取 B。
  • 每写入一个元素,就把对应读指针和 k 左移一位;B 耗尽后结束。

A = [1,2,3,0,0,0]B = [2,5,6] 为例,写入顺序依次是 6、5、3、2,此时 B 耗尽,A 的 [1,2] 已在原位,结果为 [1,2,2,3,5,6]m = 0 时循环会完整复制 B;n = 0 时循环不执行,A 保持不变。

代码实现

class Solution {
    public void merge(int[] A, int m, int[] B, int n) {
        int i = m - 1;
        int j = n - 1;
        int k = m + n - 1;

        while (j >= 0) {
            if (i >= 0 && A[i] > B[j]) {
                A[k--] = A[i--];
            } else {
                A[k--] = B[j--];
            }
        }
    }
}
func merge(A []int, m int, B []int, n int) {
	i, j, k := m-1, n-1, m+n-1

	for j >= 0 {
		if i >= 0 && A[i] > B[j] {
			A[k] = A[i]
			i--
		} else {
			A[k] = B[j]
			j--
		}
		k--
	}
}

复杂度分析

  • 时间复杂度:O(m + n)。每轮确定一个最终位置,最坏情况下处理两个数组的全部元素。
  • 空间复杂度:O(1)。只使用三个下标,直接复用 A 末尾的预留空间。

关键点总结

  • 空位在 A 的尾部,决定了应从后往前写;方向选择直接消除了读写覆盖。
  • A[k+1..] 始终是已经确定的有序后缀,这是合并正确性的核心不变量。
  • 循环条件只盯住 B:B 的剩余元素必须搬运,A 的剩余元素天然已在正确位置。
  • k = m + n - 1i = m - 1,必须区分总容量与有效长度。

易错点总结

  • 从前往后直接覆盖 AA = [1,2,3,0,0,0]B = [0,0,0] 时,首轮就会覆盖尚未读取的 1,后续无法恢复。
  • i 初始化为 A.length - 1:预留位置的 0 会被当作有效元素参与比较。i 必须从 m - 1 开始。
  • k 初始化为 m + n:第一次写入即越界;最后一个合法位置是 m + n - 1
  • 主循环只写 i >= 0 && j >= 0,却漏掉 B 的收尾A = [0], m = 0, B = [1], n = 1 时循环不执行,结果仍为 [0]。用本文的 j >= 0 单循环可自然覆盖这一边界。
  • 循环条件使用 i >= 0 || j >= 0 后仍直接访问两侧:任一指针变成 -1 都会越界;应先判断指针有效性,或采用本文的分支结构。

相似题目

题目 难度 考察点
88. 合并两个有序数组 简单 完全同构的题面,可直接对照检验指针初值与收尾循环是否写对
21. 合并两个有序链表 简单 载体换成链表后无法逆向遍历,必须正向合并并借助哑结点串接
23. 合并 K 个升序链表 困难 待合并序列从 2 条扩展到 K 条,需要优先队列或分治来降低每步选择的代价
977. 有序数组的平方 简单 双指针从两端向中间收缩而非从末端同向左移,考察对最大值来源的判断
986. 区间列表的交集 中等 双指针推进的元素是区间而非单值,每步要计算重叠而不是简单取较大者
283. 移动零 简单 同样是原地写指针追读指针,但方向为正向,靠「写指针永不超前」保证安全