力扣86题分隔链表:双链表拆解合并法详解

力扣86题分隔链表:双链表拆解合并法详解

💻零、 视频地址

因为想更好的为大佬服务,制作了同步视频,这是Bilibili的视频地址

前言

在链表的经典题型中,分隔链表是一道考察基础操作逻辑的经典题目,它看似简单,却能很好地检验我们对链表指针操作、节点拼接的掌握程度。本文将从题目解析出发,手把手教你用双虚拟头节点拆解合并法攻克这道题,结合原理分析、步骤图解和C++代码实现,让你彻底吃透链表分隔的核心逻辑~

📖 题目解析:读懂分隔链表的核心要求

力扣86题「分隔链表」的核心需求十分清晰:

给定一个单链表和一个数值x,请将链表划分为两个部分,所有小于 x 的节点排在链表前半部分,所有大于或等于 x 的节点排在链表后半部分

⚠️ 注意:划分后无需改变原链表中节点的相对顺序,仅完成区域分隔即可。

举个例子

若链表为1→4→3→2→5→2,给定x=3,则分隔后的链表应为1→2→2→4→3→5

其中小于3的节点:1、2、2;大于等于3的节点:4、3、5,严格遵循原链表中的相对顺序。

这道题的解题关键在于不额外开辟大量空间 ,仅通过指针操作完成节点的重新拼接,时间复杂度需控制在O(n)(仅遍历原链表一次),空间复杂度为O(1)(仅使用常数个指针)。

🧠 算法原理:双虚拟头节点的巧思

为什么选择双虚拟头节点

单链表的痛点在于头节点可能被修改 ,且对空链表、单节点链表的处理需要额外的边界判断。而虚拟头节点(哑节点) 是解决链表头节点问题的"万能钥匙",它是一个不存储实际值的节点,指向真正的头节点,能让我们的指针操作统一化,无需单独处理边界情况。

分隔链表的核心算法原理:

  1. 定义两个虚拟头节点 ,分别对应小值链表 (存储小于x的节点)和大值链表 (存储大于等于x的节点);

  2. 定义两个尾指针,分别指向小值链表和大值链表的末尾,用于快速拼接新节点;

  3. 遍历原链表,根据节点值的大小,将节点依次拼接到小值链表或大值链表的末尾;

  4. 遍历完成后,将小值链表的尾节点 指向大值链表的真实头节点,完成两个链表的合并;

  5. 最终返回小值链表的真实头节点,即为分隔后的结果链表。

📊 步骤图解:直观理解算法执行过程

为了让大家更清晰地看到每一步的操作,我们以链表1→4→3→2→5→2x=3为例,用图解展示双链表拆解合并的全过程(🔵代表小值链表,🟡代表大值链表)。

步骤1:初始化虚拟头节点和尾指针

  • 小值链表虚拟头节点:dummySmall,尾指针pSmall(初始指向dummySmall

  • 大值链表虚拟头节点:dummyLarge,尾指针pLarge(初始指向dummyLarge

  • 遍历指针p(初始指向原链表头节点)

Plain 复制代码
dummySmall → null  |  dummyLarge → null
   ↑                |      ↑
  pSmall            |    pLarge
原链表:1 → 4 → 3 → 2 → 5 → 2
        ↑
        p

步骤2:遍历原链表,拆分节点

  1. 节点1:小于3,拼接到小值链表末尾,pSmall后移至1p后移至4

  2. 节点4:大于3,拼接到大值链表末尾,pLarge后移至4p后移至3

  3. 节点3:等于3,拼接到大值链表末尾,pLarge后移至3p后移至2

  4. 节点2:小于3,拼接到小值链表末尾,pSmall后移至2p后移至5

  5. 节点5:大于3,拼接到大值链表末尾,pLarge后移至5p后移至2

  6. 节点2:小于3,拼接到小值链表末尾,pSmall后移至2p后移至null(遍历结束)。

拆分后结果:

Plain 复制代码
dummySmall → 1 → 2 → 2
                     ↑
                    pSmall
dummyLarge → 4 → 3 → 5
                     ↑
                    pLarge

步骤3:合并两个链表

将小值链表尾指针pSmallnext指向大值链表的真实头节点dummyLarge->next,同时将大值链表尾节点的next置为null(避免环)。

最终合并结果:

Plain 复制代码
dummySmall → 1 → 2 → 2 → 4 → 3 → 5 → null

步骤4:返回结果

返回dummySmall->next,即为分隔后的完整链表。

💻 C++代码实现:核心逻辑与细节讲解

结合上述原理,我们编写C++代码,仅保留关键核心代码,并对每一步进行详细注释,让你既能看懂逻辑,又能掌握代码书写的细节~

链表节点定义(力扣默认)

cpp 复制代码
// 单链表节点结构
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) {}
};

核心解题代码

cpp 复制代码
ListNode* partition(ListNode* head, int x) {
    // 1. 定义两个虚拟头节点,解决头节点边界问题
    ListNode* dummySmall = new ListNode(0); // 小值链表虚拟头
    ListNode* dummyLarge = new ListNode(0); // 大值链表虚拟头
    // 2. 定义尾指针,用于拼接新节点
    ListNode* pSmall = dummySmall;
    ListNode* pLarge = dummyLarge;
    // 3. 定义遍历指针,遍历原链表
    ListNode* p = head;

    while (p != nullptr) {
        // 先保存原链表的下一个节点,防止断链
        ListNode* temp = p->next;
        if (p->val < x) {
            // 节点值小于x,拼接到小值链表末尾
            pSmall->next = p;
            pSmall = pSmall->next; // 尾指针后移
        } else {
            // 节点值大于等于x,拼接到大值链表末尾
            pLarge->next = p;
            pLarge = pLarge->next; // 尾指针后移
        }
        p->next = nullptr; // 断开当前节点与原链表的连接,避免环
        p = temp; // 遍历指针后移
    }

    // 4. 合并两个链表:小值链表尾 → 大值链表真实头
    pSmall->next = dummyLarge->next;
    // 5. 保存结果头节点,释放虚拟头节点(避免内存泄漏)
    ListNode* res = dummySmall->next;
    delete dummySmall;
    delete dummyLarge;

    return res;
}

🔑 代码关键细节讲解

  1. 保存下一个节点 temp :遍历过程中,若直接将节点拼接到新链表,会丢失原链表的后续节点,因此必须先通过temp = p->next保存,确保遍历能继续;

  2. 断开节点原连接 p->next = nullptr :若不断开,原链表的节点连接会导致最终的结果链表出现,引发程序死循环;

  3. 释放虚拟头节点:C++中手动创建的节点需要手动释放,避免内存泄漏,这是工程开发中的良好习惯;

  4. 尾指针后移:每次拼接节点后,尾指针必须指向新的末尾,才能保证下一个节点能拼接到正确位置。

⚡ 算法性能分析

  • 时间复杂度O(n),其中n是原链表的节点个数。我们仅对原链表进行一次遍历,每个节点的操作都是常数时间的指针操作,无嵌套循环。

  • 空间复杂度O(1),仅使用了常数个指针变量 (虚拟头节点、尾指针、遍历指针),没有开辟额外的数组或链表空间,所有操作都是在原链表节点上完成的原地操作

这一性能是该题的最优解,因为要完成链表分隔,至少需要遍历一次所有节点,无法再降低时间复杂度。

✨ 解题总结:掌握链表解题的核心技巧

解完这道题,我们不仅掌握了分隔链表的具体方法,更能提炼出解决链表问题的通用技巧:

  1. 虚拟头节点是万能钥匙:遇到链表头节点可能被修改、边界情况复杂的问题,优先考虑使用虚拟头节点,让指针操作统一化;

  2. 尾指针优化拼接效率 :链表的末尾拼接若每次都从头遍历找尾,时间复杂度会升至O(n²),而尾指针能让拼接操作变为O(1)

  3. 防止断链和环:遍历链表时,若要移动节点,务必先保存下一个节点;拼接节点后,及时断开原连接,避免出现环;

  4. 原地操作优先 :链表问题尽量追求原地操作,减少额外空间的使用,提升算法效率。

📌 拓展思考

这道题的基础上,还可以延伸出类似的链表划分问题,比如:

  • 将链表按奇偶值分隔;

  • 将链表按给定值划分为三部分(小于、等于、大于)。

这些问题都可以用多虚拟头节点+多尾指针的思路解决,核心逻辑与分隔链表一致,只是多了一个判断分支和一个链表的合并步骤,大家可以尝试动手实现,巩固今天的知识点~

💡 其实链表的题目并不难,关键在于理清指针的指向关系 ,多画图解、多敲代码,就能慢慢形成对指针操作的直觉。希望这篇文章能让你对分隔链表和双虚拟头节点法有更深刻的理解,下次遇到同类题能轻松秒杀~

相关推荐
所谓伊人,在水一方3331 小时前
【Python数据科学实战之路】第6章 | 高级数据可视化:从统计洞察到交互叙事
开发语言·python·信息可视化
快快起来写代码1 小时前
【leetcode】容器中水的容量最小/大面积
算法·leetcode·职场和发展
愿天堂没有C++1 小时前
Pimpl 设计模式(指针指向实现)
开发语言·c++·设计模式
Nuopiane2 小时前
MyPal3(4)
java·开发语言
Fuliy962 小时前
第三阶段:进化与群体智能 (Evolutionary & Swarm Intelligence)
人工智能·笔记·python·学习·算法
kisshuan123962 小时前
[特殊字符] RollingDepth:单目视频深度估计算法解析
算法·音视频
gihigo19982 小时前
SSA奇异谱分解:时频域信号成分分析与重构
数据结构·算法·重构
深耕AI2 小时前
【 从零开始的VS Code Python环境配置:uv】
开发语言·python·uv
Takoony2 小时前
OpenClaw 深度拆解:下一代自主智能体架构全面解析
人工智能·深度学习·算法·机器学习·架构·openclaw