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

【LeetCode】24. 两两交换链表中的节点

文章目录

  • 24. 两两交换链表中的节点
    • 示例 1:
    • 示例 2:
    • 示例 3:
    • 提示:
    • 解题思路
      • 1. 算法分析
        • 1.1 问题本质
        • 1.2 关键挑战
        • 1.3 算法分类
      • 2. 核心算法详解
        • 2.1 迭代法(推荐解法)
          • 2.1.1 算法思想
          • 2.1.2 详细流程图
          • 2.1.3 交换操作详解
          • 2.1.4 指针移动策略
        • 2.2 递归法
          • 2.2.1 算法思想
          • 2.2.2 递归流程图
          • 2.2.3 递归调用栈分析
        • 2.3 优化迭代法
          • 2.3.1 算法思想
          • 2.3.2 优化对比
      • 3. 算法复杂度分析
        • 3.1 时间复杂度分析
        • 3.2 空间复杂度分析
      • 4. 边界条件处理
        • 4.1 边界情况分类
        • 4.2 详细边界处理
          • 4.2.1 空链表处理
          • 4.2.2 单节点链表处理
          • 4.2.3 奇数个节点处理
        • 4.3 边界测试用例设计
      • 5. 算法优化策略
        • 5.1 性能优化
          • 5.1.1 减少函数调用
          • 5.1.2 减少内存分配
        • 5.2 代码优化
          • 5.2.1 使用常量
          • 5.2.2 提前返回
      • 6. 实际应用场景
        • 6.1 链表重排序
        • 6.2 链表反转
        • 6.3 链表合并
      • 7. 常见错误与陷阱
        • 7.1 指针丢失错误
        • 7.2 边界条件遗漏
        • 7.3 循环条件错误
      • 8. 测试策略
        • 8.1 单元测试设计
        • 8.2 测试用例覆盖
      • 9. 代码实现要点
        • 9.1 关键代码段分析
        • 9.2 代码质量要求
      • 10. 总结与展望
        • 10.1 算法特点总结
        • 10.2 扩展思考
        • 10.3 学习建议
    • 完整题解代码

24. 两两交换链表中的节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:

在这里插入图片描述

输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

输入:head = []
输出:[]

示例 3:

输入:head = [1]
输出:[1]

提示:

  • 链表中节点的数目在范围 [0, 100] 内
  • 0 <= Node.val <= 100

解题思路

1. 算法分析

1.1 问题本质

这道题的核心是链表节点的重新连接,而不是简单的值交换。我们需要:

  • 保持节点值不变
  • 重新调整节点的Next指针
  • 确保链表结构完整且无环
1.2 关键挑战
  1. 头节点处理:交换后头节点会改变,需要特殊处理
  2. 指针关系维护:交换过程中容易丢失节点引用
  3. 边界条件:空链表、单节点、奇数个节点等特殊情况
  4. 指针顺序:交换操作的顺序至关重要,错误的顺序会导致链表断裂
1.3 算法分类

根据实现方式,可以分为三大类:

  • 迭代法:使用循环逐个处理节点对
  • 递归法:使用递归处理子问题
  • 优化法:在迭代基础上进行空间或时间优化

2. 核心算法详解

2.1 迭代法(推荐解法)
2.1.1 算法思想

迭代法的核心思想是维护三个关键指针

  • prev:当前处理节点对的前驱节点
  • first:当前处理节点对的第一个节点
  • second:当前处理节点对的第二个节点

通过这三个指针,我们可以安全地完成节点交换而不丢失引用。

2.1.2 详细流程图
flowchart TDA[开始:创建虚拟头节点dummy] --> B[初始化prev = dummy]B --> C{检查是否还有两个节点可交换}C -->|否| D[返回dummy.Next]C -->|是| E[保存first = prev.Next]E --> F[保存second = prev.Next.Next]F --> G[执行三步交换操作]G --> H[第一步:first.Next = second.Next]H --> I[第二步:second.Next = first]I --> J[第三步:prev.Next = second]J --> K[移动prev到first位置]K --> Cstyle A fill:#e1f5festyle D fill:#c8e6c9style G fill:#fff3e0style K fill:#f3e5f5
2.1.3 交换操作详解
交换后状态
交换前状态
prev.Next = second
first.Next = third
second.Next = first
second
prev
first
third
first
prev
second
third

三步交换操作

  1. first.Next = second.Next:第一个节点指向第三个节点

    • 目的:保持与后续节点的连接
    • 风险:如果先执行其他步骤,可能丢失引用
  2. second.Next = first:第二个节点指向第一个节点

    • 目的:建立反向连接
    • 时机:在第一步之后执行,确保引用安全
  3. prev.Next = second:前驱节点指向第二个节点

    • 目的:更新前驱的指向
    • 时机:最后执行,避免破坏已建立的连接
2.1.4 指针移动策略
指针移动示例
prev=node1
prev=dummy
prev=node3
prev=node5
初始状态
处理第一对
prev移动到first位置
处理第二对
prev移动到first位置
处理第三对

为什么移动到first位置?

  • first是当前处理节点对的第一个节点
  • 交换后,first变成了下一对的前驱
  • 这样prev就能正确指向下一对的前驱节点
2.2 递归法
2.2.1 算法思想

递归法采用自底向上的策略:

  1. 先递归处理后续节点
  2. 再处理当前两个节点
  3. 返回处理后的头节点

这种方法的优势是代码简洁,逻辑清晰,但需要注意递归深度。

2.2.2 递归流程图
head为空或只有一个节点
有两个或更多节点
swapPairsRecursive开始
检查边界条件
返回head
保存second = head.Next
递归调用swapPairsRecursive
处理second.Next及后续节点
交换当前两个节点
second.Next = head
head.Next = 递归结果
返回second作为新头
2.2.3 递归调用栈分析
graph TDsubgraph "递归调用栈"A[swapPairs[1,2,3,4,5]] --> B[swapPairs[3,4,5]]B --> C[swapPairs[5]]C --> D[返回[5]]D --> E[处理[3,4]对]E --> F[返回[4,3,5]]F --> G[处理[1,2]对]G --> H[返回[2,1,4,3,5]]endsubgraph "节点交换过程"I[1->2->3->4->5] --> J[1->2->4->3->5]J --> K[2->1->4->3->5]end

递归终止条件

  • head == nil:空链表
  • head.Next == nil:单节点链表

递归逻辑

  1. 保存引用second = head.Next
  2. 递归处理head.Next = swapPairsRecursive(second.Next)
  3. 交换当前second.Next = head
  4. 返回新头return second
2.3 优化迭代法
2.3.1 算法思想

优化迭代法利用Go语言的多重赋值特性,将三步交换操作合并为一行代码,减少中间变量,提高代码简洁性。

2.3.2 优化对比
优化迭代法
标准迭代法
需要3个步骤
只需1个步骤
移动prev指针
一行多重赋值
三步交换操作
保存first和second
移动prev指针

多重赋值原理

prev.Next.Next, prev.Next.Next.Next, prev.Next = prev.Next, prev.Next, prev.Next.Next

这行代码等价于:

temp1 := prev.Next.Next        // 保存second
temp2 := prev.Next.Next.Next   // 保存third
temp3 := prev.Next             // 保存firstprev.Next.Next = temp3         // second.Next = first
prev.Next.Next.Next = temp3    // second.Next.Next = first (这里有问题)
prev.Next = temp1              // prev.Next = second

注意:这种写法虽然简洁,但可读性较差,容易出错,建议在理解透彻后使用。

3. 算法复杂度分析

3.1 时间复杂度分析
graph TDA[链表长度n] --> B[迭代法]A --> C[递归法]A --> D[优化迭代法]B --> E[O(n): 每个节点访问一次]C --> F[O(n): 每个节点访问一次]D --> G[O(n): 每个节点访问一次]subgraph "详细分析"H[遍历次数: n/2] --> I[每次操作: O(1)]I --> J[总复杂度: O(n)]end

详细计算

  • 链表长度为n
  • 需要交换n/2对节点(向下取整)
  • 每对节点的交换操作是O(1)
  • 总时间复杂度:O(n/2) × O(1) = O(n)
3.2 空间复杂度分析
graph TDA[空间复杂度] --> B[迭代法]A --> C[递归法]A --> D[优化迭代法]B --> E[O(1): 只使用常数个指针变量]C --> F[O(n): 递归调用栈深度]D --> G[O(1): 只使用常数个指针变量]subgraph "空间使用对比"H[迭代法: dummy + prev + first + second] --> I[4个变量 = O(1)]J[递归法: 递归栈深度] --> K[最坏情况: n/2层 = O(n)]end

空间使用详情

  • 迭代法:O(1)

    • dummy:虚拟头节点
    • prev:前驱指针
    • first:第一个节点指针
    • second:第二个节点指针
  • 递归法:O(n)

    • 递归调用栈深度:n/2层
    • 每层保存函数参数和局部变量
  • 优化迭代法:O(1)

    • 与标准迭代法相同
    • 只是减少了中间变量

4. 边界条件处理

4.1 边界情况分类
graph TDA[边界条件] --> B[空链表]A --> C[单节点链表]A --> D[双节点链表]A --> E[奇数个节点]A --> F[偶数个节点]B --> G[直接返回nil]C --> H[无需交换,返回原头]D --> I[交换两个节点]E --> J[最后一组不完整,不交换]F --> K[所有节点对都交换]
4.2 详细边界处理
4.2.1 空链表处理
if head == nil {return nil
}
  • 原因:空链表没有节点可交换
  • 处理:直接返回nil
  • 测试用例[][]
4.2.2 单节点链表处理
if head.Next == nil {return head
}
  • 原因:只有一个节点,无法形成节点对
  • 处理:返回原头节点
  • 测试用例[1][1]
4.2.3 奇数个节点处理
// 在迭代循环中
for prev.Next != nil && prev.Next.Next != nil {// 只有当存在两个节点时才交换
}
  • 原因:最后一组可能只有一个节点
  • 处理:不交换,保持原状
  • 测试用例[1,2,3][2,1,3]
4.3 边界测试用例设计
graph LRA[边界测试] --> B[空链表]A --> C[单节点]A --> D[双节点]A --> E[三节点]A --> F[四节点]A --> G[五节点]B --> H[[] → []]C --> I[[1] → [1]]D --> J[[1,2] → [2,1]]E --> K[[1,2,3] → [2,1,3]]F --> L[[1,2,3,4] → [2,1,4,3]]G --> M[[1,2,3,4,5] → [2,1,4,3,5]]

5. 算法优化策略

5.1 性能优化
5.1.1 减少函数调用
// 优化前:多次调用len()
for i := 0; i < len(nodes); i++ {// 处理逻辑
}// 优化后:缓存长度
length := len(nodes)
for i := 0; i < length; i++ {// 处理逻辑
}
5.1.2 减少内存分配
// 优化前:每次创建新节点
newNode := &ListNode{Val: val}// 优化后:复用现有节点
// 直接修改指针关系,不创建新节点
5.2 代码优化
5.2.1 使用常量
const (MIN_NODES_FOR_SWAP = 2MAX_NODES = 100
)
5.2.2 提前返回
// 提前检查边界条件
if head == nil || head.Next == nil {return head
}

6. 实际应用场景

6.1 链表重排序
  • 场景:需要将链表按照特定规则重新排列
  • 应用:数据预处理、算法优化
  • 示例:将链表按奇偶位置分组
6.2 链表反转
  • 场景:需要部分反转链表
  • 应用:字符串处理、数据转换
  • 示例:每K个节点为一组进行反转
6.3 链表合并
  • 场景:需要交替合并两个链表
  • 应用:数据融合、算法实现
  • 示例:将两个有序链表交替合并

7. 常见错误与陷阱

7.1 指针丢失错误
错误操作顺序
先修改prev.Next
丢失first引用
无法完成交换
链表断裂
正确操作顺序
先修改first.Next
再修改second.Next
最后修改prev.Next
交换成功

错误示例

// 错误:先修改prev.Next,丢失引用
prev.Next = second        // 丢失first引用
first.Next = second.Next  // 无法访问first
second.Next = first       // 无法访问first
7.2 边界条件遗漏
  • 问题:忘记处理空链表或单节点链表
  • 后果:程序崩溃或结果错误
  • 解决:在函数开始处添加边界检查
7.3 循环条件错误
  • 问题:循环条件设置不当
  • 后果:无限循环或提前退出
  • 解决:仔细分析循环终止条件

8. 测试策略

8.1 单元测试设计
测试策略
功能测试
边界测试
性能测试
异常测试
正常交换
多节点交换
空链表
单节点
奇数节点
大链表性能
异常输入
8.2 测试用例覆盖

功能测试用例

  1. [1,2,3,4][2,1,4,3]:标准偶数节点
  2. [1,2,3][2,1,3]:奇数节点
  3. [1,2][2,1]:双节点

边界测试用例

  1. [][]:空链表
  2. [1][1]:单节点
  3. [1,2,3,4,5][2,1,4,3,5]:大奇数链表

异常测试用例

  1. nilnil:空指针
  2. 包含重复值的链表
  3. 包含负值的链表

9. 代码实现要点

9.1 关键代码段分析
func swapPairs(head *ListNode) *ListNode {// 1. 创建虚拟头节点dummy := &ListNode{Next: head}prev := dummy// 2. 循环处理节点对for prev.Next != nil && prev.Next.Next != nil {// 3. 保存当前两个节点first := prev.Nextsecond := prev.Next.Next// 4. 执行三步交换first.Next = second.Next    // 第一步second.Next = first         // 第二步prev.Next = second          // 第三步// 5. 移动prev指针prev = first}return dummy.Next
}
9.2 代码质量要求

可读性

  • 变量命名清晰:first, second, prev
  • 注释详细:每个步骤都有说明
  • 结构清晰:逻辑分明的代码块

健壮性

  • 边界条件检查完整
  • 指针操作安全
  • 异常情况处理

可维护性

  • 函数职责单一
  • 代码复用性好
  • 易于扩展和修改

10. 总结与展望

10.1 算法特点总结

优势

  1. 时间复杂度优秀:O(n),每个节点只访问一次
  2. 空间复杂度可控:迭代法O(1),递归法O(n)
  3. 实现相对简单:逻辑清晰,易于理解
  4. 适用性广泛:可以处理各种长度的链表

局限性

  1. 只能两两交换:无法处理其他交换模式
  2. 递归版本有栈溢出风险:对于超长链表
  3. 指针操作容易出错:需要仔细处理引用关系
10.2 扩展思考

变种问题

  1. K个一组反转:每K个节点为一组进行反转
  2. 交替合并:将两个链表交替合并
  3. 奇偶分离:将链表按奇偶位置分离

优化方向

  1. 并行处理:对于超长链表,可以考虑并行处理
  2. 缓存优化:利用CPU缓存特性优化访问模式
  3. 内存池:减少频繁的内存分配和释放
10.3 学习建议
  1. 理解指针操作:这是链表操作的基础
  2. 画图分析:复杂问题用图表辅助理解
  3. 多角度思考:尝试不同的解题思路
  4. 实践验证:多写测试用例验证正确性
  5. 总结规律:找出解题的通用模式

这道题是链表操作的经典题目,通过深入理解其解题思路,可以掌握链表操作的核心技巧,为后续更复杂的链表问题打下坚实基础。

本仓库 24/main.go 提供了三种完整的实现方式,并在 main() 函数中包含了全面的测试用例,可以直接运行验证算法的正确性。

完整题解代码

package mainimport ("fmt"
)type ListNode struct {Val  intNext *ListNode
}// swapPairs 迭代法:虚拟头 + 三指针操作
// 时间复杂度: O(n);空间复杂度: O(1)
func swapPairs(head *ListNode) *ListNode {dummy := &ListNode{Next: head}prev := dummyfor prev.Next != nil && prev.Next.Next != nil {// 保存当前两个节点first := prev.Nextsecond := prev.Next.Next// 交换操作first.Next = second.Nextsecond.Next = firstprev.Next = second// 移动到下一组prev = first}return dummy.Next
}// swapPairsRecursive 递归法:先处理后续,再交换当前两个
func swapPairsRecursive(head *ListNode) *ListNode {if head == nil || head.Next == nil {return head}// 保存第二个节点second := head.Next// 递归处理后续节点head.Next = swapPairsRecursive(second.Next)// 交换当前两个节点second.Next = headreturn second
}// swapPairsOptimized 优化迭代:减少变量使用
func swapPairsOptimized(head *ListNode) *ListNode {dummy := &ListNode{Next: head}prev := dummyfor prev.Next != nil && prev.Next.Next != nil {// 直接交换prev.Next.Next, prev.Next.Next.Next, prev.Next =prev.Next, prev.Next, prev.Next.Nextprev = prev.Next.Next}return dummy.Next
}// 辅助:从切片构造链表
func buildList(vals []int) *ListNode {if len(vals) == 0 {return nil}head := &ListNode{Val: vals[0]}curr := headfor i := 1; i < len(vals); i++ {curr.Next = &ListNode{Val: vals[i]}curr = curr.Next}return head
}// 辅助:链表转切片
func listToSlice(head *ListNode) []int {res := []int{}for head != nil {res = append(res, head.Val)head = head.Next}return res
}func main() {// 示例1: head=[1,2,3,4] -> [2,1,4,3]h1 := buildList([]int{1, 2, 3, 4})ans1 := swapPairs(h1)fmt.Printf("示例1: %v\n", listToSlice(ans1))// 示例2: head=[] -> []h2 := buildList([]int{})ans2 := swapPairs(h2)fmt.Printf("示例2: %v\n", listToSlice(ans2))// 示例3: head=[1] -> [1]h3 := buildList([]int{1})ans3 := swapPairs(h3)fmt.Printf("示例3: %v\n", listToSlice(ans3))// 额外: 奇数个节点 head=[1,2,3] -> [2,1,3]h4 := buildList([]int{1, 2, 3})ans4 := swapPairs(h4)fmt.Printf("额外: %v\n", listToSlice(ans4))// 测试递归版本h5 := buildList([]int{1, 2, 3, 4, 5})ans5 := swapPairsRecursive(h5)fmt.Printf("递归版: %v\n", listToSlice(ans5))
}
http://www.dtcms.com/a/347763.html

相关文章:

  • 青少年机器人技术(五级)等级考试试卷(2021年12月)
  • Linux:4_进程概念
  • Python 文件操作全解析:模式、方法与实战案例
  • openharmony之启动恢复子系统详解
  • 控制建模matlab练习14:线性状态反馈控制器-③极点配置
  • 河南萌新联赛2025第(六)场:郑州大学
  • nodejs 集成mongodb实现增删改查
  • 基于深度学习的中草药识别系统:从零到部署的完整实践
  • CA6150主轴箱系统设计cad+设计说明书
  • Java 学习笔记(基础篇8)
  • MQTT 核心概念与协议演进全景解读(二)
  • BEVDepth
  • 9.Shell脚本修炼手册---数值计算实践
  • python re模块常用方法
  • 取件码-快递取件助手, 短信自动识别ios app Tech Support
  • Access开发打造专业的开关按钮效果
  • rust语言 (1.88) egui (0.32.1) 学习笔记(逐行注释)(七) 鼠标在控件上悬浮时的提示
  • Meta押注Midjourney:一场关于生成式AI的加速赛
  • 【读代码】SQLBot:开源自然语言转SQL智能助手原理与实践
  • GUAVA 实现限流
  • GEO优化服务商赋能全球数字经济发展 技术创新引领行业新格局
  • Java—— 动态代理
  • 基于Python与Tkinter的校园点餐系统设计与实现
  • Spring Data Redis基础
  • [Vid-LLM] docs | 视频理解任务
  • Windows应急响应一般思路(三)
  • 第1.2节:早期AI发展(1950-1980)
  • 老字号:用 “老根” 熬活的 “新味道”
  • redis---string类型详解
  • 大模型四种常见安全问题与攻击案例