面试题 02.04. 分割链表 - 题解与详细分析

题目描述

给你一个链表的头节点 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 的节点,采用尾插法插入到新链表尾部

  • 这样就能保证所有小于 x 的节点都在大于等于 x 的节点之前

代码实现

复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* partition(struct ListNode* head, int x) {
    struct ListNode* cur = head;
    struct ListNode* newhead = NULL;
    struct ListNode* newtail = NULL;
    struct ListNode* temp = NULL;
    
    while(cur)
    {
        temp = cur->next;  // 保存下一个节点

        if(cur->val < x)
        {
            // 小于x的节点:头插法
            if(newhead == NULL)
            {
                newhead = newtail = cur;
                newhead->next = NULL;
            }
            else
            {
                cur->next = newhead;
                newhead = cur;
            }
        }
        else
        {
            // 大于等于x的节点:尾插法
            if(newhead == NULL)
            {
                newhead = newtail = cur;
                newhead->next = NULL;
            }
            else
            {
                newtail->next = cur;
                newtail = cur;
                newtail->next = NULL;
            }
        }

        cur = temp;  // 移动到下一个节点
    }

    return newhead;
}

代码详解

变量说明

  • cur:当前遍历的节点

  • newhead:新链表的头节点

  • newtail:新链表的尾节点

  • temp:临时保存下一个节点

核心逻辑

复制代码
while(cur)
{
    temp = cur->next;  // 保存下一个节点

    if(cur->val < x)
    {
        // 头插法:将节点插入到链表头部
        if(newhead == NULL)
        {
            newhead = newtail = cur;
            newhead->next = NULL;
        }
        else
        {
            cur->next = newhead;
            newhead = cur;
        }
    }
    else
    {
        // 尾插法:将节点插入到链表尾部
        if(newhead == NULL)
        {
            newhead = newtail = cur;
            newhead->next = NULL;
        }
        else
        {
            newtail->next = cur;
            newtail = cur;
            newtail->next = NULL;
        }
    }

    cur = temp;  // 移动到下一个节点
}

执行过程可视化

以示例1 head = [1,4,3,2,5,2], x = 3 为例:

复制代码
初始状态:
链表: 1->4->3->2->5->2->NULL
newhead = NULL, newtail = NULL

第1步:节点1 (值1 < 3)
头插法:newhead -> 1 -> NULL, newtail -> 1

第2步:节点4 (值4 >= 3)
尾插法:newhead -> 1 -> 4 -> NULL, newtail -> 4

第3步:节点3 (值3 >= 3)
尾插法:newhead -> 1 -> 4 -> 3 -> NULL, newtail -> 3

第4步:节点2 (值2 < 3)
头插法:newhead -> 2 -> 1 -> 4 -> 3 -> NULL, newtail -> 3

第5步:节点5 (值5 >= 3)
尾插法:newhead -> 2 -> 1 -> 4 -> 3 -> 5 -> NULL, newtail -> 5

第6步:节点2 (值2 < 3)
头插法:newhead -> 2 -> 2 -> 1 -> 4 -> 3 -> 5 -> NULL, newtail -> 5

最终结果:2->2->1->4->3->5->NULL

优化与改进

方法二:双链表法(保持相对顺序)

如果我们希望保持每个分区内节点的相对顺序,可以使用两个链表:

复制代码
struct ListNode* partition(struct ListNode* head, int x) {
    // 创建两个哑节点
    struct ListNode less_dummy, ge_dummy;
    struct ListNode* less_tail = &less_dummy;
    struct ListNode* ge_tail = &ge_dummy;
    less_dummy.next = NULL;
    ge_dummy.next = NULL;
    
    struct ListNode* cur = head;
    while (cur != NULL) {
        if (cur->val < x) {
            // 添加到小于x的链表
            less_tail->next = cur;
            less_tail = cur;
        } else {
            // 添加到大于等于x的链表
            ge_tail->next = cur;
            ge_tail = cur;
        }
        cur = cur->next;
    }
    
    // 连接两个链表
    less_tail->next = ge_dummy.next;
    ge_tail->next = NULL;
    
    return less_dummy.next;
}

输出结果: [1,2,2,4,3,5](保持相对顺序)

方法三:原地分割

复制代码
struct ListNode* partition(struct ListNode* head, int x) {
    struct ListNode* cur = head;
    struct ListNode* prev = NULL;
    struct ListNode* insert_pos = NULL;
    
    while (cur != NULL) {
        if (cur->val < x) {
            if (insert_pos != NULL) {
                // 将当前节点移动到insert_pos之后
                if (prev != NULL) {
                    prev->next = cur->next;
                }
                struct ListNode* temp = insert_pos->next;
                insert_pos->next = cur;
                cur->next = temp;
                insert_pos = cur;
                cur = prev->next;
            } else {
                insert_pos = cur;
                prev = cur;
                cur = cur->next;
            }
        } else {
            prev = cur;
            cur = cur->next;
        }
    }
    
    return head;
}

复杂度分析

原方法

  • 时间复杂度:O(n),只需遍历链表一次

  • 空间复杂度:O(1),只使用常数级别的额外空间

双链表法

  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

关键点总结

  1. 头插法与尾插法的结合:小于x的节点用头插法,大于等于x的节点用尾插法

  2. 边界处理:注意处理空链表和单节点链表的情况

  3. 指针操作:在修改指针前保存下一个节点,避免链表断裂

  4. 尾节点处理:确保尾节点的next为NULL,避免形成环

应用场景

这种链表分割技巧在以下场景中有应用:

  1. 快速排序的链表版本:分割操作是快速排序的核心

  2. 数据过滤:根据条件将数据分成两部分

  3. 负载均衡:将任务根据优先级分配到不同队列

总结

这道题考察了链表的基本操作和双指针技巧:

  1. 核心思路:通过头插法和尾插法的组合实现链表分割

  2. 灵活性:题目不要求保持相对顺序,给了我们更多的操作空间

  3. 指针操作:熟练掌握链表的插入、删除操作是关键

  4. 边界情况:注意处理空链表、单节点链表等特殊情况

掌握这种链表分割技巧对于理解更复杂的链表算法(如链表排序)很有帮助。

相关推荐
zh_xuan1 小时前
kotlin Flow的用法
android·开发语言·kotlin·协程·flow
Mr YiRan5 小时前
C++面向对象继承与操作符重载
开发语言·c++·算法
一只鹿鹿鹿8 小时前
智慧水利一体化建设方案
大数据·运维·开发语言·数据库·物联网
CoderJia程序员甲8 小时前
GitHub 热榜项目 - 日榜(2026-02-22)
人工智能·ai·大模型·github·ai教程
CoderJia程序员甲9 小时前
GitHub 热榜项目 - 日榜(2026-02-21)
ai·大模型·llm·github·ai教程
没有医保李先生9 小时前
字节对齐的总结
java·开发语言
蚊子码农9 小时前
算法题解记录--239滑动窗口最大值
数据结构·算法
Elastic 中国社区官方博客10 小时前
使用 Elastic 进行网络监控:统一网络可观测性
大数据·开发语言·网络·人工智能·elasticsearch·搜索引擎·全文检索
Codefengfeng10 小时前
Python Base环境中加包的方法
开发语言·python