【LeetCode】88. 合并两个有序数组

文章目录

88. 合并两个有序数组

题目描述

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

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

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

示例 1:

输入: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] ,其中斜体加粗标注的为 nums1 中的元素。

示例 2:

输入:nums1 = [1], m = 1, nums2 = [], n = 0

输出:[1]

解释:需要合并 [1] 和 [] 。

合并结果是 [1] 。

示例 3:

输入: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

进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?

解题思路

问题深度分析

这是经典的双指针算法 问题,也是数组合并 的典型应用。核心在于从后往前遍历,在O(m+n)时间内将两个有序数组合并为一个有序数组。

问题本质

给定两个已排序的数组nums1和nums2,将nums2合并到nums1中,使合并后的数组保持非递减顺序。

核心思想

双指针 + 逆向遍历

  1. 双指针:使用两个指针分别指向两个数组的有效元素末尾
  2. 逆向遍历:从后往前填充nums1数组
  3. 元素比较:比较两个指针指向的元素,取较大值填充
  4. 位置调整:将较大的元素放在合适的位置

关键技巧

  • 从后往前遍历,避免覆盖未处理的元素
  • 使用两个指针分别遍历nums1和nums2的有效元素
  • 将比较结果较大的元素放在nums1的末尾
  • 处理剩余元素
关键难点分析

难点1:从后往前遍历的必要性

  • 如果从前往后遍历,会覆盖nums1中未处理的元素
  • 需要从后往前填充,避免数据丢失
  • 时间复杂度为O(m+n)

难点2:边界条件的处理

  • nums1为空数组的情况
  • nums2为空数组的情况
  • 指针边界检查

难点3:剩余元素的处理

  • 当nums1的指针先到达边界时的处理
  • 当nums2的指针先到达边界时的处理
  • 需要将所有剩余元素移动到nums1
典型情况分析

情况1:一般情况

复制代码
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3

初始状态:
i=2, j=2, k=5
nums1 = [1,2,3,0,0,0]
nums2 = [2,5,6]

步骤1:比较3和6,取6
nums1 = [1,2,3,0,0,6], i=2, j=1, k=4

步骤2:比较3和5,取5
nums1 = [1,2,3,0,5,6], i=2, j=0, k=3

步骤3:比较3和2,取3
nums1 = [1,2,3,3,5,6], i=1, j=0, k=2

步骤4:比较2和2,取2
nums1 = [1,2,2,3,5,6], i=1, j=-1, k=1

步骤5:复制剩余元素
nums1 = [1,2,2,3,5,6]

结果: [1,2,2,3,5,6]

情况2:nums1为空

复制代码
nums1 = [0], m = 0
nums2 = [1], n = 1
结果: [1]

情况3:nums2为空

复制代码
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [], n = 0
结果: [1,2,3]

情况4:nums1全部小于nums2

复制代码
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [4,5,6], n = 3
结果: [1,2,3,4,5,6]
算法对比
算法 时间复杂度 空间复杂度 特点
双指针(逆序) O(m+n) O(1) 最优解法
双指针(正序) O(m+n) O(m) 空间复杂度高
双指针(正序) O(m+n) O(1) 但会覆盖数据
排序算法 O((m+n)log(m+n)) O(1) 效率较低

注:m为nums1长度,n为nums2长度

算法流程图

主算法流程(双指针逆序遍历)
graph TD A[开始: nums1, m, nums2, n] --> B[m == 0?] B -->|是| C[直接复制nums2到nums1] C --> D[结束] B -->|否| E[n == 0?] E -->|是| D E -->|否| F[初始化指针: i=m-1, j=n-1, k=m+n-1] F --> G{k >= 0?} G -->|否| D G -->|是| H{i >= 0 && j >= 0?} H -->|是| I[比较nums1[i]和nums2[j]] I --> J[nums1[i] > nums2[j]?] J -->|是| K[nums1[k] = nums1[i], i--, k--] J -->|否| L[nums1[k] = nums2[j], j--, k--] K --> G L --> G H -->|否| M{i >= 0?} M -->|是| N[复制nums1剩余元素] M -->|否| O[复制nums2剩余元素] N --> D O --> D
详细比较流程
graph TD A[当前位置 k] --> B{还有元素未处理?} B -->|否| C[合并完成] B -->|是| D{两个数组都有元素?} D -->|是| E[比较nums1[i]和nums2[j]] E --> F[选择较大值] F --> G[放入nums1[k]] G --> H[更新指针] H --> B D -->|否| I{只有nums1有元素?} I -->|是| J[复制nums1剩余元素] I -->|否| K[复制nums2剩余元素] J --> C K --> C
合并过程可视化
graph LR subgraph 初始状态 A1[nums1: 1,2,3,0,0,0] B1[nums2: 2,5,6] C1[i=2, j=2, k=5] end subgraph 第一轮 A2[比较nums1[2]=3和nums2[2]=6] B2[6较大, nums1[5]=6] C2[i=2, j=1, k=4] end subgraph 第二轮 A3[比较nums1[2]=3和nums2[1]=5] B3[5较大, nums1[4]=5] C3[i=2, j=0, k=3] end subgraph 第三轮 A4[比较nums1[2]=3和nums2[0]=2] B4[3较大, nums1[3]=3] C4[i=1, j=0, k=2] end subgraph 最终结果 A5[nums1: 1,2,2,3,5,6] end A1 --> A2 --> A3 --> A4 --> A5

复杂度分析

时间复杂度详解

双指针算法(逆序遍历):O(m+n)

  • 遍历nums1的有效元素:O(m)
  • 遍历nums2的有效元素:O(n)
  • 合并剩余元素:O(1)
  • 总时间:O(m+n)

排序算法:O((m+n)log(m+n))

  • 先合并两个数组
  • 然后排序
  • 总时间:O((m+n)log(m+n))
空间复杂度详解

双指针算法(逆序遍历):O(1)

  • 只使用常数额外空间
  • 原地合并两个数组
  • 总空间:O(1)

关键优化技巧

技巧1:双指针逆序遍历(最优解法)
go 复制代码
func merge(nums1 []int, m int, nums2 []int, n int) {
    // 三个指针:i指向nums1有效元素末尾,j指向nums2末尾,k指向nums1末尾
    i, j, k := m-1, n-1, m+n-1
    
    // 从后往前填充nums1
    for k >= 0 {
        if j < 0 {
            // nums2已经全部处理完毕,停止
            break
        }
        if i >= 0 && nums1[i] > nums2[j] {
            nums1[k] = nums1[i]
            i--
        } else {
            nums1[k] = nums2[j]
            j--
        }
        k--
    }
}

优势

  • 时间复杂度:O(m+n)
  • 空间复杂度:O(1)
  • 原地合并,不需要额外空间
技巧2:双指针正序遍历(需要额外空间)
go 复制代码
func merge(nums1 []int, m int, nums2 []int, n int) {
    // 复制nums1的有效元素
    temp := make([]int, m)
    copy(temp, nums1[:m])
    
    i, j, k := 0, 0, 0
    
    // 从前往后填充nums1
    for i < m && j < n {
        if temp[i] <= nums2[j] {
            nums1[k] = temp[i]
            i++
        } else {
            nums1[k] = nums2[j]
            j++
        }
        k++
    }
    
    // 复制剩余元素
    for i < m {
        nums1[k] = temp[i]
        i++
        k++
    }
    for j < n {
        nums1[k] = nums2[j]
        j++
        k++
    }
}

特点:使用额外空间,但逻辑清晰

技巧3:简化版双指针
go 复制代码
func merge(nums1 []int, m int, nums2 []int, n int) {
    i, j, k := m-1, n-1, m+n-1
    
    for j >= 0 {
        if i >= 0 && nums1[i] > nums2[j] {
            nums1[k] = nums1[i]
            i--
        } else {
            nums1[k] = nums2[j]
            j--
        }
        k--
    }
}

特点:代码更简洁,逻辑更清晰

技巧4:优化版(减少比较次数)
go 复制代码
func merge(nums1 []int, m int, nums2 []int, n int) {
    i, j, k := m-1, n-1, m+n-1
    
    for i >= 0 && j >= 0 {
        if nums1[i] > nums2[j] {
            nums1[k] = nums1[i]
            i--
        } else {
            nums1[k] = nums2[j]
            j--
        }
        k--
    }
    
    // 复制nums2的剩余元素
    for j >= 0 {
        nums1[k] = nums2[j]
        j--
        k--
    }
}

特点:减少不必要的比较,提高效率

边界情况处理

  1. nums1为空数组:直接复制nums2到nums1
  2. nums2为空数组:nums1保持不变
  3. 两个数组都为空:返回空数组
  4. nums1全部小于nums2:直接将nums2追加到nums1
  5. nums2全部小于nums1:nums1不变

测试用例设计

基础测试
复制代码
输入: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
说明: 一般情况
简单情况
复制代码
输入: nums1 = [1], m = 1, nums2 = [], n = 0
输出: [1]
说明: nums2为空
特殊情况
复制代码
输入: nums1 = [0], m = 0, nums2 = [1], n = 1
输出: [1]
说明: nums1为空
边界情况
复制代码
输入: nums1 = [4,5,6,0,0,0], m = 3, nums2 = [1,2,3], n = 3
输出: [1,2,3,4,5,6]
说明: nums2全部小于nums1

常见错误与陷阱

错误1:从前往后遍历
go 复制代码
// ❌ 错误:从前往后遍历会覆盖未处理的元素
i, j := 0, 0
for i < m && j < n {
    if nums1[i] <= nums2[j] {
        i++
    } else {
        nums1[i] = nums2[j]  // 错误:覆盖了nums1[i]
        j++
    }
}

问题:会覆盖nums1中未处理的元素

错误2:边界检查不正确
go 复制代码
// ❌ 错误:边界检查不正确
for k >= 0 {
    if nums1[i] > nums2[j] {  // 错误:没有检查i和j是否越界
        nums1[k] = nums1[i]
        i--
    } else {
        nums1[k] = nums2[j]
        j--
    }
    k--
}

问题:没有检查指针是否越界

错误3:剩余元素未处理
go 复制代码
// ❌ 错误:剩余元素未处理
for i >= 0 && j >= 0 {
    // 只处理了i和j都大于等于0的情况
    if nums1[i] > nums2[j] {
        nums1[k] = nums1[i]
        i--
    } else {
        nums1[k] = nums2[j]
        j--
    }
    k--
}

问题:没有处理剩余的nums1或nums2元素

实战技巧总结

  1. 双指针模板:i=m-1, j=n-1, k=m+n-1
  2. 逆向遍历:从后往前填充,避免覆盖
  3. 元素比较:比较两个指针指向的元素
  4. 剩余处理:处理剩余元素
  5. 边界检查:检查指针是否越界

进阶扩展

扩展1:返回新数组
go 复制代码
func mergeNew(nums1 []int, m int, nums2 []int, n int) []int {
    result := make([]int, m+n)
    // 合并逻辑
    return result
}
扩展2:支持多个数组合并
go 复制代码
func mergeMultiple(arrays [][]int) []int {
    // 合并多个数组
    // ...
}
扩展3:原地合并多个有序数组
go 复制代码
func mergeInPlace(nums1 []int, m int, arrays [][]int) {
    // 原地合并多个数组
    // ...
}

应用场景

  1. 数组合并:合并多个有序数组
  2. 排序算法:归并排序的核心操作
  3. 数据处理:数据清洗和合并
  4. 算法竞赛:双指针经典应用
  5. 系统设计:数据合并和同步

代码实现

本题提供了四种不同的解法,重点掌握双指针逆序遍历算法。

测试结果

测试用例 双指针逆序 双指针正序 简化版 优化版
基础测试
简单情况
特殊情况
边界情况

核心收获

  1. 双指针算法:数组合并的经典应用
  2. 逆向遍历:避免覆盖未处理的元素
  3. 元素比较:准确比较两个数组的元素
  4. 剩余处理:处理剩余元素
  5. 边界处理:各种边界情况的考虑

应用拓展

  • 数组合并和排序
  • 双指针经典应用
  • 归并排序基础
  • 数据处理技术
  • 系统设计应用

完整题解代码

go 复制代码
package main

import (
	"fmt"
)

// =========================== 方法一:双指针逆序遍历(最优解法) ===========================

func merge(nums1 []int, m int, nums2 []int, n int) {
	// 三个指针:i指向nums1有效元素末尾,j指向nums2末尾,k指向nums1末尾
	i, j, k := m-1, n-1, m+n-1

	// 从后往前填充nums1
	for k >= 0 {
		if j < 0 {
			// nums2已经全部处理完毕,停止
			break
		}
		if i >= 0 && nums1[i] > nums2[j] {
			nums1[k] = nums1[i]
			i--
		} else {
			nums1[k] = nums2[j]
			j--
		}
		k--
	}
}

// =========================== 方法二:双指针正序遍历(需要额外空间) ===========================

func merge2(nums1 []int, m int, nums2 []int, n int) {
	// 复制nums1的有效元素
	temp := make([]int, m)
	copy(temp, nums1[:m])

	i, j, k := 0, 0, 0

	// 从前往后填充nums1
	for i < m && j < n {
		if temp[i] <= nums2[j] {
			nums1[k] = temp[i]
			i++
		} else {
			nums1[k] = nums2[j]
			j++
		}
		k++
	}

	// 复制剩余元素
	for i < m {
		nums1[k] = temp[i]
		i++
		k++
	}
	for j < n {
		nums1[k] = nums2[j]
		j++
		k++
	}
}

// =========================== 方法三:简化版双指针 ===========================

func merge3(nums1 []int, m int, nums2 []int, n int) {
	i, j, k := m-1, n-1, m+n-1

	for j >= 0 {
		if i >= 0 && nums1[i] > nums2[j] {
			nums1[k] = nums1[i]
			i--
		} else {
			nums1[k] = nums2[j]
			j--
		}
		k--
	}
}

// =========================== 方法四:优化版(减少比较次数) ===========================

func merge4(nums1 []int, m int, nums2 []int, n int) {
	i, j, k := m-1, n-1, m+n-1

	for i >= 0 && j >= 0 {
		if nums1[i] > nums2[j] {
			nums1[k] = nums1[i]
			i--
		} else {
			nums1[k] = nums2[j]
			j--
		}
		k--
	}

	// 复制nums2的剩余元素
	for j >= 0 {
		nums1[k] = nums2[j]
		j--
		k--
	}
}

// =========================== 测试代码 ===========================

func main() {
	fmt.Println("=== LeetCode 88: 合并两个有序数组 ===\n")

	testCases := []struct {
		name     string
		nums1    []int
		m        int
		nums2    []int
		n        int
		expected []int
	}{
		{
			name:     "Test1: Basic case",
			nums1:    []int{1, 2, 3, 0, 0, 0},
			m:        3,
			nums2:    []int{2, 5, 6},
			n:        3,
			expected: []int{1, 2, 2, 3, 5, 6},
		},
		{
			name:     "Test2: nums2 is empty",
			nums1:    []int{1},
			m:        1,
			nums2:    []int{},
			n:        0,
			expected: []int{1},
		},
		{
			name:     "Test3: nums1 is empty",
			nums1:    []int{0},
			m:        0,
			nums2:    []int{1},
			n:        1,
			expected: []int{1},
		},
		{
			name:     "Test4: nums1 all less than nums2",
			nums1:    []int{1, 2, 3, 0, 0, 0},
			m:        3,
			nums2:    []int{4, 5, 6},
			n:        3,
			expected: []int{1, 2, 3, 4, 5, 6},
		},
		{
			name:     "Test5: nums2 all less than nums1",
			nums1:    []int{4, 5, 6, 0, 0, 0},
			m:        3,
			nums2:    []int{1, 2, 3},
			n:        3,
			expected: []int{1, 2, 3, 4, 5, 6},
		},
		{
			name:     "Test6: Single element in both",
			nums1:    []int{2, 0},
			m:        1,
			nums2:    []int{1},
			n:        1,
			expected: []int{1, 2},
		},
		{
			name:     "Test7: Empty arrays",
			nums1:    []int{0},
			m:        0,
			nums2:    []int{},
			n:        0,
			expected: []int{0},
		},
		{
			name:     "Test8: All same elements",
			nums1:    []int{1, 1, 1, 0, 0, 0},
			m:        3,
			nums2:    []int{1, 1, 1},
			n:        3,
			expected: []int{1, 1, 1, 1, 1, 1},
		},
	}

	methods := map[string]func([]int, int, []int, int){
		"双指针逆序遍历(最优解法)":   merge,
		"双指针正序遍历(需要额外空间)": merge2,
		"简化版双指针":          merge3,
		"优化版(减少比较次数)":     merge4,
	}

	for name, method := range methods {
		fmt.Printf("方法%s:%s\n", name, name)
		passCount := 0
		for i, tt := range testCases {
			// 复制输入数组,避免修改影响后续测试
			nums1Copy := make([]int, len(tt.nums1))
			copy(nums1Copy, tt.nums1)

			method(nums1Copy, tt.m, tt.nums2, tt.n)

			status := "✅"
			if !equal(nums1Copy, tt.expected) {
				status = "❌"
			} else {
				passCount++
			}
			fmt.Printf("  测试%d: %s\n", i+1, status)
			if status == "❌" {
				fmt.Printf("    输入: nums1=%v, m=%d, nums2=%v, n=%d\n", tt.nums1, tt.m, tt.nums2, tt.n)
				fmt.Printf("    输出: %v\n", nums1Copy)
				fmt.Printf("    期望: %v\n", tt.expected)
			}
		}
		fmt.Printf("  通过: %d/%d\n\n", passCount, len(testCases))
	}
}

func equal(a, b []int) bool {
	if len(a) != len(b) {
		return false
	}
	for i := 0; i < len(a); i++ {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}
相关推荐
胖咕噜的稞达鸭4 小时前
封装map和set(红黑树作为底层结构如何实现map和set插入遍历)
c语言·数据结构·c++·算法·gitee·哈希算法
runafterhit4 小时前
算法基础 典型题 数学(基础)
算法
三维小码4 小时前
相机外参初始估计
算法·计算机视觉
宁清明6 小时前
【小宁的学习日记2 C语言】C语言判断
c语言·学习·算法
2401_841495647 小时前
【数据结构】基于Prim算法的最小生成树
java·数据结构·c++·python·算法·最小生成树·prim
木井巳8 小时前
[Java数据结构和算法] HashMap 和 HashSet
java·数据结构·1024程序员节
祈祷苍天赐我java之术9 小时前
解析常见的限流算法
java·数据结构·算法
Shinom1ya_10 小时前
算法 day 34
算法
啊董dong10 小时前
课后作业-2025-10-26
c++·算法·noi