力扣解题-86. 分隔链表
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
示例 1:
输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]
示例 2:
输入:head = [2,1], x = 2
输出:[1,2]
提示:
链表中节点的数目在范围 [0, 200] 内
-100 <= Node.val <= 100
-200 <= x <= 200
Related Topics
链表、双指针
第一次解答
解题思路
核心方法:双哑节点分治(新建节点版),通过两个哑节点分别构建“小于x的链表”和“大于等于x的链表”,遍历原链表后将两个链表拼接,逻辑直观且能保证节点相对顺序,但会创建新节点(额外空间开销)。
核心逻辑拆解
分隔链表的核心是“分类收集+顺序拼接”,且需保留原节点的相对位置:
- 双哑节点初始化:
- 创建
smallDummy(小值链表哑节点)和small指针(小值链表尾指针); - 创建
bigDummy(大值链表哑节点)和big指针(大值链表尾指针); - 两个哑节点分别管理两类节点,避免头节点的特殊处理;
- 创建
- 遍历分类收集:
- 定义
curr指针从head开始遍历原链表; - 对每个节点
curr:- 若
curr.val < x:创建值相同的新节点,接入小值链表(small.next = 新节点),small后移; - 否则:创建值相同的新节点,接入大值链表(
big.next = 新节点),big后移;
- 若
- 遍历完成后,小值链表包含所有小于x的节点,大值链表包含所有≥x的节点,且各自保留原相对顺序;
- 定义
- 拼接两个链表:
- 将小值链表的尾节点(
small)的next指向大值链表的真实头节点(bigDummy.next); - 将大值链表的尾节点(
big)的next置为null(避免链表成环);
- 将小值链表的尾节点(
- 返回结果:返回小值链表的真实头节点(
smallDummy.next)。
具体步骤(以示例1 head=[1,4,3,2,5,2]、x=3为例)
| 步骤 | curr值 | 操作 | 小值链表 | 大值链表 |
|---|---|---|---|---|
| 1 | 1 | 加入小值链表 | 0→1 | 0 |
| 2 | 4 | 加入大值链表 | 0→1 | 0→4 |
| 3 | 3 | 加入大值链表 | 0→1 | 0→4→3 |
| 4 | 2 | 加入小值链表 | 0→1→2 | 0→4→3 |
| 5 | 5 | 加入大值链表 | 0→1→2 | 0→4→3→5 |
| 6 | 2 | 加入小值链表 | 0→1→2→2 | 0→4→3→5 |
| 7 | 拼接 | small.next=bigDummy.next | 0→1→2→2→4→3→5 | - |
| 最终结果链表:1→2→2→4→3→5,与示例一致。 |
性能说明
- 时间复杂度:O(n)(仅一次线性遍历原链表,n为节点数);
- 空间复杂度:O(n)(创建了n个新节点存储结果,额外空间开销);
- 优势:
- 逻辑简单易懂,分类收集的思路符合直觉;
- 完美保留两个分区的节点初始相对位置;
- 双哑节点避免了头节点为空的边界问题;
- 可优化点:无需创建新节点,直接复用原链表节点可将空间复杂度降至O(1)。
public ListNode partition(ListNode head, int x) {
ListNode smallDummy=new ListNode(0);
ListNode small=smallDummy;
ListNode bigDummy=new ListNode(0);
ListNode big=bigDummy;
ListNode curr=head;
while (curr!=null){
if (curr.val<x){
small.next=new ListNode(curr.val);
small=small.next;
}else {
big.next = new ListNode(curr.val);
big = big.next;
}
curr=curr.next;
}
small.next=bigDummy.next;
big.next=null;
return smallDummy.next;
}
示例解答
解题思路
解法1:双哑节点优化版(复用原节点,O(1)额外空间)
核心方法:双哑节点+复用原节点,在第一次解答的分治思路基础上,直接复用原链表节点(不创建新节点),仅调整节点的next指针,额外空间复杂度优化为O(1),是工程中更优的实现方式。
代码实现
public ListNode partition(ListNode head, int x) {
// 小值链表哑节点
ListNode smallDummy = new ListNode(0);
ListNode small = smallDummy;
// 大值链表哑节点
ListNode bigDummy = new ListNode(0);
ListNode big = bigDummy;
ListNode curr = head;
while (curr != null) {
// 保存下一个节点,避免断链
ListNode next = curr.next;
// 断开当前节点与原链表的连接
curr.next = null;
if (curr.val < x) {
small.next = curr; // 直接复用原节点
small = small.next;
} else {
big.next = curr; // 直接复用原节点
big = big.next;
}
curr = next;
}
// 拼接两个链表
small.next = bigDummy.next;
return smallDummy.next;
}
优势说明
- 时间复杂度:O(n),与原解法一致;
- 额外空间复杂度:O(1)(仅使用两个哑节点和指针,无新节点创建);
- 核心优化点:
- 复用原链表节点,避免新节点创建的内存开销;
- 遍历过程中保存
curr.next并断开当前节点,防止原链表的指针干扰新链表; - 逻辑与原解法一致,仅节点处理方式优化,易理解易迁移。
解法2:单指针遍历拼接法(思路拓展)
核心方法:先找分界点再拼接,通过一次遍历找到第一个≥x的节点的前驱,将后续所有小于x的节点移动到分界点前,无需额外哑节点(仅需O(1)空间),但逻辑稍复杂。
代码实现
public ListNode partition(ListNode head, int x) {
if (head == null) {
return null;
}
// 哑节点处理头节点小于x的情况
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode pre = dummy;
// 第一步:找到第一个≥x的节点的前驱
while (pre.next != null && pre.next.val < x) {
pre = pre.next;
}
// 此时pre是最后一个小于x的节点,pre.next是第一个≥x的节点
ListNode curr = pre;
// 第二步:遍历后续节点,将小于x的节点移动到pre后
while (curr.next != null) {
if (curr.next.val < x) {
// 取出小于x的节点
ListNode moveNode = curr.next;
curr.next = moveNode.next;
// 插入到pre后
moveNode.next = pre.next;
pre.next = moveNode;
// pre后移
pre = pre.next;
} else {
curr = curr.next;
}
}
return dummy.next;
}
适用场景说明
- 时间复杂度:O(n)(一次遍历完成所有操作);
- 空间复杂度:O(1)(仅使用指针变量);
- 优势:无需创建新链表,仅通过指针调整完成分隔,空间效率最优;
- 局限性:逻辑稍复杂,需要精准处理节点的插入/移动,新手易出错。
总结
- 双哑节点新建节点版(第一次解答):O(n)时间+O(n)空间,逻辑直观、易实现,适合理解核心思路;
- 双哑节点复用原节点版(最优解):O(n)时间+O(1)额外空间,复用原节点+分治思路,工程首选;
- 单指针遍历拼接法:O(n)时间+O(1)空间,逻辑稍复杂但空间效率最优,适合进阶学习;
- 关键技巧:
- 核心思想:分治(分类收集)是解决链表分隔问题的通用思路,双哑节点是简化头节点处理的关键;
- 顺序保证:遍历原链表时按顺序收集节点,天然保留初始相对位置;
- 空间优化:优先复用原节点,避免创建新节点的额外开销。