LeetCode 88. 合并两个有序数组(双指针详解 + Java Python 实现)

LeetCode 88. 合并两个有序数组(双指针详解 + Java/Python 实现)

题目链接

88. 合并两个有序数组 - 力扣(LeetCode)


题目描述

给你两个按 非递减顺序 排列的整数数组 nums1nums2,另有两个整数 mn,分别表示 nums1nums2 中的元素数目。

请你 合并 nums2nums1 ,使合并后的数组同样按 非递减顺序 排列。

注意 :最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0,应忽略。nums2 的长度为 n


示例

示例 1

text 复制代码
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6]。
合并结果是 [1,2,2,3,5,6]。

示例 2

text 复制代码
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 []。
合并结果是 [1]。

示例 3

text 复制代码
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1]。
合并结果是 [1]。
注意,因为 m = 0,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。

提示

  • nums1.length == m + n
  • nums2.length == n
  • 0 <= m, n <= 200
  • 1 <= m + n <= 200
  • -10^9 <= nums1[i], nums2[j] <= 10^9

解题思路

这道题是合并两个有序数组的经典题目。最直观的思路是创建一个临时数组,将两个数组的元素按顺序合并到临时数组中,最后再复制回 nums1。但这种方法需要额外的 O(m+n) 空间。

更优的解法是利用 nums1 后面已经预留了 n 个位置的特点,从后往前 进行合并,这样可以原地完成,不需要额外空间。


方法一:新建临时数组(双路归并)

思路

这是最直观的解法,类似于归并排序中的合并步骤:

  1. 创建一个长度为 m + n 的临时数组 temp
  2. 使用两个指针 ij 分别指向 nums1nums2 的开头。
  3. 比较两个指针指向的元素,将较小的放入 temp 中,并移动对应指针。
  4. 当某个数组遍历完后,将另一个数组剩余的元素全部复制到 temp 中。
  5. 最后将 temp 的内容复制回 nums1

Java 代码

java 复制代码
class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int[] temp = new int[m + n];
        int i = 0, j = 0, idx = 0;
        while (i < m && j < n) {
            if (nums1[i] <= nums2[j]) {
                temp[idx++] = nums1[i++];
            } else {
                temp[idx++] = nums2[j++];
            }
        }
        while (i < m) temp[idx++] = nums1[i++];
        while (j < n) temp[idx++] = nums2[j++];
        // 把临时数组全部复制回 nums1
        System.arraycopy(temp, 0, nums1, 0, m + n);
    }
}

Python 代码

python 复制代码
class Solution(object):
    def merge(self, nums1, m, nums2, n):
        temp = []
        p1 = p2 = 0
        while p1 < m and p2 < n:
            if nums1[p1] <= nums2[p2]:
                temp.append(nums1[p1])
                p1 += 1
            else:
                temp.append(nums2[p2])
                p2 += 1
        while p1 < m:
            temp.append(nums1[p1])
            p1 += 1
        while p2 < n:
            temp.append(nums2[p2])
            p2 += 1
        # nums1[:] 原地覆盖原列表(关键!不能写 nums1 = temp)
        nums1[:] = temp

复杂度分析

  • 时间复杂度O(m + n),需要遍历两个数组的所有元素。
  • 空间复杂度O(m + n),需要额外的临时数组。

优缺点

  • 优点:思路简单直观,容易理解。
  • 缺点 :需要额外的 O(m+n) 空间,不符合题目"原地合并"的最佳要求。

方法二:原地从后往前双指针(最优解法|推荐背诵)

思路

这种方法的核心是 从后往前 合并,利用 nums1 后面预留的空间:

  1. 使用三个指针:
    • p1 指向 nums1 有效元素的末尾(m - 1)。
    • p2 指向 nums2 的末尾(n - 1)。
    • tail 指向 nums1 的最终末尾(m + n - 1)。
  2. 比较 nums1[p1]nums2[p2],将较大的元素放到 nums1[tail] 位置。
  3. 移动对应的指针和 tail 指针。
  4. 当某个数组遍历完后,如果 nums2 还有剩余元素,将其全部复制到 nums1 前面。
  5. 如果 nums1 还有剩余元素,不需要处理,因为它们已经在正确的位置上。

为什么从后往前不会覆盖有效数据?

因为 nums1 后面有 n 个空位(值为 0),从后往前填充时,即使覆盖了某些位置,那些位置也是已经处理过的或者原本就是空位,不会影响还未处理的有效数据。

Java 代码

java 复制代码
class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int p1 = m - 1;    // nums1有效元素末尾
        int p2 = n - 1;    // nums2末尾
        int tail = m + n - 1; // 写入位置(nums1最终末尾)
        while (p1 >= 0 && p2 >= 0) {
            if (nums1[p1] > nums2[p2]) {
                nums1[tail--] = nums1[p1--];
            } else {
                nums1[tail--] = nums2[p2--];
            }
        }
        // 如果nums2还有剩余元素,全部向前填充
        while (p2 >= 0) {
            nums1[tail--] = nums2[p2--];
        }
    }
}

Python 代码

python 复制代码
class Solution(object):
    def merge(self, nums1, m, nums2, n):
        p1 = m - 1
        p2 = n - 1
        tail = m + n - 1
        while p1 >= 0 and p2 >= 0:
            if nums1[p1] > nums2[p2]:
                nums1[tail] = nums1[p1]
                p1 -= 1
            else:
                nums1[tail] = nums2[p2]
                p2 -= 1
            tail -= 1
        # 处理nums2剩下的数据
        while p2 >= 0:
            nums1[tail] = nums2[p2]
            p2 -= 1
            tail -= 1

代码说明

  1. 初始化三个指针
    • p1 = m - 1:指向 nums1 最后一个有效元素。
    • p2 = n - 1:指向 nums2 最后一个元素。
    • tail = m + n - 1:指向 nums1 最终末尾位置。
  2. 主循环 :当 p1 >= 0 && p2 >= 0 时,比较两个指针指向的��素,将较大的放到 tail 位置。
  3. 处理剩余元素
    • 如果 nums2 还有剩余(p2 >= 0),说明 nums2 的元素都比较小,需要填充到 nums1 前面。
    • 如果 nums1 还有剩余(p1 >= 0),不需要处理,因为它们已经在正确的位置上。

复杂度分析

  • 时间复杂度O(m + n),需要遍历两个数组的所有元素。
  • 空间复杂度O(1),只使用了常数级别的额外空间。

优点

  • 原地合并:不需要额外的数组空间。
  • 效率高:每个元素只被处理一次。
  • 代码简洁:逻辑清晰,容易记忆。

两种方法对比

方法 时间复杂度 空间复杂度 优缺点
新建临时数组 O(m + n) O(m + n) 思路简单,但需要额外空间
从后往前双指针 O(m + n) O(1) 原地合并,最优解法

常见问题解答

Q1:为什么从后往前合并不会覆盖有效数据?

因为 nums1 的总长度是 m + n,后面有 n 个空位。从后往前填充时,tail 指针始终在 p1 指针的后面(或相等),所以即使覆盖,也是覆盖已经处理过的位置或者空位,不会影响还未处理的有效数据。

Q2:为什么只需要处理 nums2 剩余的情况,不需要处理 nums1 剩余的情况?

因为 nums1 的有效元素本来就在数组的前面部分,如果 p1 还有剩余,说明这些元素已经比 nums2 的所有元素都小,它们已经在正确的位置上,不需要移动。

Q3:Python 中为什么用 nums1: = temp 而不是 nums1 = temp?

因为 nums1 = temp 只是将局部变量 nums1 指向了新的列表,不会修改原列表的内容。而 nums1[:] = temp 是切片赋值,会原地修改 nums1 的内容,这样才能正确地将结果写回原数组。


总结

这道题是双指针技巧的经典应用,重点掌握 从后往前合并 的技巧:

  1. 利用预留空间nums1 后面有 n 个空位,可以从后往前填充。
  2. 三个指针p1 指向 nums1 有效元素末尾,p2 指向 nums2 末尾,tail 指向最终写入位置。
  3. 比较并移动 :每次将较大的元素放到 tail 位置。
  4. 处理剩余 :只需处理 nums2 剩余的情况。

关键点回顾:

  • 从后往前合并,避免覆盖有效数据
  • 使用三个指针:p1p2tail
  • 只需处理 nums2 剩余的情况
  • 时间复杂度 O(m+n),空间复杂度 O(1)

这个技巧在很多数组合并题目中都会用到,建议熟练掌握。

如果觉得本文对你有帮助,欢迎点赞、收藏、关注,我会持续更新更多 LeetCode 题解和算法干货!

相关推荐
Nil2081 小时前
leetcode 238除了自身以外数组的乘积
数据结构·算法·leetcode
土司大王1 小时前
LeetCode hot100——最长连续序列
数据结构·算法·leetcode
caimouse1 小时前
ReactOS 图形系统分析(17):区域填充 — EngPaint / IntEngPaint(paint.c)
c语言·开发语言·算法
生态学者1 小时前
Ecological Indicators | 机场鸟调的新方法:用 AWM-PC1 读懂干扰梯度下的鸟类群落
人工智能·算法·r语言·微信公众平台
北风toto1 小时前
研发效能与后端核心技术全景指南:从CI/CD到共性组件实战
java·开发语言·ci/cd
01二进制代码漫游日记1 小时前
C++基础入门速通
java·开发语言·c++
Herbert_hwt1 小时前
第六章 Java深入理解接口、函数式接口与lambda表达式
java·开发语言·算法
API快乐传递者1 小时前
电商竞品分析接口实战指南:从数据采集到决策洞察的全链路方案
java·python
2401_868534781 小时前
集中式存储和分布式存储
python·pygame