
🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在算法与数据结构的世界里,链表是一座连接理论与工程的关键桥梁,更是算法面试中考察频率极高的"黄金考点".它以非连续的内存存储特性,与数组形成鲜明分野------数组依赖连续空间实现随机访问,而链表通过指针/引用串联节点,在动态增删场景中展现出无可替代的灵活性.然而,许多学习者在链表学习与实战中常陷入两大困境:一是指针操作混乱,节点断裂、内存泄漏等问题频发;二是边界条件特判繁琐,头节点处理与空指针异常成为解题"拦路虎".这些问题的本质,在于对链表核心操作的底层逻辑理解不深,缺乏系统化的技巧拆解与实战演练.本文聚焦链表核心操作技巧,以实战剖析为核心,从原理深度解析与代码落地实现双维度展开.我们将逐一拆解哨兵节点(Dummy Node)的统一化思维、快慢指针的高效遍历策略、三指针反转的经典迭代逻辑,以及头插/尾插、指定位置增删、环检测等高频操作的底层实现.结合C++语言特性与LeetCode经典题型,我们不仅会给出可直接复用的代码模板,更会深入剖析每一步操作的设计思路与易错点,帮助你彻底打通指针逻辑,构建从原理到实战的完整知识闭环.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!
目录
1.链表算法思想背景介绍
链表的实现原理
链表是线性表的链式存储结构 ,其核心实现原理是:不依赖连续的内存空间存储数据,而是将数据封装为独立的节点,通过指针/引用将离散的节点串联成线性序列 ,所有增删改查操作均通过调整节点间的指针指向完成.它彻底解决了数组连续内存、扩容困难、插入删除效率低的痛点,是动态数据存储的核心实现方式.
1️⃣核心结构:链表的最小单元------节点
链表的所有实现都基于节点(Node),这是不可拆分的基本存储单元,每个节点固定包含两部分:
- 数据域(Data):存储实际需要保存的数据(如数字、字符串、对象等);
- 指针域(Next) :存储下一个节点的内存地址,用于建立节点之间的关联.
以最基础的单链表为例,节点的逻辑结构如下:
+----------------+
| 数据域 | 指针域 |
| Data | Next | ---> 指向【下一个节点】
+----------------+
2️⃣整体存储原理
- 内存分配:非连续、离散化
数组需要预先申请一整块连续内存,而链表的节点可以散落在内存的任意位置,无需连续空间,系统会按需为每个节点分配内存. - 关联方式:指针寻址
节点之间没有物理上的相邻关系,完全依靠指针域记录的地址互相连接.就像一串钥匙扣,每个钥匙(节点)不挨着,但通过挂环(指针)串在一起. - 链表入口:头指针(Head)
链表必须有一个头指针,它不存储数据,仅指向链表的第一个节点;
- 空链表:头指针直接指向
NULL(空地址); - 非空链表:从头指针出发,依次跳转指针,就能遍历所有节点.
- 链表结尾:尾节点
最后一个节点的指针域不指向任何节点,固定赋值为NULL,作为遍历结束的标志.
单链表完整逻辑结构:
头指针 -> 节点1 -> 节点2 -> 节点3 -> ... -> 尾节点 -> NULL
3️⃣核心操作的实现原理(单链表)
链表的所有操作不移动数据本身 ,仅修改指针指向,这是它与数组最本质的区别:
1.遍历查找
- 原理:从头指针开始,用临时指针依次跳转,直到遇到
NULL停止; - 特点:只能顺序查找 ,无法像数组一样随机访问(不能直接找第n个节点),时间复杂度 O ( n ) O(n) O(n).
2.节点插入(以中间插入为例)
- 创建新节点;
- 让新节点的指针指向插入位置的后继节点;
- 让插入位置的前驱节点指针指向新节点;
- 特点:仅需修改2个指针,无需移动其他节点,时间复杂度 O ( 1 ) O(1) O(1)(忽略查找位置的耗时).
3.节点删除
- 找到待删除节点的前驱节点;
- 修改前驱节点的指针,直接指向待删除节点的后继节点(跳过待删除节点);
- 释放待删除节点的内存;
- 特点:断开指针关联即可,无数据迁移,效率极高.
4️⃣拓展链表的实现原理差异
在基础单链表之上,衍生出常用链表结构,原理仅在指针域上做扩展:
- 双向链表
节点拥有两个指针:prev(指向前驱节点)+next(指向后继节点),支持向前+向后双向遍历,解决了单链表无法回溯的问题. - 循环链表
尾节点的指针不指向NULL,而是重新指向头节点,形成闭环,适合环形数据场景(如轮询调度).
2.两数相加(OJ题)

算法思路:解法(模拟两数相加的过程即可):
两个链表都是逆序存储数字的,即两个链表的个位数、⼗位数等都已经对应,可以直接相加.在相加过程中,我们要注意是否产⽣进位,产⽣进位时需要将进位和链表数字⼀同相加.如果产⽣进位的位置在链表尾部,即答案位数⽐原链表位数⻓⼀位,还需要再new⼀个结点储存最⾼位.


核心代码
cpp
class Solution
{
public:
//函数功能:接收两个链表l1、l2,返回相加后的新链表
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2)
{
//1.定义遍历指针:不修改原链表头节点,用临时指针遍历
ListNode *cur1 = l1, *cur2 = l2;
//2.虚拟头节点(哨兵节点):链表操作核心技巧!
// 作用:统一处理所有节点的插入逻辑,不用单独判断结果链表为空的情况
ListNode* newhead = new ListNode(0);
//3.尾指针:始终指向结果链表的最后一个节点,方便尾插新节点
ListNode* prev = newhead;
//4.进位变量:存储「当前位的和 + 上一位的进位」,初始进位为0
int t = 0;
//5.核心循环:三大终止条件(任意一个满足就继续循环)
//cur1没遍历完 || cur2没遍历完 || 还有进位未处理
while (cur1 || cur2 || t)
{
//累加链表1当前位的值(如果节点存在)
if (cur1)
{
t += cur1->val; //把当前数字加到总和里
cur1 = cur1->next; //指针后移
}
//累加链表2当前位的值(如果节点存在)
if (cur2)
{
t += cur2->val;
cur2 = cur2->next;
}
//6.构造结果节点:当前位的结果 = 总和 % 10(取个位数)
prev->next = new ListNode(t % 10);
prev = prev->next; //尾指针后移,指向新的末尾
//7.更新进位:总和 / 10(整数除法,取十位数,0或1)
t /= 10;
}
//8.内存管理:虚拟头节点是临时创建的,释放内存避免泄漏
prev = newhead->next; // 真正的结果链表头节点
delete newhead;
return prev; // 返回结果链表
}
};
完整测试代码
cpp
#include <iostream>
using namespace std;
//1.定义链表节点结构体
struct ListNode
{
int val; //节点存储的值
ListNode *next; //指向下一个节点的指针
//构造函数:初始化节点
ListNode(int x) : val(x), next(NULL) {}
};
class Solution
{
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2)
{
ListNode *cur1 = l1, *cur2 = l2;
ListNode* newhead = new ListNode(0);
ListNode* prev = newhead;
int t = 0;
while (cur1 || cur2 || t)
{
if (cur1)
{
t += cur1->val;
cur1 = cur1->next;
}
if (cur2)
{
t += cur2->val;
cur2 = cur2->next;
}
prev->next = new ListNode(t % 10);
prev = prev->next;
t /= 10;
}
prev = newhead->next;
delete newhead;
return prev;
}
};
//3.根据数组创建链表(尾插法)
ListNode* createList(int arr[], int n)
{
if (n == 0) return NULL;
ListNode* head = new ListNode(arr[0]);
ListNode* cur = head;
for (int i = 1; i < n; i++)
{
cur->next = new ListNode(arr[i]);
cur = cur->next;
}
return head;
}
//4.打印链表
void printList(ListNode* head)
{
ListNode* cur = head;
while (cur)
{
cout << cur->val;
if (cur->next) cout << " -> ";
cur = cur->next;
}
cout << endl;
}
int main()
{
Solution sol;
// 测试用例 1:常规场景
// l1 = 2 -> 4 -> 3 (数字 342)
// l2 = 5 -> 6 -> 4 (数字 465)
// 结果:7 -> 0 -> 8 (数字 807)
int a1[] = {2, 4, 3};
int a2[] = {5, 6, 4};
ListNode* l1 = createList(a1, 3);
ListNode* l2 = createList(a2, 3);
cout << "测试用例1:" << endl;
cout << "链表1:"; printList(l1);
cout << "链表2:"; printList(l2);
ListNode* res1 = sol.addTwoNumbers(l1, l2);
cout << "相加结果:"; printList(res1);
cout << "-------------------------" << endl;
// 测试用例 2:最高位进位
// l1 = 9 -> 9 -> 9 (数字 999)
// l2 = 9 -> 9 -> 9 (数字 999)
// 结果:8 -> 9 -> 9 -> 1 (数字 1998)
int a3[] = {9,9,9};
int a4[] = {9,9,9};
ListNode* l3 = createList(a3, 3);
ListNode* l4 = createList(a4, 3);
cout << "测试用例2:" << endl;
cout << "链表1:"; printList(l3);
cout << "链表2:"; printList(l4);
ListNode* res2 = sol.addTwoNumbers(l3, l4);
cout << "相加结果:"; printList(res2);
cout << "-------------------------" << endl;
// 测试用例 3:零值场景
// l1 = 0 (数字 0)
// l2 = 0 (数字 0)
// 结果:0
int a5[] = {0};
int a6[] = {0};
ListNode* l5 = createList(a5, 1);
ListNode* l6 = createList(a6, 1);
cout << "测试用例3:" << endl;
cout << "链表1:"; printList(l5);
cout << "链表2:"; printList(l6);
ListNode* res3 = sol.addTwoNumbers(l5, l6);
cout << "相加结果:"; printList(res3);
return 0;
}

3.两两交换链表中的节点(OJ题)

算法思路:解法(模拟):
- 先处理特殊边界:链表为空/只有一个节点时,无需交换,直接返回原链表;
- 创建虚拟头节点:因为链表的头节点会参与交换,用虚拟头节点统一所有节点的操作逻辑,避免单独处理头节点;
- 多指针定位:定义4个指针,分别记录前驱节点、当前交换节点1、当前交换节点2、下一组交换节点,防止交换时链表断裂;
- 循环交换:只要当前组有两个节点,就执行指针交换操作,完成后将所有指针后移,处理下一组节点;
- 清理并返回:删除虚拟头节点,返回交换后的链表头.
- 4个指针分别锁定四个关键位置,交换时不会丢失链表节点的指向.
cpp
ListNode *prev = newHead, //前驱节点:指向当前交换组的前一个节点
*cur = prev->next,//当前节点:待交换的第一个节点
*next = cur->next,//后继节点:待交换的第二个节点
*nnext = next->next;//下下节点:下一组待交换的第一个节点
核心代码
cpp
class Solution
{
public:
//函数功能:输入链表头节点head,返回两两交换后的新链表头节点
ListNode* swapPairs(ListNode* head)
{
//1. 边界条件处理
//如果链表为空,或者只有1个节点:无法两两交换,直接返回原链表
if (head == nullptr || head->next == nullptr)
return head;
//2. 创建虚拟头节点(哨兵节点)
//作用:解决【头节点会被交换】的问题,统一所有节点的交换逻辑,不用单独处理头节点
ListNode* newHead = new ListNode(0);
newHead->next = head; //让虚拟头节点指向原链表的第一个节点
//3. 初始化4个关键指针
// prev:指向待交换节点的**前一个节点**(初始是虚拟头节点)
// cur:待交换的第一个节点
// next:待交换的第二个节点
// nnext:下一组待交换的第一个节点(防止交换后链表断裂)
ListNode *prev = newHead, *cur = prev->next, *next = cur->next, *nnext = next->next;
//4. 核心循环:只要有两个节点就交换
//循环条件:cur和next都存在,说明有两个节点可以交换
while (cur && next)
{
//核心:交换两个节点
//步骤1:前驱节点指向第二个节点(交换后,第二个节点变成组内第一个)
prev->next = next;
//步骤2:第二个节点指向第一个节点(完成交换)
next->next = cur;
//步骤3:第一个节点指向下一组的第一个节点(衔接后续链表,防止断裂)
cur->next = nnext;
//指针后移:准备交换下一组节点
prev = cur; //前驱指针移动到【交换后的尾节点】,为下一次交换做准备
cur = nnext; //当前指针移动到下一组的第一个节点
//如果cur存在,更新next为下一个节点
if (cur)
next = cur->next;
//如果next存在,更新nnext为下下个节点
if (next)
nnext = next->next;
}
//5. 清理内存 + 返回结果
cur = newHead->next; //虚拟头节点的next就是交换后链表的**真实头节点**
delete newHead; //释放虚拟头节点,避免内存泄漏
return cur; //返回新链表
}
};
完整测试代码
cpp
#include <iostream>
using namespace std;
//1. 链表节点结构体定义
struct ListNode
{
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution
{
public:
//函数功能:输入链表头节点head,返回两两交换后的新链表头节点
ListNode* swapPairs(ListNode* head)
{
//1.边界条件处理
//如果链表为空,或者只有1个节点:无法两两交换,直接返回原链表
if (head == nullptr || head->next == nullptr)
return head;
//2.创建虚拟头节点(哨兵节点)
//作用:解决【头节点会被交换】的问题,统一所有节点的交换逻辑,不用单独处理头节点
ListNode* newHead = new ListNode(0);
newHead->next = head; // 让虚拟头节点指向原链表的第一个节点
//3.初始化4个关键指针
//prev:指向待交换节点的**前一个节点**(初始是虚拟头节点)
//cur:待交换的第一个节点
//next:待交换的第二个节点
//nnext:下一组待交换的第一个节点(防止交换后链表断裂)
ListNode *prev = newHead, *cur = prev->next, *next = cur->next, *nnext = next->next;
//4 核心循环:只要有两个节点就交换
//循环条件:cur和next都存在,说明有两个节点可以交换
while (cur && next)
{
//步骤1:前驱节点指向第二个节点(交换后,第二个节点变成组内第一个)
prev->next = next;
//步骤2:第二个节点指向第一个节点(完成交换)
next->next = cur;
//步骤3:第一个节点指向下一组的第一个节点(衔接后续链表,防止断裂)
cur->next = nnext;
prev = cur; //前驱指针移动到【交换后的尾节点】,为下一次交换做准备
cur = nnext; //当前指针移动到下一组的第一个节点
//如果cur存在,更新next为下一个节点
if (cur)
next = cur->next;
//如果next存在,更新nnext为下下个节点
if (next)
nnext = next->next;
}
//5. 清理内存 + 返回结果
cur = newHead->next; //虚拟头节点的next就是交换后链表的**真实头节点**
delete newHead; //释放虚拟头节点,避免内存泄漏
return cur; //返回新链表
}
};
//根据数组创建链表(尾插法)
ListNode* createList(int arr[], int n)
{
if (n == 0) return NULL;
ListNode* head = new ListNode(arr[0]);
ListNode* cur = head;
for (int i = 1; i < n; i++)
{
cur->next = new ListNode(arr[i]);
cur = cur->next;
}
return head;
}
//打印链表
void printList(ListNode* head)
{
ListNode* cur = head;
while (cur)
{
cout << cur->val;
if (cur->next) cout << " -> ";
cur = cur->next;
}
cout << endl;
}
int main()
{
Solution sol;
//测试用例1:偶数个节点(标准场景)
//输入:1 -> 2 -> 3 -> 4
//输出:2 -> 1 -> 4 -> 3
int a1[] = {1,2,3,4};
ListNode* l1 = createList(a1, 4);
cout << "测试用例1(偶数节点):" << endl;
cout << "原链表:"; printList(l1);
ListNode* res1 = sol.swapPairs(l1);
cout << "交换后:"; printList(res1);
cout << "-------------------------" << endl;
//测试用例2:奇数个节点
//输入:1 -> 2 -> 3
//输出:2 -> 1 -> 3
int a2[] = {1,2,3};
ListNode* l2 = createList(a2, 3);
cout << "测试用例2(奇数节点):" << endl;
cout << "原链表:"; printList(l2);
ListNode* res2 = sol.swapPairs(l2);
cout << "交换后:"; printList(res2);
cout << "-------------------------" << endl;
//测试用例3:空链表
cout << "测试用例3(空链表):" << endl;
ListNode* l3 = nullptr;
cout << "原链表:空链表" << endl;
ListNode* res3 = sol.swapPairs(l3);
cout << "交换后:"; printList(res3);
cout << "-------------------------" << endl;
//测试用例4:单个节点
int a4[] = {5};
ListNode* l4 = createList(a4, 1);
cout << "测试用例4(单个节点):" << endl;
cout << "原链表:"; printList(l4);
ListNode* res4 = sol.swapPairs(l4);
cout << "交换后:"; printList(res4);
return 0;
}

4.重排链表(OJ题)

算法思路:解法(模拟)
步骤1:边界条件预处理
如果链表为空、只有1个节点、只有2个节点;无法/无需按照规则重排,直接退出函数,避免无效操作.
步骤2:快慢双指针寻找链表中间节点
- 定义两个指针:
slow(慢指针,每次走1步)、fast(快指针,每次走2步); - 遍历结束时,
fast到达链表末尾,slow恰好指向链表中点; - 关键操作 :找到中点后,执行
slow->next = nullptr,将链表从中间断开 ,拆分为前半段 和后半段 两个独立链表,防止后续操作混乱.- 示例:
1→2→3→4→5断开后 → 前半段1→2→3,后半段4→5。
- 示例:
步骤3:头插法反转后半段链表
- 创建虚拟头节点,用头插法将后半段链表反转;
- 作用:原后半段是正序,反转后变为倒序,刚好匹配重排需求(前正序+后倒序 = 目标顺序);
- 示例:后半段
4→5反转后 →5→4.
- 示例:后半段
步骤4:双指针交替合并两个链表
- 用两个指针分别遍历前半段 和反转后的后半段;
- 合并规则:先取前半段1个节点,再取后半段1个节点,循环交替拼接;
- 处理奇偶长度:后半段节点数可能少于前半段,合并时判断后半段节点是否存在,避免空指针;
- 示例:前半段
1→2→3+ 反转后半段5→4→ 合并为1→5→2→4→3.
- 示例:前半段
步骤5:内存清理
释放代码中创建的虚拟头节点,避免内存泄漏.
核心代码
cpp
class Solution
{
public:
void reorderList(ListNode* head)
{
//处理边界情况
if (head == nullptr || head->next == nullptr || head->next->next == nullptr)
return;
//1.找到链表的中间节点 - 快慢双指针(⼀定要考虑slow的落点在哪⾥)
ListNode *slow = head, *fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
//2.把slow后⾯的部分给逆序 - 头插法
ListNode* head2 = new ListNode(0);
ListNode* cur = slow->next;
slow->next = nullptr; //注意把两个链表给断开
while (cur) {
ListNode* next = cur->next;
cur->next = head2->next;
head2->next = cur;
cur = next;
}
//3.合并两个链表 - 双指针
ListNode* ret = new ListNode(0);
ListNode* prev = ret;
ListNode *cur1 = head, *cur2 = head2->next;
while (cur1)
{
//先放第⼀个链表
prev->next = cur1;
cur1 = cur1->next;
prev = prev->next;
//再放第⼆个链表
if (cur2)
{
prev->next = cur2;
prev = prev->next;
cur2 = cur2->next;
}
}
delete head2;
delete ret;
}
};
完整测试代码
cpp
#include <iostream>
using namespace std;
//链表节点结构体定义
struct ListNode
{
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution
{
public:
void reorderList(ListNode* head)
{
//处理边界情况
if (head == nullptr || head->next == nullptr || head->next->next == nullptr)
return;
//1.找到链表的中间节点 - 快慢双指针
ListNode *slow = head, *fast = head;
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
//2.把slow后面的部分给逆序 - 头插法
ListNode* head2 = new ListNode(0);
ListNode* cur = slow->next;
slow->next = nullptr; //断开两个链表
while (cur) {
ListNode* next = cur->next;
cur->next = head2->next;
head2->next = cur;
cur = next;
}
//3.合并两个链表 - 双指针
ListNode* ret = new ListNode(0);
ListNode* prev = ret;
ListNode *cur1 = head, *cur2 = head2->next;
while (cur1)
{
//先放第一个链表
prev->next = cur1;
cur1 = cur1->next;
prev = prev->next;
//再放第二个链表
if (cur2)
{
prev->next = cur2;
prev = prev->next;
cur2 = cur2->next;
}
}
delete head2;
delete ret;
}
};
//数组转链表
ListNode* createList(int arr[], int n)
{
if (n == 0) return NULL;
ListNode* head = new ListNode(arr[0]);
ListNode* cur = head;
for (int i = 1; i < n; i++)
{
cur->next = new ListNode(arr[i]);
cur = cur->next;
}
return head;
}
//打印链表
void printList(ListNode* head)
{
if (!head) {
cout << "空链表" << endl;
return;
}
ListNode* cur = head;
while (cur)
{
cout << cur->val;
if (cur->next) cout << " -> ";
cur = cur->next;
}
cout << endl;
}
int main()
{
Solution sol;
//测试用例1:偶数长度链表
//原链表:1->2->3->4
//重排后:1->4->2->3
int a1[] = {1,2,3,4};
ListNode* l1 = createList(a1, 4);
cout << "测试用例1(偶数节点):" << endl;
cout << "原链表:"; printList(l1);
sol.reorderList(l1);
cout << "重排后:"; printList(l1);
cout << "-------------------------" << endl;
//测试用例2:奇数长度链表
//原链表:1->2->3->4->5
//重排后:1->5->2->4->3
int a2[] = {1,2,3,4,5};
ListNode* l2 = createList(a2, 5);
cout << "测试用例2(奇数节点):" << endl;
cout << "原链表:"; printList(l2);
sol.reorderList(l2);
cout << "重排后:"; printList(l2);
cout << "-------------------------" << endl;
//测试用例3:空链表
cout << "测试用例3(空链表):" << endl;
ListNode* l3 = nullptr;
cout << "原链表:"; printList(l3);
sol.reorderList(l3);
cout << "重排后:"; printList(l3);
cout << "-------------------------" << endl;
//测试用例4:单个节点
int a4[] = {6};
ListNode* l4 = createList(a4, 1);
cout << "测试用例4(单个节点):" << endl;
cout << "原链表:"; printList(l4);
sol.reorderList(l4);
cout << "重排后:"; printList(l4);
cout << "-------------------------" << endl;
//测试用例5:两个节点
int a5[] = {7,8};
ListNode* l5 = createList(a5, 2);
cout << "测试用例5(两个节点):" << endl;
cout << "原链表:"; printList(l5);
sol.reorderList(l5);
cout << "重排后:"; printList(l5);
return 0;
}

5.合并k个升序链表(OJ题)

算法思路:解法⼀(利⽤堆):
合并两个有序链表是⽐较简单且做过的,就是⽤双指针依次⽐较链表1 、链表2未排序的最⼩元素,选择更⼩的那⼀个加⼊有序的答案链表中.合并K个升序链表时,我们依旧可以选择K个链表中,头结点值最⼩的那⼀个.那么如何快速的得到头结点最⼩的是哪⼀个呢?⽤堆这个数据结构就好啦,我们可以把所有的头结点放进⼀个⼩根堆中,这样就能快速的找到每次K个链表中,最⼩的元素是哪个.
步骤1:自定义小根堆比较规则
- C++ 默认的优先队列是大根堆(堆顶为最大值),不符合需求;
- 自定义结构体
cmp重载运算符,将优先队列改为小根堆 ,保证堆顶始终是值最小的链表节点.
步骤2:初始化小根堆
- 遍历 K 个链表,将每个链表的非空头节点依次加入小根堆;
- 此时堆中包含了所有链表的第一个节点,堆顶就是全局最小的节点.
步骤3:创建虚拟头节点
创建虚拟头节点 ret,用于统一拼接结果链表,避免处理头节点为空的边界情况.
步骤4:循环合并链表(核心逻辑)
- 取出堆顶的最小节点,将其拼接到结果链表的末尾;
- 移动结果链表的指针,指向新的末尾;
- 关键操作:如果取出的节点还有后继节点,就将其后继节点加入堆中)保持堆里始终有各链表当前的候选最小节点);
- 重复上述操作,直到堆中没有任何节点.
步骤5:清理并返回结果
- 虚拟头节点的下一个节点,就是合并后链表的真实头节点;
- 释放虚拟头节点的内存,避免内存泄漏;
- 返回最终的合并链表.
核心代码
cpp
class Solution
{
struct cmp
{
bool operator()(const ListNode* l1, const ListNode* l2)
{
return l1->val > l2->val;
}
};
public:
ListNode* mergeKLists(vector<ListNode*>& lists)
{
//创建⼀个⼩根堆
priority_queue<ListNode*, vector<ListNode*>, cmp> heap;
//让所有的头结点进⼊⼩根堆
for (auto l : lists)
if (l)
heap.push(l);
//合并k个有序链表
ListNode* ret = new ListNode(0);
ListNode* prev = ret;
while (!heap.empty())
{
ListNode* t = heap.top();
heap.pop();
prev->next = t;
prev = t;
if (t->next)
heap.push(t->next);
}
prev = ret->next;
delete ret;
return prev;
}
};
算法思路:解法⼆(递归/分治思想):
逐⼀⽐较时,答案链表越来越⻓,每个跟它合并的⼩链表的元素都需要⽐较很多次才可以成功排序.
⽐如,我们有8个链表,每个链表⻓为100.逐⼀合并时,我们合并链表的⻓度分别为(0, 100), (100, 100), (200, 100), (300, 100), (400, 100), (500,100), (600, 100), (700, 100).所有链表的总⻓度共计3600.
如果尽可能让⻓度相同的链表进⾏两两合并呢?这时合并链表的⻓度分别是(100, 100) x 4, (200, 200)
x 2, (400, 400),共计2400.⽐上⼀种的计算量整整少了1/3.迭代的做法代码细节会稍多⼀些,这⾥给出递归的实现,代码相对简洁,不易写错.
算法流程:
- 特判,如果题⽬给出空链表,⽆需合并,直接返回;
- 返回递归结果.
递归函数设计:
- 递归出⼝:如果当前要合并的链表编号范围左右值相等,⽆需合并,直接返回当前链表;
- 应⽤⼆分思想,等额划分左右两段需要合并的链表,使这两段合并后的⻓度尽可能相等;
- 对左右两段分别递归,合并[l, r]范围内的链表;
- 再调⽤ mergeTwoLists 函数进⾏合并(就是合并两个有序链表)
步骤1:主函数入口
主函数 mergeKLists 仅做一件事:调用递归分治函数 ,传入链表数组和完整区间 [0, lists.size()-1],启动分治流程.
步骤2:递归分治拆分(核心函数 merge)
该函数负责拆分问题+递归求解:
- 终止条件
- 区间无效(
left > right):返回空指针; - 区间只剩1个链表 (
left == right):无需合并,直接返回该链表(递归到底层).
- 区间无效(
- 二分拆分
计算区间中点mid,将K个链表平分为左右两个子区间,把大问题拆分为两个规模减半的子问题. - 递归求解
分别递归处理左区间[left, mid]、右区间[mid+1, right],得到两个已合并完成的有序链表l1、l2. - 两两合并
调用mergeTwoList函数,合并l1和l2,返回合并后的有序链表.
步骤3:双指针合并两个有序链表(基础函数 mergeTwoList)
这是分治算法的基础操作,专门解决合并两个有序链表:
- 边界处理:若其中一个链表为空,直接返回另一个链表;
- 虚拟头节点:创建临时虚拟头节点,统一拼接逻辑,无需处理头节点为空的情况;
- 双指针遍历 :同时遍历两个链表,取节点值较小的节点拼接到结果链表;
- 拼接剩余节点:一个链表遍历完后,直接拼接另一个链表的剩余部分;
- 返回合并后的有序链表.
步骤4:递归回溯归并
所有子区间的合并结果会逐层向上合并 ,最终将K个链表合并为一个全局有序的链表.
核心代码
cpp
class Solution
{
public:
ListNode* mergeKLists(vector<ListNode*>& lists)
{
return merge(lists, 0, lists.size() - 1);
}
ListNode* merge(vector<ListNode*>& lists, int left, int right)
{
if (left > right)
return nullptr;
if (left == right)
return lists[left];
//1.平分数组
int mid = left + right >> 1;
//[left, mid] [mid + 1, right]
//2.递归处理左右区间
ListNode* l1 = merge(lists, left, mid);
ListNode* l2 = merge(lists, mid + 1, right);
//3.合并两个有序链表
return mergeTowList(l1, l2);
}
ListNode* mergeTowList(ListNode* l1, ListNode* l2)
{
if (l1 == nullptr)
return l2;
if (l2 == nullptr)
return l1;
//合并两个有序链表
ListNode head;
ListNode *cur1 = l1, *cur2 = l2, *prev = &head;
head.next = nullptr;
while (cur1 && cur2)
{
if (cur1->val <= cur2->val)
{
prev = prev->next = cur1;
cur1 = cur1->next;
} else
{
prev = prev->next = cur2;
cur2 = cur2->next;
}
}
if (cur1)
prev->next = cur1;
if (cur2)
prev->next = cur2;
return head.next;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
//链表节点结构体定义
struct ListNode
{
int val;
ListNode *next;
ListNode(int x = 0) : val(x), next(NULL) {}
};
class Solution
{
public:
ListNode* mergeKLists(vector<ListNode*>& lists)
{
return merge(lists, 0, lists.size() - 1);
}
ListNode* merge(vector<ListNode*>& lists, int left, int right)
{
if (left > right)
return nullptr;
if (left == right)
return lists[left];
//平分数组
int mid = left + right >> 1;
//递归处理左右区间
ListNode* l1 = merge(lists, left, mid);
ListNode* l2 = merge(lists, mid + 1, right);
//合并两个有序链表
return mergeTowList(l1, l2);
}
ListNode* mergeTowList(ListNode* l1, ListNode* l2)
{
if (l1 == nullptr)
return l2;
if (l2 == nullptr)
return l1;
//合并两个有序链表
ListNode head;
ListNode *cur1 = l1, *cur2 = l2, *prev = &head;
head.next = nullptr;
while (cur1 && cur2)
{
if (cur1->val <= cur2->val)
{
prev = prev->next = cur1;
cur1 = cur1->next;
}
else
{
prev = prev->next = cur2;
cur2 = cur2->next;
}
}
if (cur1)
prev->next = cur1;
if (cur2)
prev->next = cur2;
return head.next;
}
};
//数组转链表
ListNode* createList(int arr[], int n)
{
if (n == 0) return NULL;
ListNode* head = new ListNode(arr[0]);
ListNode* cur = head;
for (int i = 1; i < n; i++)
{
cur->next = new ListNode(arr[i]);
cur = cur->next;
}
return head;
}
//打印链表
void printList(ListNode* head)
{
if (!head)
{
cout << "空链表" << endl;
return;
}
ListNode* cur = head;
while (cur)
{
cout << cur->val;
if (cur->next) cout << " -> ";
cur = cur->next;
}
cout << endl;
}
int main()
{
Solution sol;
//测试用例1:标准场景(3个有序链表)
//链表1:1->4->5
//链表2:1->3->4
//链表3:2->6
//合并结果:1->1->2->3->4->4->5->6
cout << "===== 测试用例1:3个有序链表 =====" << endl;
int a1[] = {1,4,5};
int a2[] = {1,3,4};
int a3[] = {2,6};
ListNode* l1 = createList(a1, 3);
ListNode* l2 = createList(a2, 3);
ListNode* l3 = createList(a3, 2);
vector<ListNode*> lists1 = {l1, l2, l3};
ListNode* res1 = sol.mergeKLists(lists1);
cout << "合并结果:"; printList(res1);
//测试用例2:空链表数组
cout << "\n===== 测试用例2:空链表数组 =====" << endl;
vector<ListNode*> lists2;
ListNode* res2 = sol.mergeKLists(lists2);
cout << "合并结果:"; printList(res2);
//测试用例3:包含空链表的场景
cout << "\n===== 测试用例3:包含空链表 =====" << endl;
int a4[] = {7,9};
ListNode* l4 = createList(a4, 2);
vector<ListNode*> lists3 = {nullptr, l4, nullptr};
ListNode* res3 = sol.mergeKLists(lists3);
cout << "合并结果:"; printList(res3);
//测试用例4:单个链表
cout << "\n===== 测试用例4:单个链表 =====" << endl;
int a5[] = {2,3,5,8};
ListNode* l5 = createList(a5, 4);
vector<ListNode*> lists4 = {l5};
ListNode* res4 = sol.mergeKLists(lists4);
cout << "合并结果:"; printList(res4);
return 0;
}

6.k个一组翻转链表(OJ题)

算法思路:解法(模拟):
我们可以把链表按K个为⼀组进⾏分组,组内进⾏反转,并且记录反转后的头尾结点,使其可以和前、后连接起来.思路⽐较简单,但是实现起来是⽐较复杂的.我们可以先求出⼀共需要逆序多少组(假设逆序n组),然后重复n次⻓度为 k 的链表的逆序即可.
步骤1:统计总节点数,计算需要翻转的组数
- 遍历整个链表,统计总节点数量;
- 计算翻转组数:
组数 = 总节点数 / k(整数除法,自动舍去余数,余数即为最后不足k个、不翻转的节点);
✅ 作用:提前确定循环翻转次数,无需在翻转时判断节点数量.
步骤2:初始化辅助节点
- 创建虚拟头节点
newHead:固定链表入口,统一所有组的翻转逻辑,无需单独处理头节点翻转; prev指针:始终指向当前待翻转组的前驱节点,用于头插法拼接翻转后的节点;cur指针:遍历原链表,指向当前待处理的节点.
步骤3:循环翻转每一组(核心逻辑)
循环执行 组数 次,每次翻转长度为 k 的节点:
- 保存当前组的第一个节点
tmp:该节点翻转后会变成当前组的最后一个节点,用于定位下一组的前驱; - 对本组
k个节点执行头插法翻转 :
逐个将节点插入到prev->next位置,实现本组节点的逆序; - 移动指针:将
prev移动到tmp(当前组尾节点),准备下一组翻转.
步骤4:拼接剩余未翻转节点
循环结束后,cur 指向最后不足k个、无需翻转的节点,直接将其拼接到已翻转链表的末尾即可.
步骤5:清理内存并返回结果
- 虚拟头节点的下一个节点,就是翻转后的完整链表头;
- 释放虚拟头节点,避免内存泄漏;
- 返回最终结果链表.
举例演示
链表:1->2->3->4->5,k=2
- 总节点数=5,组数=5/2=2组;
- 第一组翻转:
1->2→2->1; - 第二组翻转:
3->4→4->3; - 剩余节点
5直接拼接; - 最终结果:
2->1->4->3->5。
核心代码
cpp
class Solution
{
public:
ListNode* reverseKGroup(ListNode* head, int k)
{
//1.先求出需要逆序多少组
int n = 0;
ListNode* cur = head;
while (cur)
{
cur = cur->next;
n++;
}
n /= k;
//2.重复 n 次:⻓度为 k 的链表的逆序即可
ListNode* newHead = new ListNode(0);
ListNode* prev = newHead;
cur = head;
for (int i = 0; i < n; i++)
{
ListNode* tmp = cur;
for (int j = 0; j < k; j++)
{
ListNode* next = cur->next;
cur->next = prev->next;
prev->next = cur;
cur = next;
}
prev = tmp;
}
//把不需要翻转的接上
prev->next = cur;
cur = newHead->next;
delete newHead;
return cur;
}
};
完整测试代码
cpp
#include <iostream>
using namespace std;
//链表节点结构体定义
struct ListNode
{
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution
{
public:
ListNode* reverseKGroup(ListNode* head, int k)
{
//1.先求出需要逆序多少组
int n = 0;
ListNode* cur = head;
while (cur)
{
cur = cur->next;
n++;
}
n /= k;
//2.重复 n 次:长度为 k 的链表的逆序即可
ListNode* newHead = new ListNode(0);
ListNode* prev = newHead;
cur = head;
for (int i = 0; i < n; i++)
{
ListNode* tmp = cur;
for (int j = 0; j < k; j++)
{
ListNode* next = cur->next;
cur->next = prev->next;
prev->next = cur;
cur = next;
}
prev = tmp;
}
//把不需要翻转的接上
prev->next = cur;
cur = newHead->next;
delete newHead;
return cur;
}
};
//数组转链表(尾插法)
ListNode* createList(int arr[], int n)
{
if (n == 0) return NULL;
ListNode* head = new ListNode(arr[0]);
ListNode* cur = head;
for (int i = 1; i < n; i++)
{
cur->next = new ListNode(arr[i]);
cur = cur->next;
}
return head;
}
//打印链表
void printList(ListNode* head)
{
if (!head)
{
cout << "空链表" << endl;
return;
}
ListNode* cur = head;
while (cur)
{
cout << cur->val;
if (cur->next) cout << " -> ";
cur = cur->next;
}
cout << endl;
}
int main()
{
Solution sol;
//测试用例1:标准场景 k=2
//原链表:1->2->3->4->5
//翻转后:2->1->4->3->5
int a1[] = {1,2,3,4,5};
ListNode* l1 = createList(a1, 5);
cout << "测试用例1 (k=2):" << endl;
cout << "原链表:"; printList(l1);
ListNode* res1 = sol.reverseKGroup(l1, 2);
cout << "翻转后:"; printList(res1);
cout << "-------------------------" << endl;
//测试用例2:k=3
//原链表:1->2->3->4->5
//翻转后:3->2->1->4->5
int a2[] = {1,2,3,4,5};
ListNode* l2 = createList(a2, 5);
cout << "测试用例2 (k=3):" << endl;
cout << "原链表:"; printList(l2);
ListNode* res2 = sol.reverseKGroup(l2, 3);
cout << "翻转后:"; printList(res2);
cout << "-------------------------" << endl;
//测试用例3:k=1(不翻转)
int a3[] = {1,2,3,4};
ListNode* l3 = createList(a3, 4);
cout << "测试用例3 (k=1):" << endl;
cout << "原链表:"; printList(l3);
ListNode* res3 = sol.reverseKGroup(l3, 1);
cout << "翻转后:"; printList(res3);
cout << "-------------------------" << endl;
//测试用例4:空链表
cout << "测试用例4(空链表):" << endl;
ListNode* l4 = nullptr;
cout << "原链表:"; printList(l4);
ListNode* res4 = sol.reverseKGroup(l4, 2);
cout << "翻转后:"; printList(res4);
cout << "-------------------------" << endl;
//测试用例5:k等于链表长度
//原链表:6->7->8 k=3
//翻转后:8->7->6
int a5[] = {6,7,8};
ListNode* l5 = createList(a5, 3);
cout << "测试用例5 (k=链表长度):" << endl;
cout << "原链表:"; printList(l5);
ListNode* res5 = sol.reverseKGroup(l5, 3);
cout << "翻转后:"; printList(res5);
return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容:【优选算法】(实战玩转哈希表:底层逻辑与刷题技巧)
每日心灵鸡汤:做自己,而不是解释自己!
你没有必要不停地向别人说其实我是怎样的人,因为这是无效的,人们只愿意看到他们愿意看到的你,
不用在意这样的话语,那样的目光,没有必要让所有人都知道真实的你.干掉标准答案,你就是标准答案,因为相信你的人自然相信你,而不相信你的人百口莫辩,你的时间就那么长,外界的声音只是参考,不喜欢就不参考.你就是你,他人就是他人,请好好爱自己,尊重理解自己.大风刮倒梧桐树,自有旁人论长短.
