文章目录
- [88. 合并两个有序数组](#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中,使合并后的数组保持非递减顺序。
核心思想
双指针 + 逆向遍历:
- 双指针:使用两个指针分别指向两个数组的有效元素末尾
- 逆向遍历:从后往前填充nums1数组
- 元素比较:比较两个指针指向的元素,取较大值填充
- 位置调整:将较大的元素放在合适的位置
关键技巧:
- 从后往前遍历,避免覆盖未处理的元素
- 使用两个指针分别遍历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长度
算法流程图
主算法流程(双指针逆序遍历)
详细比较流程
合并过程可视化
复杂度分析
时间复杂度详解
双指针算法(逆序遍历):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--
}
}
特点:减少不必要的比较,提高效率
边界情况处理
- nums1为空数组:直接复制nums2到nums1
- nums2为空数组:nums1保持不变
- 两个数组都为空:返回空数组
- nums1全部小于nums2:直接将nums2追加到nums1
- 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元素
实战技巧总结
- 双指针模板:i=m-1, j=n-1, k=m+n-1
- 逆向遍历:从后往前填充,避免覆盖
- 元素比较:比较两个指针指向的元素
- 剩余处理:处理剩余元素
- 边界检查:检查指针是否越界
进阶扩展
扩展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) {
// 原地合并多个数组
// ...
}
应用场景
- 数组合并:合并多个有序数组
- 排序算法:归并排序的核心操作
- 数据处理:数据清洗和合并
- 算法竞赛:双指针经典应用
- 系统设计:数据合并和同步
代码实现
本题提供了四种不同的解法,重点掌握双指针逆序遍历算法。
测试结果
| 测试用例 | 双指针逆序 | 双指针正序 | 简化版 | 优化版 |
|---|---|---|---|---|
| 基础测试 | ✅ | ✅ | ✅ | ✅ |
| 简单情况 | ✅ | ✅ | ✅ | ✅ |
| 特殊情况 | ✅ | ✅ | ✅ | ✅ |
| 边界情况 | ✅ | ✅ | ✅ | ✅ |
核心收获
- 双指针算法:数组合并的经典应用
- 逆向遍历:避免覆盖未处理的元素
- 元素比较:准确比较两个数组的元素
- 剩余处理:处理剩余元素
- 边界处理:各种边界情况的考虑
应用拓展
- 数组合并和排序
- 双指针经典应用
- 归并排序基础
- 数据处理技术
- 系统设计应用
完整题解代码
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
}