dummy=ListNode(-1)代表的就是虚拟节点的意思。
因为dummy本身就有假的虚拟的的意思,而且-1 0 NONE等值(不符合实际情况的)一般也认为是虚拟节点的标志。
分割链表
def partition(self, head: ListNode, x: int) -> ListNode:
定义一个分割函数,其中head的类型是链表(head既可以是头节点也可以是链表)
# 存放小于 x 的链表的虚拟头结点
dummy1 = ListNode(-1) 建一个虚拟头节点1
# 存放大于等于 x 的链表的虚拟头结点
dummy2 = ListNode(-1) 建一个虚拟头节点2
# p1, p2 指针负责生成结果链表
p1, p2 = dummy1, dummy2 给虚拟节点配上指针
# p 负责遍历原链表,类似合并两个有序链表的逻辑
# 这里是将一个链表分解成两个链表
p = head 给原先的链表也配上指针
while p: 当p也就是指针所对应的一直有节点时进行这个循环
if p.val >= x:
p2.next = p 让p2所对应节点的指针与p链接,然后再让p2往前移动
p2 = p2.next
else:
p1.next = p
p1 = p1.next 同理
# 不能直接让 p 指针前进,
# p = p.next
# 断开原链表中的每个节点的 next 指针
temp = p.next 先将指针对应的下一个节点存放再temp以免丢失
这里进行分解时要将节点与原来的链表的关系断干净,所以不能直接将指针直接移动到下一个位置,得利用其先进行干净。
p.next = None 然后让指针对应的节点与之后节点的关系断干净。
p = temp 之后继续 p=p.next 往前面移动
# 连接两个链表
p1.next = dummy2.next 进行链接(因为再循环之外所以代表的就是于其之后的所有节点)
return dummy1.next 然后返回分割后又链接后的链表
AI总结
巧用虚拟头节点:轻松解决链表分割问题
一、核心问题:链表分割的本质需求
题目定义
给定一个链表的头节点 head
和一个特定值 x
,要求将链表分隔为两部分:
- 所有小于 x的节点排在前面,
- 所有大于或等于 x的节点排在后面,
- 并且保留各节点的初始相对顺序。
示例解析
输入:head = [1, 4, 3, 2, 5, 2]
, x = 3
输出:[1, 2, 2, 4, 3, 5]
- 小于 3 的节点:
1, 2, 2
(顺序不变), - 大于等于 3 的节点:
4, 3, 5
(顺序不变), - 最终合并为:
[小节点部分] + [大节点部分]
。
二、核心思路:"拆分 - 合并" 的链表手术
直接在原链表上移动节点就像在复杂电路中改线,极易出错。更聪明的做法是:
化整为零,再合二为一—— 先将原链表拆成两个子链表,再按顺序合并。
-
创建两个子链表:
small链表
:存储所有小于 x 的节点,large链表
:存储所有大于等于 x 的节点。
-
遍历拆分:
从头到尾扫描原链表,根据节点值判断归属,分别追加到对应子链表。 -
合并结果:
将small链表
的尾部与large链表
的头部连接,形成最终链表。
三、神兵利器:虚拟头节点的妙用
为什么需要虚拟头节点?
想象你要组装玩具火车:
- 没有虚拟头节点时,添加第一个车厢需要单独处理(比如 "如果火车头为空,就把车厢当车头"),
- 有了虚拟头节点(类似火车头的挂钩),不管有没有车厢,都可以统一用 "挂钩后面接车厢" 的逻辑。
虚拟头节点的核心价值:
- 简化边界处理:避免判断链表是否为空,
- 统一操作逻辑:无论链表是否为空,追加节点的代码逻辑一致,
- 代码更健壮:减少因边界条件漏写导致的 bug。
四、代码实战:从思路到实现
python
# Definition for singly-linked list.
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def partition(self, head: ListNode, x: int) -> ListNode:
# 1. 初始化:创建虚拟头节点和操作指针
dummy1 = ListNode(-1) # small链表的虚拟头节点(哨兵)
p1 = dummy1 # p1指向small链表的尾部,负责追加节点
dummy2 = ListNode(-1) # large链表的虚拟头节点
p2 = dummy2 # p2指向large链表的尾部
p = head # p遍历原链表
# 2. 遍历原链表,拆分节点到两个子链表
while p:
if p.val < x:
# 接到small链表尾部
p1.next = p
p1 = p1.next
else:
# 接到large链表尾部
p2.next = p
p2 = p2.next
# 关键步骤:断开原链表连接,防止链表环
temp = p.next # 暂存下一个节点
p.next = None # 切断当前节点与原链表的联系
p = temp # 移动到下一个节点
# 3. 合并两个子链表:small链表 + large链表
p1.next = dummy2.next # small尾部连接large头部
# 4. 返回结果:跳过虚拟头节点,返回真实头节点
return dummy1.next
五、关键细节解析(重要性评级)
1. 虚拟头节点(★★★★★)
- 本质:不存储有效数据的 "哨兵节点",作为链表操作的固定锚点。
- 典型场景:
- 构建新链表时,
- 处理头节点可能为空的情况,
- 统一头节点和普通节点的操作逻辑。
2. 双指针技巧(★★★★)
- p1/p2 指针:分别维护
small
和large
链表的尾部,实现 O (1) 时间复杂度的尾部追加。 - p 指针:遍历原链表,类似 "数据分拣员",决定节点归属。
3. 断开原链表(★★★★)
- 必须执行
p.next = None
:
若不切断原链表连接,节点会同时属于原链表和新链表,导致:- 链表环(节点的 next 仍指向原链表后续节点),
- 数据混乱(重复节点或顺序错误)。
- 类比:搬家时必须切断旧房子的水电,才能真正入住新房。
六、学习成果检验
问题 1:为什么用dummy2.next
连接链表?
解析:
p2
指针在循环中不断后移,最终指向large链表
的尾部(最后一个节点),dummy2
是虚拟头节点,dummy2.next
始终指向large链表
的真实头节点,- 因此
p1.next = dummy2.next
能正确连接large链表
的整个序列,而非仅尾部节点。
问题 2:省略p.next = None
会发生什么?
反例:
输入head = [1,4,2]
, x=3
,若不切断连接:
small链表
:1→2,large链表
:4,- 但
2.next
仍指向原链表的None
,4.next
仍指向原链表的2
, - 合并后链表为
1→2→4
,但4.next→2
形成环,遍历时会陷入死循环。
问题 3:虚拟头节点的核心价值是什么?
答案:
解决链表操作中的边界条件问题,避免处理 "空链表" 时的特殊逻辑,让代码对 "头节点" 和 "普通节点" 的操作逻辑统一,提升代码的简洁性和健壮性。
七、总结:从一道题到一类问题
通过本题,我们掌握了两个关键能力:
- 链表操作的核心思维:拆分复杂问题为 "遍历 - 处理 - 合并" 的标准流程,
- 虚拟头节点的万能技巧:这把 "瑞士军刀" 能解决 90% 的链表边界问题(如合并、删除、插入)。
记住:链表操作的本质是指针的舞蹈,而虚拟头节点就是这场舞蹈中最稳定的支点。多练习、多思考,你会发现更多链表问题的解法都藏在这个核心技巧中!
如果觉得有帮助,欢迎点赞收藏~ 你还想了解哪些算法技巧?评论区告诉我!