LeetCode第86题:分隔链表

74 阅读7分钟

LeetCode第86题:分隔链表

题目描述

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。

你应当 保留 两个分区中每个节点的初始相对顺序。

难度

中等

问题链接

分隔链表

示例

示例 1:

分隔链表示例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 的节点,并且要保持节点的相对顺序不变。

一个直观的思路是使用两个指针分别维护两个链表:

  1. 一个链表存储所有小于 x 的节点
  2. 另一个链表存储所有大于等于 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. 节点值为 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++ 提交

代码亮点

  1. 双指针技巧:使用两个指针分别维护两个链表,使得代码结构清晰。
  2. 虚拟头节点:使用虚拟头节点简化了链表操作,避免了对头节点的特殊处理。
  3. 一次遍历:只需要遍历原始链表一次,时间复杂度为 O(n)。
  4. 原地操作:不需要创建新的节点,只是重新组织原有节点的连接关系,空间复杂度为 O(1)。
  5. 防止环形链表:最后设置尾节点的 nextnull,避免形成环。

常见错误分析

  1. 忘记设置尾节点的 next 为 null:如果不设置尾节点的 nextnull,可能会形成环,导致无限循环。
  2. 错误地连接两个链表:如果连接两个链表的顺序错误,可能会导致结果不符合要求。
  3. 没有使用虚拟头节点:如果不使用虚拟头节点,需要对头节点进行特殊处理,容易出错。
  4. 混淆了节点的值和节点本身:在链表操作中,需要区分节点的值和节点本身,避免混淆。
  5. 内存泄漏:在 C++ 实现中,如果不释放虚拟头节点的内存,可能会导致内存泄漏。

解法比较

解法时间复杂度空间复杂度优点缺点
双指针法O(n)O(1)简单直观,一次遍历即可完成需要注意链表的连接操作
创建新链表O(n)O(n)实现简单,不需要修改原始链表需要额外的空间来创建新节点
插入排序O(n²)O(1)可以处理更复杂的排序要求时间复杂度高,不适合大规模数据

相关题目