解构 LeetCode 21: 合并两个有序链表 |“虚拟头节点”与“指针”的艺术

大家好!今天我们深入拆解经典算法题 ——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
  • 剩余 p14 → 5(值都比 3 大)
  • 直接拼接后:1 → 2 → 3 → 4 → 5(仍有序)

问题 3:虚拟头节点的值(-1)有什么用?

虚拟头节点的值无实际意义,只是为了占位置。最终返回 dummy.next,跳过虚拟头节点,不影响结果。

五、知识点自查:检验学习成果

回答以下问题,巩固核心知识点:

  1. 虚拟头节点的核心作用是什么?
    → 简化头节点的边界处理(无需判断新链表是否为空),让代码更简洁。

  2. 循环内 p.next = p1 和 p = p.next 能否颠倒顺序?为什么?
    → 不能。颠倒后会导致 p 先移动(可能跳过有效节点),无法正确挂载新节点。

  3. 循环结束后直接拼接剩余链表的前提条件是什么?
    → 原链表本身有序,剩余节点的值必然大于等于已拼接的所有节点,直接拼接不破坏有序性。

六、总结:掌握两大编程思想

通过这道题,你不仅学会了 “合并有序链表” 的解法,更掌握了:

  1. 指针分离思想:用不同指针(施工指针、遍历指针)分离 “操作链表尾部” 和 “遍历原链表” 的职责,让逻辑更清晰。
  2. 虚拟节点技巧:用虚拟头节点简化边界条件,这在链表操作(如删除、合并)中非常常用。

将这些思想内化,面对其他链表问题(如合并 K 个有序链表、删除倒数第 N 个节点)时,你会更得心应手!

如果有其他疑问,欢迎留言讨论~ 觉得有收获的话,点赞 + 收藏鼓励一下吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值