大家好!今天我们深入拆解经典算法题 ——LeetCode 21. 合并两个有序链表。这道题不仅是面试高频考点,更是理解链表操作、指针思想的绝佳实践。
本文会从 **“代码为什么这么写”** 的本质逻辑出发,带你彻底吃透每一行代码的设计意图,掌握链表合并的核心思路。
一、题目回顾:明确目标
问题描述
给定两个非递减有序的链表 l1 和 l2,合并它们为一个新的非递减有序链表并返回头节点。
示例
输入:l1 = [1,2,4],l2 = [1,3,4]
输出:[1,1,2,3,4,4]
函数签名(Python)
python
运行
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
二、核心概念:理解 “内功心法”
在看代码前,先掌握 3 个关键概念,它们是链表操作的 “底层逻辑”:
1. 虚拟头节点(Dummy Head Node)
- 作用:简化边界处理,避免 “空链表” 判断。
- 类比:给链表挂一个 “班级牌”,无论链表是否为空,都能统一操作(比如直接
p.next挂节点)。 - 必要性:如果不用虚拟头节点,需要单独处理 “新链表为空” 时头节点的初始化,代码会更复杂。
2. 指针的 “舞蹈”
- 施工指针
p:始终指向新链表的尾部,负责 “挂载新节点”(类似建筑工人,每次把新砖块接到当前建筑末尾)。 - 遍历指针
p1/p2:分别遍历原链表l1/l2,负责 “选哪个节点挂载”(类似两个采购员,每次选较小的货物递给工人)。
3. 链表的 “拼接本质”
链表节点的 next 是引用传递。只要将 p.next 指向剩余链表的头节点,就能一次性拼接剩余所有节点(因为头节点的 next 已串联后续节点)。
三、代码逐行剖析:逻辑全解析
结合 “内功心法”,逐行拆解代码的设计逻辑:
python
运行
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
# 1. 初始化:搭建舞台(虚拟头节点 + 指针)
dummy = ListNode(-1) # 虚拟头节点:简化头节点处理,相当于“班级牌”
p = dummy # 施工指针 p:初始指向虚拟头节点,负责“接新节点”
p1, p2 = l1, l2 # 遍历指针 p1/p2:分别遍历 l1/l2 的当前节点
# 2. 主循环:穿针引线,构建新链表
while p1 is not None and p2 is not None:
# 核心逻辑:选较小的节点挂载到新链表尾部
if p1.val > p2.val:
p.next = p2 # 把 p2 节点接到新链表尾部
p2 = p2.next # p2 后移,准备下一次比较
else:
p.next = p1 # 把 p1 节点接到新链表尾部
p1 = p1.next # p1 后移,准备下一次比较
p = p.next # 施工指针 p 后移到新链表的尾部(刚接上的节点)
# 3. 收尾工作:处理剩余节点(最多一个链表有剩余)
if p1 is not None:
p.next = p1 # p1 剩余部分直接拼接(利用链表引用特性)
if p2 is not None:
p.next = p2 # p2 剩余部分直接拼接(利用链表引用特性)
# 4. 返回结果:虚拟头节点的下一个节点才是新链表的真实头
return dummy.next
关键逻辑拆解
1. 虚拟头节点的价值
- 无需判断
p是否为空:如果不用dummy,第一次挂载节点时需要单独处理p为None的情况(比如p = p1或p = p2)。 - 代码更简洁:统一用
p.next挂载节点,逻辑更连贯。
2. 循环内的指针操作
p.next = p1/p2:将较小节点 “接” 到新链表尾部(类似工人把砖块接到建筑末尾)。p1 = p1.next/p2 = p2.next:遍历指针后移(类似采购员移动到下一个货物)。p = p.next:施工指针后移到新链表尾部(工人移动到新接的砖块位置,准备下次接砖)。
3. 收尾逻辑的本质
循环结束时,p1 和 p2 至少有一个为 None(链表已遍历完)。由于原链表本身有序,剩余节点直接拼接即可保证整体有序(比如 p1 剩余节点值都比已拼接的大,直接接在尾部不影响顺序)。
四、常见疑惑深度解答
问题 1:p.next = p1 和 p = p.next 能颠倒顺序吗?
不能!
- 原逻辑:先把
p1接到p.next,再让p移动到新尾部(刚接的节点)。 - 颠倒后:
p = p.next会先移动指针(此时p.next可能是None或旧节点),再执行p.next = p1会导致错误(可能丢失后续节点或覆盖已有节点)。
示例(颠倒顺序):
python
运行
# 错误逻辑:先移动 p,再挂载节点
p = p.next # p 移动到原链表尾部(假设原尾部是 A)
p.next = p1 # 把 p1 接到 A 的后面(正确逻辑应先接节点再移动)
这会导致 p 移动后,无法正确挂载新节点(原 p.next 的位置被跳过)。
问题 2:为什么循环结束后直接拼接剩余链表?
因为原链表本身有序,且循环的结束条件是 “至少一个链表已遍历完”。剩余节点的值都大于等于已拼接的所有节点(否则会被循环处理),所以直接拼接不会破坏有序性。
示例:
- 已拼接:
1 → 2 → 3 - 剩余
p1:4 → 5(值都比 3 大) - 直接拼接后:
1 → 2 → 3 → 4 → 5(仍有序)
问题 3:虚拟头节点的值(-1)有什么用?
虚拟头节点的值无实际意义,只是为了占位置。最终返回 dummy.next,跳过虚拟头节点,不影响结果。
五、知识点自查:检验学习成果
回答以下问题,巩固核心知识点:
-
虚拟头节点的核心作用是什么?
→ 简化头节点的边界处理(无需判断新链表是否为空),让代码更简洁。 -
循环内
p.next = p1和p = p.next能否颠倒顺序?为什么?
→ 不能。颠倒后会导致p先移动(可能跳过有效节点),无法正确挂载新节点。 -
循环结束后直接拼接剩余链表的前提条件是什么?
→ 原链表本身有序,剩余节点的值必然大于等于已拼接的所有节点,直接拼接不破坏有序性。
六、总结:掌握两大编程思想
通过这道题,你不仅学会了 “合并有序链表” 的解法,更掌握了:
- 指针分离思想:用不同指针(施工指针、遍历指针)分离 “操作链表尾部” 和 “遍历原链表” 的职责,让逻辑更清晰。
- 虚拟节点技巧:用虚拟头节点简化边界条件,这在链表操作(如删除、合并)中非常常用。
将这些思想内化,面对其他链表问题(如合并 K 个有序链表、删除倒数第 N 个节点)时,你会更得心应手!
如果有其他疑问,欢迎留言讨论~ 觉得有收获的话,点赞 + 收藏鼓励一下吧!

605

被折叠的 条评论
为什么被折叠?



