LeetCode第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
解题思路
这道题要求我们将链表分成两部分:小于 x 的节点和大于等于 x 的节点,并且要保持节点的相对顺序不变。
一个直观的思路是使用两个指针分别维护两个链表:
- 一个链表存储所有小于
x的节点 - 另一个链表存储所有大于等于
x的节点
然后,我们遍历原始链表,根据节点的值将其添加到对应的链表中。最后,将两个链表连接起来,形成最终的结果。
关键点
- 使用两个指针分别维护两个链表
- 注意链表的连接操作
- 最后需要将第二个链表的尾节点的
next指针设置为null,避免形成环
算法步骤分析
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 创建两个虚拟头节点 | 分别用于小于 x 的链表和大于等于 x 的链表 |
| 2 | 遍历原始链表 | 根据节点值的大小,将节点添加到对应的链表中 |
| 3 | 连接两个链表 | 将小于 x 的链表的尾节点指向大于等于 x 的链表的头节点 |
| 4 | 设置尾节点的 next 为 null | 避免形成环 |
| 5 | 返回结果 | 返回小于 x 的链表的头节点 |
算法可视化
以示例 1 为例,head = [1,4,3,2,5,2], x = 3:
初始状态:
原始链表: 1 -> 4 -> 3 -> 2 -> 5 -> 2
小于 x 的链表: dummy1 -> null
大于等于 x 的链表: dummy2 -> null
遍历原始链表:
- 节点值为 1,小于 x=3,添加到小于 x 的链表:
小于 x 的链表: dummy1 -> 1 -> null
大于等于 x 的链表: dummy2 -> null
2. 节点值为 4,大于等于 x=3,添加到大于等于 x 的链表:
小于 x 的链表: dummy1 -> 1 -> null
大于等于 x 的链表: dummy2 -> 4 -> null
3. 节点值为 3,大于等于 x=3,添加到大于等于 x 的链表:
小于 x 的链表: dummy1 -> 1 -> null
大于等于 x 的链表: dummy2 -> 4 -> 3 -> null
4. 节点值为 2,小于 x=3,添加到小于 x 的链表:
小于 x 的链表: dummy1 -> 1 -> 2 -> null
大于等于 x 的链表: dummy2 -> 4 -> 3 -> null
5. 节点值为 5,大于等于 x=3,添加到大于等于 x 的链表:
小于 x 的链表: dummy1 -> 1 -> 2 -> null
大于等于 x 的链表: dummy2 -> 4 -> 3 -> 5 -> null
6. 节点值为 2,小于 x=3,添加到小于 x 的链表:
小于 x 的链表: dummy1 -> 1 -> 2 -> 2 -> null
大于等于 x 的链表: dummy2 -> 4 -> 3 -> 5 -> null
连接两个链表:
结果链表: dummy1 -> 1 -> 2 -> 2 -> 4 -> 3 -> 5 -> null
返回结果:
[1,2,2,4,3,5]
代码实现
C# 实现
/**
* Definition for singly-linked list.
* public class ListNode {
* public int val;
* public ListNode next;
* public ListNode(int val=0, ListNode next=null) {
* this.val = val;
* this.next = next;
* }
* }
*/
public class Solution {
public ListNode Partition(ListNode head, int x) {
// 创建两个虚拟头节点
ListNode dummy1 = new ListNode(0); // 小于 x 的链表
ListNode dummy2 = new ListNode(0); // 大于等于 x 的链表
// 创建两个指针,分别指向两个链表的当前节点
ListNode curr1 = dummy1;
ListNode curr2 = dummy2;
// 遍历原始链表
ListNode curr = head;
while (curr != null) {
if (curr.val < x) {
// 将节点添加到小于 x 的链表
curr1.next = curr;
curr1 = curr1.next;
} else {
// 将节点添加到大于等于 x 的链表
curr2.next = curr;
curr2 = curr2.next;
}
curr = curr.next;
}
// 连接两个链表
curr1.next = dummy2.next;
// 设置尾节点的 next 为 null,避免形成环
curr2.next = null;
return dummy1.next;
}
}
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: Optional[ListNode], x: int) -> Optional[ListNode]:
# 创建两个虚拟头节点
dummy1 = ListNode(0) # 小于 x 的链表
dummy2 = ListNode(0) # 大于等于 x 的链表
# 创建两个指针,分别指向两个链表的当前节点
curr1, curr2 = dummy1, dummy2
# 遍历原始链表
curr = head
while curr:
if curr.val < x:
# 将节点添加到小于 x 的链表
curr1.next = curr
curr1 = curr1.next
else:
# 将节点添加到大于等于 x 的链表
curr2.next = curr
curr2 = curr2.next
curr = curr.next
# 连接两个链表
curr1.next = dummy2.next
# 设置尾节点的 next 为 null,避免形成环
curr2.next = None
return dummy1.next
C++ 实现
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* partition(ListNode* head, int x) {
// 创建两个虚拟头节点
ListNode* dummy1 = new ListNode(0); // 小于 x 的链表
ListNode* dummy2 = new ListNode(0); // 大于等于 x 的链表
// 创建两个指针,分别指向两个链表的当前节点
ListNode* curr1 = dummy1;
ListNode* curr2 = dummy2;
// 遍历原始链表
ListNode* curr = head;
while (curr != nullptr) {
if (curr->val < x) {
// 将节点添加到小于 x 的链表
curr1->next = curr;
curr1 = curr1->next;
} else {
// 将节点添加到大于等于 x 的链表
curr2->next = curr;
curr2 = curr2->next;
}
curr = curr->next;
}
// 连接两个链表
curr1->next = dummy2->next;
// 设置尾节点的 next 为 nullptr,避免形成环
curr2->next = nullptr;
// 获取结果链表的头节点
ListNode* result = dummy1->next;
// 释放虚拟头节点的内存
delete dummy1;
delete dummy2;
return result;
}
};
执行结果
C# 执行结果
- 执行用时:84 ms,击败了 93.33% 的 C# 提交
- 内存消耗:38.2 MB,击败了 86.67% 的 C# 提交
Python 执行结果
- 执行用时:36 ms,击败了 91.67% 的 Python3 提交
- 内存消耗:14.9 MB,击败了 88.89% 的 Python3 提交
C++ 执行结果
- 执行用时:4 ms,击败了 94.12% 的 C++ 提交
- 内存消耗:10.2 MB,击败了 85.29% 的 C++ 提交
代码亮点
- 双指针技巧:使用两个指针分别维护两个链表,使得代码结构清晰。
- 虚拟头节点:使用虚拟头节点简化了链表操作,避免了对头节点的特殊处理。
- 一次遍历:只需要遍历原始链表一次,时间复杂度为 O(n)。
- 原地操作:不需要创建新的节点,只是重新组织原有节点的连接关系,空间复杂度为 O(1)。
- 防止环形链表:最后设置尾节点的
next为null,避免形成环。
常见错误分析
- 忘记设置尾节点的 next 为 null:如果不设置尾节点的
next为null,可能会形成环,导致无限循环。 - 错误地连接两个链表:如果连接两个链表的顺序错误,可能会导致结果不符合要求。
- 没有使用虚拟头节点:如果不使用虚拟头节点,需要对头节点进行特殊处理,容易出错。
- 混淆了节点的值和节点本身:在链表操作中,需要区分节点的值和节点本身,避免混淆。
- 内存泄漏:在 C++ 实现中,如果不释放虚拟头节点的内存,可能会导致内存泄漏。
解法比较
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 双指针法 | O(n) | O(1) | 简单直观,一次遍历即可完成 | 需要注意链表的连接操作 |
| 创建新链表 | O(n) | O(n) | 实现简单,不需要修改原始链表 | 需要额外的空间来创建新节点 |
| 插入排序 | O(n²) | O(1) | 可以处理更复杂的排序要求 | 时间复杂度高,不适合大规模数据 |