💜 C++ 底层矩阵 · 代码永不停歇
| 👤 作者主页 | 🔥 C++ 核心专栏 |
|---|---|
| 💾 算法题解仓库 | 📁 代码仓库 |
一、前言
哈喽大家好,欢迎来到【底层技术矩阵】算法进阶之路--链表篇
🎯 这篇博客能帮你解决什么问题?
读完这篇,你将掌握链表题的「三板斧」:
- 快慢指针:环形链表、中点、倒数第K个节点的最优解法
- 相交链表:判断相交+定位入口的两种经典思路
- 反转链表:从基础反转到区间、K个一组的全场景覆盖
同时,你会掌握链表题的通用解题技巧:虚拟头节点、画图模拟、边界处理,再也不怕指针绕晕。
二、链表的核心操作
在进入实战之前,我们不妨先来回忆一下链表的核心操作以及常见技巧便于加深我们对链表的印象
2.1.引入虚拟头节点--哨兵位
由于我们算法题中的链表一般都是单向不带头链表,往往会有一些特殊的边界情况需要处理,比如当头节点为空的时候,需要作为边界情况单独处理,而我们引入虚拟头节点之后,就算头节点为空,也仍能将其作为中间节点处理(此时的头是虚拟头节点),不用单独处理
2.2.画图
其实在我们做算法题的时候,最应该做的就是画图,尤其是在链表这一专题更应该画图,否则有时候指针的指向就会把自已给绕晕
三、链表的常见题型
3.1.快慢双指针
核心原理:是利用两个指针的速度不同,使得在单词的遍历中实现对应的目标
快慢双指针均初始化为头节点,快指针一次走两步,慢指针一次走一步
时间复杂度:仅需单次遍历,时间复杂度为O(n)
- 判断是否有环
- 核心逻辑:
1.无环链表中,fast 速度更快,会率先到达尾节点,循环终止
2.有环链表中,fast 会在环内循环,而 slow 以 1 倍速追赶,由于两者速度差为 1,必然会在环内相遇。
- 核心逻辑:
cpp
bool hasCycle(ListNode* head) {
// 空链表或单节点无环
if (head == nullptr || head->next == nullptr) {
return false;
}
ListNode* fast = head,*slow = head;
// 循环条件:保证fast能安全移动两步,避免空指针异常
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (fast == slow)
return true;
}
return false;
}
- 寻找链表的中间节点
- 核心逻辑:利用快指针比慢指针快一倍这一特点,当快指针到达末尾时,慢指针刚好指向中间节点,要注意的是如果链表的节点数目是
偶数的话,指向的是偏右的那个中间节点,如果节点数目是奇数的话,则刚好指向中间节点
cpp
ListNode* findMiddle(ListNode* head) {
if (head == nullptr) {
return nullptr;
}
ListNode* fast = head,*slow = head;
// 当fast到达末尾时,slow刚好在中点
while (fast != nullptr && fast->next != nullptr) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
拓展 :如何找到偏左的中间节点?
只需修改循环条件,即可在偶数长度链表中停在偏左节点:
cpp
ListNode* findMiddleLeft(ListNode* head) {
if (head == nullptr) {
return nullptr;
}
ListNode* fast = head;
ListNode* slow = head;
// 提前终止循环,避免slow移动到偏右位置
while (fast->next && fast->next->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
- 寻找倒数第K个节点
- 暴力解法:要寻找倒数第K个节点,先遍历一遍,找出整个链表的节点个数,然后转换成正数第N个节点,再遍历一遍即可
- 快慢双指针:利用倒数第K个节点距离末尾距离为K这一核心特点,先让块指针先移动K步,而慢指针仍然指向头节点,此时
快慢双指针相距K,接着让快慢双指针同速移动,这样当快指针走到末尾的时候,慢指针就指向倒数第K个节点
cpp
ListNode* findKthToTail(ListNode* head, int k) {
if (head == nullptr || k <= 0) {
return nullptr;
}
ListNode* fast = head,*slow = head;
// 1.fast先走k步,制造距离差
while (k--) {
// 边界处理:k大于链表长度时直接返回
if (fast == nullptr) {
return nullptr;
}
fast = fast->next;
}
// 2.fast与slow同速移动,直到fast到达尾节点
while (fast != nullptr) {
fast = fast->next;
slow = slow->next;
}
return slow;
}
⚠️ 易错点避坑指南
- 判环的循环条件while(fast && fast->next),必须两个都判断并且fast必须先判断,否则会导致空指针解引用。
- 找中点时,偶数长度的节点偏左 / 偏右,循环条件不同,别搞混。
- 倒数第 K 个节点,k大于链表长度、k<=0的边界,一定要提前处理
实战链接 :
判断链表有环:LeetCode 141.环形链表
寻找链表中点:Leetcode 876.链表的中间节点
寻找倒数第 K 个节点:LCR021. 链表中倒数第 k 个节点
3.2.判相交节点
两个单链表可能存在相交关系:从某个节点开始,后续的所有节点被两个链表共享,
如图:A和B构成相交关系
我们需要解决两个核心问题 :
1.如何判断两个链表是否相交?
2.如果相交,如何找到相交的入口节点?

那么怎么判断两个链表具有相交关系呢?
可以看出相交链表的核心特征是「共享公共后缀」,因此尾节点必然相同,我们只需要对两个链表分别遍历到尾节点,然后判断尾节点是否相同即可
cpp
bool isintersect(ListNode*head1,ListNode* head2){
// 边界处理:空链表不可能相交
if (head1 == nullptr || head2 == nullptr) {
return false;
}
ListNode* cur1 = head1,cur2 = head2;
while(cur1->next){
cur1 = cur1->next;
}
while(cur2->next){
cur2 = cur2->next;
}
return cur1 == cur2;
}
进阶 :既然知道了两个链表已经相交了,那么怎么判断相交的入口点呢?
思路 :利用快慢双指针
1.先暴力遍历出两个链表的长度,算出长度之差的绝对值(L)
2.让快指针指向长度长的链表的头节点,先移动L步,此时两个指针到尾节点的距离相等
3.接着同速移动快慢双指针,当快慢双指针相遇,即找到了相交的入口点
cpp
ListNode* getIntersectionNode(ListNode* head1, ListNode* head2) {
// 边界处理:空链表直接返回
if (head1 == nullptr || head2 == nullptr) {
return nullptr;
}
ListNode* cur1 = head1;
ListNode* cur2 = head2;
int l1 = 0, l2 = 0;
// 统计两个链表的长度
while (cur1 != nullptr) {
l1++;
cur1 = cur1->next;
}
while (cur2 != nullptr) {
l2++;
cur2 = cur2->next;
}
cur1 = head1,cur2 = head2;
int diff = abs(l1 - l2);
// 让长链表的指针先走diff步
if (l1 > l2) {
while (diff--) cur1 = cur1->next;
} else {
while (diff--) cur2 = cur2->next;
}
// 同步移动,直到相遇
while (cur1 != cur2) {
cur1 = cur1->next;
cur2 = cur2->next;
}
return cur1;
}
⚠️ 易错点避坑指南
- 判断相交时,不能直接比较值,必须比较节点地址
- 找入口节点时,长度差计算必须取绝对值,否则会出现负数循环次数
实战链接
3.3.反转链表
我们在刷题的时候,可能会遇到题目要求我们将链表反转的情况
如图所示:这就需要用到反转链表算法了

反转链表算法属于是基础但比较容易出错的算法了,今天我带你们彻底理清思路,避免出错
核心思路:反转链表的本质是逐个修改每个节点的next指向,但直接修改会导致链表断链,因此我们需要用额外指针保存节点地址,保证不断链,需要定义两个指针cur,prev
cpp
ListNode* reverseList(ListNode* head) {
// 边界处理:空链表或单节点直接返回
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode* prev = nullptr;
ListNode* cur = head;
while (cur) {
// 1. 保存后继节点,防止断链
ListNode* next = cur->next;
// 2. 反转当前节点的指向
cur->next = prev;
// 3. 双指针同步前移
prev = cur;
cur = next;
}
// prev此时指向原尾节点,即反转后的新头节点
return prev;
}
拓展:
- 拓展一:反转链表的前N个节点
例子:
核心思路:仅需要把前N个节点当成整个链表即可,与反转整个链表 唯一的不同**就在于:反转前N个节点需要记录第N+1个节点,防止断链,其余的操作均与反转整个链表类似,最后再与前N个节点与第N+1个节点链接即可
cpp
ListNode* reverseN(ListNode* head, int n) {
if (head == nullptr || n <= 1) return head; // 处理 n=0,1 及空链表
// 检查链表长度是否足够
ListNode* p = head;
for (int i = 0; i < n; ++i) {
if (!p) return head; // 长度不足 n,不做反转
p = p->next;
}
ListNode* prev = nullptr,*cur = head;
for (int i = 0; i < n; ++i) {
ListNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
head->next = cur; // 原 head 指向第 n+1 个节点
return prev; // 原第 n 个节点成为新头
}
- 拓展二:反转链表的区间[left,right]
例子:
- 核心思路:依旧是把待反转的区间当成整个链表,提前记录left-1的节点,然后将反转后的区间与两边节点连起来即可 - 边界情况:需要单独处理left == 0的情况,因为此时取不到left-1,但是我们可以引入虚拟头节点,避免单独讨论,保证代码的统一性
cpp
// pre:反转区间的前驱节点(例子中是 1)
// start:反转区间的源头节点(例子中是 2,反转后会变成区间尾节点)
// prev:反转后的新区间头节点(例子中是 4)
// cur:反转区间的后继节点(例子中是 5)
ListNode* reverseBetween(ListNode* head, int left, int right) {
if (head == nullptr || left == right) return head;
// 虚拟头节点,简化边界处理
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* pre = dummy;
// 1. 移动 pre 到 left 的前一个节点
for (int i = 0; i < left-1; ++i) {
pre = pre->next;
}
// 2. start 指向 left 节点
ListNode* start = pre->next;
// 3. 反转从 left 到 right 的节点
ListNode* cur = start;
ListNode* prev = nullptr;
for (int i = left; i <= right; ++i) {
ListNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
// 4. 连接回原链表
pre->next = prev; // left 的前一个节点指向新头
start->next = cur; // 原 left 节点(现尾部)指向 right 后的节点
return dummy->next; // 注意:返回的是虚拟头节点的next
}
- 拓展三:K个一组反转链表
问题描述:给定一个链表,以K个节点为一组进行反转,若不足K个节点则不反转
例子:

到了这里,相信大家已经对K个节点的反转已经心中有数,而题目中无非就是多组K个节点进行反转而已,只需要递归处理每一组K个节点即可
- 核心思路:
1.分组判断 :先检查当前链表是否有 k 个节点,不足则直接返回头节点。
2.反转当前组 :用基础的迭代法反转当前 k 个节点,记录新的头节点。
3.递归处理下一组:将当前组的尾节点(反转后的尾)指向递归返回的下一组的头节点。
cpp
// 用于处理K个节点的反转
ListNode* reverseK(ListNode* head, int k) {
ListNode* prev = nullptr;
ListNode* cur = head;
for (int i = 0; i < k; i++) {
ListNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
ListNode* reverseKGroup(ListNode* head, int k) {
if (head == nullptr || k == 1) return head;
// 检查是否有 k 个节点
ListNode* check = head;
for (int i = 0; i < k; i++) {
if (check == nullptr) return head;
check = check->next;
}
// 反转前 k 个节点
ListNode* newHead = reverseK(head, k);
// 递归处理下一组并连接:经过反转后,head已经成为尾,newhead成为头
head->next = reverseKGroup(check, k);
return newHead;
}
⚠️ 易错点避坑指南
- 基础反转要返回新头节点
- 处理区间翻转建议使用虚拟头节点处理left == 1,尤其注意在返回值的时候返回的是虚拟头节点的next
- K 个一组反转,需要提前判断链表长度是否足够k个,不足直接返回
实战链接
四、结尾
🧩 链表题通用解题步骤
- 先看边界:空链表?单节点?头节点特殊处理?先把这些情况列出来。
- 再选技巧 :
- 判环/中点/倒数第K个 → 快慢指针
- 反转/区间反转/K个一组 → 迭代/递归反转+虚拟头节点
- 相交链表 → 尾节点判断/长度差法/双指针法
- 画图模拟:画3-5个节点,手动走一遍指针移动过程,确定指针指向没有问题
- 写代码+注释:关键步骤加注释,尤其是边界处理和断链保护的地方,这样可以保证自已的思路更加清晰
