力扣86题分隔链表:双链表拆解合并法详解
- [💻零、 视频地址](#💻零、 视频地址)
- ✨**前言**✨
- [📖 题目解析:读懂分隔链表的核心要求](#📖 题目解析:读懂分隔链表的核心要求)
- [🧠 算法原理:双虚拟头节点的巧思](#🧠 算法原理:双虚拟头节点的巧思)
- [📊 步骤图解:直观理解算法执行过程](#📊 步骤图解:直观理解算法执行过程)
- [💻 C++代码实现:核心逻辑与细节讲解](#💻 C++代码实现:核心逻辑与细节讲解)
-
- 链表节点定义(力扣默认)
- 核心解题代码
- [🔑 代码关键细节讲解](#🔑 代码关键细节讲解)
- [⚡ 算法性能分析](#⚡ 算法性能分析)
- [✨ 解题总结:掌握链表解题的核心技巧](#✨ 解题总结:掌握链表解题的核心技巧)
- [📌 拓展思考](#📌 拓展思考)
💻零、 视频地址
因为想更好的为大佬服务,制作了同步视频,这是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)(仅使用常数个指针)。
🧠 算法原理:双虚拟头节点的巧思
为什么选择双虚拟头节点?
单链表的痛点在于头节点可能被修改 ,且对空链表、单节点链表的处理需要额外的边界判断。而虚拟头节点(哑节点) 是解决链表头节点问题的"万能钥匙",它是一个不存储实际值的节点,指向真正的头节点,能让我们的指针操作统一化,无需单独处理边界情况。
分隔链表的核心算法原理:
-
定义两个虚拟头节点 ,分别对应小值链表 (存储小于
x的节点)和大值链表 (存储大于等于x的节点); -
定义两个尾指针,分别指向小值链表和大值链表的末尾,用于快速拼接新节点;
-
遍历原链表,根据节点值的大小,将节点依次拼接到小值链表或大值链表的末尾;
-
遍历完成后,将小值链表的尾节点 指向大值链表的真实头节点,完成两个链表的合并;
-
最终返回小值链表的真实头节点,即为分隔后的结果链表。
📊 步骤图解:直观理解算法执行过程
为了让大家更清晰地看到每一步的操作,我们以链表1→4→3→2→5→2、x=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:小于3,拼接到小值链表末尾,pSmall后移至1,p后移至4; -
节点
4:大于3,拼接到大值链表末尾,pLarge后移至4,p后移至3; -
节点
3:等于3,拼接到大值链表末尾,pLarge后移至3,p后移至2; -
节点
2:小于3,拼接到小值链表末尾,pSmall后移至2,p后移至5; -
节点
5:大于3,拼接到大值链表末尾,pLarge后移至5,p后移至2; -
节点
2:小于3,拼接到小值链表末尾,pSmall后移至2,p后移至null(遍历结束)。
拆分后结果:
Plain
dummySmall → 1 → 2 → 2
↑
pSmall
dummyLarge → 4 → 3 → 5
↑
pLarge
步骤3:合并两个链表
将小值链表尾指针pSmall的next指向大值链表的真实头节点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;
}
🔑 代码关键细节讲解
-
保存下一个节点
temp:遍历过程中,若直接将节点拼接到新链表,会丢失原链表的后续节点,因此必须先通过temp = p->next保存,确保遍历能继续; -
断开节点原连接
p->next = nullptr:若不断开,原链表的节点连接会导致最终的结果链表出现环,引发程序死循环; -
释放虚拟头节点:C++中手动创建的节点需要手动释放,避免内存泄漏,这是工程开发中的良好习惯;
-
尾指针后移:每次拼接节点后,尾指针必须指向新的末尾,才能保证下一个节点能拼接到正确位置。
⚡ 算法性能分析
-
时间复杂度 :
O(n),其中n是原链表的节点个数。我们仅对原链表进行一次遍历,每个节点的操作都是常数时间的指针操作,无嵌套循环。 -
空间复杂度 :
O(1),仅使用了常数个指针变量 (虚拟头节点、尾指针、遍历指针),没有开辟额外的数组或链表空间,所有操作都是在原链表节点上完成的原地操作。
这一性能是该题的最优解,因为要完成链表分隔,至少需要遍历一次所有节点,无法再降低时间复杂度。
✨ 解题总结:掌握链表解题的核心技巧
解完这道题,我们不仅掌握了分隔链表的具体方法,更能提炼出解决链表问题的通用技巧:
-
虚拟头节点是万能钥匙:遇到链表头节点可能被修改、边界情况复杂的问题,优先考虑使用虚拟头节点,让指针操作统一化;
-
尾指针优化拼接效率 :链表的末尾拼接若每次都从头遍历找尾,时间复杂度会升至
O(n²),而尾指针能让拼接操作变为O(1); -
防止断链和环:遍历链表时,若要移动节点,务必先保存下一个节点;拼接节点后,及时断开原连接,避免出现环;
-
原地操作优先 :链表问题尽量追求原地操作,减少额外空间的使用,提升算法效率。
📌 拓展思考
这道题的基础上,还可以延伸出类似的链表划分问题,比如:
-
将链表按奇偶值分隔;
-
将链表按给定值划分为三部分(小于、等于、大于)。
这些问题都可以用多虚拟头节点+多尾指针的思路解决,核心逻辑与分隔链表一致,只是多了一个判断分支和一个链表的合并步骤,大家可以尝试动手实现,巩固今天的知识点~
💡 其实链表的题目并不难,关键在于理清指针的指向关系 ,多画图解、多敲代码,就能慢慢形成对指针操作的直觉。希望这篇文章能让你对分隔链表和双虚拟头节点法有更深刻的理解,下次遇到同类题能轻松秒杀~
