【算法--链表】86.分割链表--通俗讲解

49 阅读4分钟

一、题目是啥?一句话说清

给你一个链表和一个值 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之前,且相对顺序不变)

二、解题核心

使用两个临时链表:一个收集所有小于 x 的节点,另一个收集所有大于或等于 x 的节点。遍历原链表,将每个节点分配到对应的临时链表中,最后将两个临时链表连接起来。 这就像把一堆水果分成两筐:一筐放所有苹果,另一筐放所有橙子,然后把苹果筐放在橙子筐前面。

三、关键在哪里?(3个核心点)

想理解并解决这道题,必须抓住以下三个关键点:

1. 两个临时链表的使用

  • 是什么:创建两个临时链表,分别存储小于 x 的节点和大于等于 x 的节点。
  • 为什么重要:这样可以保持节点的相对顺序,因为节点被按顺序添加到对应的链表中。

2. 哑节点(Dummy Node)的运用

  • 是什么:为每个临时链表创建一个哑节点作为头节点,简化链表操作。
  • 为什么重要:哑节点可以避免处理空链表的边界情况,让代码更简洁。

3. 正确连接链表

  • 是什么:遍历完成后,将小于 x 的链表的末尾指向大于等于 x 的链表的头节点,并将大于等于 x 的链表的末尾指向 null。
  • 为什么重要:如果连接不正确,可能会导致链表断开或形成环。

四、看图理解流程(通俗理解版本)

让我们用链表 1 → 4 → 3 → 2 → 5 → 2 和 x=3 的例子来可视化过程:

  1. 初始化

    • 创建两个哑节点:leftDummy 和 rightDummy。
    • 初始化两个指针 left 和 right,分别指向 leftDummy 和 rightDummy。
    • 初始状态:leftDummy → null, rightDummy → null
  2. 遍历原链表

    • 节点1:值1 < 3,添加到 left 链表:leftDummy → 1
    • 节点4:值4 ≥ 3,添加到 right 链表:rightDummy → 4
    • 节点3:值3 ≥ 3,添加到 right 链表:rightDummy → 4 → 3
    • 节点2:值2 < 3,添加到 left 链表:leftDummy → 1 → 2
    • 节点5:值5 ≥ 3,添加到 right 链表:rightDummy → 4 → 3 → 5
    • 节点2:值2 < 3,添加到 left 链表:leftDummy → 1 → 2 → 2
  3. 连接链表

    • 将 left 链表的末尾(最后一个2)指向 right 链表的头节点(4)。
    • 将 right 链表的末尾(5)指向 null。
    • 最终链表:leftDummy → 1 → 2 → 2 → 4 → 3 → 5
  4. 返回结果:返回 leftDummy.next,即节点1。

五、C++ 代码实现(附详细注释)

#include <iostream>
using namespace std;

// 链表节点定义
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 leftDummy(0);  // 用于存储小于x的节点
        ListNode rightDummy(0); // 用于存储大于等于x的节点
        
        // 指针用于遍历和构建两个链表
        ListNode* left = &leftDummy;
        ListNode* right = &rightDummy;
        
        // 遍历原链表
        while (head != nullptr) {
            if (head->val < x) {
                // 将节点添加到left链表
                left->next = head;
                left = left->next;
            } else {
                // 将节点添加到right链表
                right->next = head;
                right = right->next;
            }
            head = head->next;
        }
        
        // 连接两个链表:left的末尾指向right的头节点
        left->next = rightDummy.next;
        // 将right的末尾指向null,避免循环
        right->next = nullptr;
        
        // 返回left链表的头节点
        return leftDummy.next;
    }
};

// 辅助函数:打印链表
void printList(ListNode* head) {
    while (head != nullptr) {
        cout << head->val << " ";
        head = head->next;
    }
    cout << endl;
}

// 测试代码
int main() {
    // 创建示例链表:1->4->3->2->5->2
    ListNode* head = new ListNode(1, new ListNode(4, new ListNode(3, new ListNode(2, new ListNode(5, new ListNode(2))))));
    
    Solution solution;
    ListNode* result = solution.partition(head, 3);
    
    printList(result); // 输出:1 2 2 4 3 5
    
    // 释放内存(实际面试中可能不需要完整释放)
    return 0;
}

六、注意事项

  • 内存管理:在C++中,我们不需要分配新节点,只是重新连接现有节点,所以不需要额外内存分配。
  • 指针操作:在遍历原链表时,需要移动 head 指针,确保不会丢失节点。
  • 连接链表:一定要将 right 链表的末尾指向 null,否则可能导致链表循环。
  • 相对顺序:由于是顺序遍历原链表并添加到对应链表,所以相对顺序自然保持。

七、总结

理解此题的关键在于:

  • 两个临时链表:分别收集小于 x 和大于等于 x 的节点,保持相对顺序。
  • 哑节点简化操作:避免处理空链表的边界情况。
  • 正确连接:确保两个链表连接后,末尾指向 null。

掌握这三点,你就能高效解决分割链表问题。这道题是链表操作的经典题目,考察了对链表遍历和指针操作的理解。多练习几次,注意细节,就能熟练运用。