【LeetCode】88. 合并两个有序数组
文章目录
- 88. 合并两个有序数组
- 题目描述
- 示例 1:
- 示例 2:
- 示例 3:
- 提示:
- 进阶:你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗?
- 解题思路
- 问题深度分析
- 问题本质
- 核心思想
- 关键难点分析
- 典型情况分析
- 算法对比
- 算法流程图
- 主算法流程(双指针逆序遍历)
- 详细比较流程
- 合并过程可视化
- 复杂度分析
- 时间复杂度详解
- 空间复杂度详解
- 关键优化技巧
- 技巧1:双指针逆序遍历(最优解法)
- 技巧2:双指针正序遍历(需要额外空间)
- 技巧3:简化版双指针
- 技巧4:优化版(减少比较次数)
- 边界情况处理
- 测试用例设计
- 基础测试
- 简单情况
- 特殊情况
- 边界情况
- 常见错误与陷阱
- 错误1:从前往后遍历
- 错误2:边界检查不正确
- 错误3:剩余元素未处理
- 实战技巧总结
- 进阶扩展
- 扩展1:返回新数组
- 扩展2:支持多个数组合并
- 扩展3:原地合并多个有序数组
- 应用场景
- 代码实现
- 测试结果
- 核心收获
- 应用拓展
- 完整题解代码
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长度
算法流程图
主算法流程(双指针逆序遍历)
graph TDA[开始: nums1, m, nums2, n] --> B[m == 0?]B -->|是| C[直接复制nums2到nums1]C --> D[结束]B -->|否| E[n == 0?]E -->|是| DE -->|否| F[初始化指针: i=m-1, j=n-1, k=m+n-1]F --> G{k >= 0?}G -->|否| DG -->|是| 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 --> GL --> GH -->|否| M{i >= 0?}M -->|是| N[复制nums1剩余元素]M -->|否| O[复制nums2剩余元素]N --> DO --> D
详细比较流程
graph TDA[当前位置 k] --> B{还有元素未处理?}B -->|否| C[合并完成]B -->|是| D{两个数组都有元素?}D -->|是| E[比较nums1[i]和nums2[j]]E --> F[选择较大值]F --> G[放入nums1[k]]G --> H[更新指针]H --> BD -->|否| I{只有nums1有元素?}I -->|是| J[复制nums1剩余元素]I -->|否| K[复制nums2剩余元素]J --> CK --> C
合并过程可视化
graph LRsubgraph 初始状态A1[nums1: 1,2,3,0,0,0]B1[nums2: 2,5,6]C1[i=2, j=2, k=5]endsubgraph 第一轮A2[比较nums1[2]=3和nums2[2]=6]B2[6较大, nums1[5]=6]C2[i=2, j=1, k=4]endsubgraph 第二轮A3[比较nums1[2]=3和nums2[1]=5]B3[5较大, nums1[4]=5]C3[i=2, j=0, k=3]endsubgraph 第三轮A4[比较nums1[2]=3和nums2[0]=2]B4[3较大, nums1[3]=3]C4[i=1, j=0, k=2]endsubgraph 最终结果A5[nums1: 1,2,2,3,5,6]endA1 --> 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:双指针逆序遍历(最优解法)
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// 从后往前填充nums1for 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:双指针正序遍历(需要额外空间)
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// 从前往后填充nums1for 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:简化版双指针
func merge(nums1 []int, m int, nums2 []int, n int) {i, j, k := m-1, n-1, m+n-1for j >= 0 {if i >= 0 && nums1[i] > nums2[j] {nums1[k] = nums1[i]i--} else {nums1[k] = nums2[j]j--}k--}
}
特点:代码更简洁,逻辑更清晰
技巧4:优化版(减少比较次数)
func merge(nums1 []int, m int, nums2 []int, n int) {i, j, k := m-1, n-1, m+n-1for 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:从前往后遍历
// ❌ 错误:从前往后遍历会覆盖未处理的元素
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:边界检查不正确
// ❌ 错误:边界检查不正确
for k >= 0 {if nums1[i] > nums2[j] { // 错误:没有检查i和j是否越界nums1[k] = nums1[i]i--} else {nums1[k] = nums2[j]j--}k--
}
问题:没有检查指针是否越界
错误3:剩余元素未处理
// ❌ 错误:剩余元素未处理
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:返回新数组
func mergeNew(nums1 []int, m int, nums2 []int, n int) []int {result := make([]int, m+n)// 合并逻辑return result
}
扩展2:支持多个数组合并
func mergeMultiple(arrays [][]int) []int {// 合并多个数组// ...
}
扩展3:原地合并多个有序数组
func mergeInPlace(nums1 []int, m int, arrays [][]int) {// 原地合并多个数组// ...
}
应用场景
- 数组合并:合并多个有序数组
- 排序算法:归并排序的核心操作
- 数据处理:数据清洗和合并
- 算法竞赛:双指针经典应用
- 系统设计:数据合并和同步
代码实现
本题提供了四种不同的解法,重点掌握双指针逆序遍历算法。
测试结果
| 测试用例 | 双指针逆序 | 双指针正序 | 简化版 | 优化版 |
|---|---|---|---|---|
| 基础测试 | ✅ | ✅ | ✅ | ✅ |
| 简单情况 | ✅ | ✅ | ✅ | ✅ |
| 特殊情况 | ✅ | ✅ | ✅ | ✅ |
| 边界情况 | ✅ | ✅ | ✅ | ✅ |
核心收获
- 双指针算法:数组合并的经典应用
- 逆向遍历:避免覆盖未处理的元素
- 元素比较:准确比较两个数组的元素
- 剩余处理:处理剩余元素
- 边界处理:各种边界情况的考虑
应用拓展
- 数组合并和排序
- 双指针经典应用
- 归并排序基础
- 数据处理技术
- 系统设计应用
完整题解代码
package mainimport ("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// 从后往前填充nums1for 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// 从前往后填充nums1for 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-1for 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-1for 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 stringnums1 []intm intnums2 []intn intexpected []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 := 0for 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
}
