当前位置: 首页 > news >正文

【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中,使合并后的数组保持非递减顺序。

核心思想

双指针 + 逆向遍历

  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 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--}
}

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

边界情况处理

  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:从前往后遍历
// ❌ 错误:从前往后遍历会覆盖未处理的元素
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元素

实战技巧总结

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

进阶扩展

扩展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) {// 原地合并多个数组// ...
}

应用场景

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

代码实现

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

测试结果

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

核心收获

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

应用拓展

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

完整题解代码

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
}
http://www.dtcms.com/a/536020.html

相关文章:

  • Redis键过期策略深度剖析:惰性删除与定期删除的完美平衡
  • 【紧急救援】MySQL root密码忘记?一键重置脚本和全平台解决方案
  • Redis Commander:一款基于Web、免费开源的Redis管理工具
  • 云南省住房和城乡建设局网站做投标的在什么网站找信息
  • 操作系统5.3.4 磁盘的管理
  • Go 编程基础
  • 【Go】P13 Go 语言核心概念:指针 (Pointer) 详解
  • oss中的文件替换后chrome依旧下载到缓存文件概述
  • Go Web 编程快速入门 08 - JSON API:编码、解码与内容协商
  • Golang交叉编译到Android上运行
  • 学网站开发去哪学最好的公文写作网站
  • F035 vue+neo4j中医南药药膳知识图谱可视化系统 | vue+flask
  • 图形数据库Neo4J简介
  • QR算法:矩阵特征值计算的基石
  • 宁波网站建设公司代理珠海集团网站建设报价
  • 「用Python来学微积分」17. 导数与导函数
  • RAID技术:RAID 0/1/5/10 原理、配置与故障恢复
  • 7.1-性能与测试工具
  • linux磁盘使用流程
  • KVM虚拟化部署全流程指南
  • 【用homebrew配置nginx+配置前端项目与后端联调】Macbook M1(附一些homebrew操作)
  • 建立个人博客网站wordpress免费发布信息大全
  • 做设计转钱网站公司网站开发模板
  • 网站建设目标是什么意思win7用本地文件做网站模板
  • VR党建骑行|VR红色骑行漫游|虚拟骑行设备
  • 人脸识别1-Windows下基于MSVC编译opencv-4.5.5
  • 上海网站建设的公司站长之家查询
  • 游戏盾和高防IP的差异与选择
  • 内管理模式和外管理模式的网络比较
  • 【android bluetooth 协议分析 11】【AVDTP详解 4】【A2dp Sink 状态机通俗讲解】