单链表是数据结构课程的第一个核心线性结构,也是算法入门与校招面试的必练内容。不同于数组支持下标随机访问,单链表依靠 next 指针串联节点,所有操作都围绕「指针指向修改」展开,极其考验对边界场景的把控能力。
本文以三道最经典的单链表应用题为例,从思路推导到代码实现、从易错点复盘到算法思想升华,系统梳理单链表的核心操作与解题套路。
一、单链表基础回顾
单链表的每个节点由「数据域」和「后继指针域」组成,只能从头节点出发单向遍历,无法反向访问,也不能直接通过下标定位节点。 C 语言中节点的标准定义如下:
cpp
struct ListNode {
int val;
struct ListNode *next;
};
typedef struct ListNode ListNode;
单链表操作的三大核心特点:
- 已知前驱节点时,插入 / 删除节点的时间复杂度为 O (1),只需修改指针指向,无需移动元素。
- 访问任意节点必须从头遍历,时间复杂度 O (n),不支持随机访问。
- 头节点可能发生变化(如删除头节点、反转链表),是最容易出错的边界点。
在开始刷题前,先记住单链表的通用避坑原则:
- 访问
ptr->val或ptr->next前,必须确保ptr不为NULL,否则会触发空指针崩溃。 - 修改节点的
next指针前,先保存原后继节点,避免链表断裂、遍历中断。 - 重新拼接链表后,必须确保最终尾节点的
next = NULL,标记链表结束。
二、经典题逐题精讲
2.1 移除链表元素(LeetCode 203)
题目描述
给你一个链表的头节点 head 和一个整数 val,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点。

题意分析
这道题是单链表「删除操作」的入门题,核心难点不在删除逻辑本身,而在边界场景:
- 头节点本身就是要删除的节点,新头节点会变化
- 链表尾部是要删除的节点
- 链表所有节点都需要删除,最终返回空链表
- 链表本身就是空链表
解法一:尾插法构建新链表
这是最符合直觉的思路:不直接在原链表上做剪断删除,而是遍历原链表,把值不等于 val 的合法节点,按原有顺序重新拼接成一个新链表。
完整代码
cpp
struct ListNode* removeElements(struct ListNode* head, int val) {
// 新链表的头、尾指针,初始为空
ListNode *newHead, *newTail;
newHead = newTail = NULL;
ListNode *pcur = head;
// 遍历原链表
while(pcur)
{
if(pcur->val != val)
{
// 当前节点需要保留,尾插到新链表
if(newHead == NULL)
{
// 新链表为空,第一个节点同时是头和尾
newHead = newTail = pcur;
}
else
{
// 接到尾部,尾指针后移
newTail->next = pcur;
newTail = newTail->next;
}
}
pcur = pcur->next;
}
// 关键收尾:新链表非空时,尾节点next置空
if(newTail != NULL)
{
newTail->next = NULL;
}
return newHead;
}
思路细节拆解
- 天然解决头节点删除问题:第一个被保留的节点自然成为新头,无需单独判断原头节点是否要删。
- 尾节点置空是必写步骤:如果原链表尾部是要删除的节点,新链表最后一个节点的
next仍会指向原链表的无效节点,必须手动置空标记链表结束。 - 空指针防护:如果所有节点都被删除,
newTail仍为NULL,必须先判断非空再访问next。
解法二:虚拟头节点原地删除
这是链表删除题的工业级标准写法,核心是在原链表头部加一个「哨兵节点(dummy)」,让所有待删除节点都成为 "某个节点的后继",删除逻辑完全统一。
完整代码
cpp
struct ListNode* removeElements(struct ListNode* head, int val) {
// 创建虚拟头节点,next指向原链表头
ListNode dummy;
dummy.next = head;
ListNode* cur = &dummy;
while (cur->next != NULL) {
if (cur->next->val == val) {
// 下一个节点要删除:直接跳过该节点
cur->next = cur->next->next;
} else {
// 下一个节点保留:指针后移
cur = cur->next;
}
}
return dummy.next;
}
这种写法的优势非常明显:无需单独处理头节点,无需手动置空尾节点,代码更简洁,边界场景全部自动兼容。
2.2 反转链表(LeetCode 206)
题目描述
给你单链表的头节点 head,请你反转链表,并返回反转后的链表。

题意分析
反转链表的本质,是把每个节点的 next 指针从「指向后继」改为「指向前驱」。最大的难点是:直接修改当前节点的 next 会丢失后继节点,导致链表断裂、遍历无法继续。
解法:三指针迭代原地反转
这是面试的标准最优解,空间复杂度 O (1),全程只通过三个指针配合,逐个翻转节点指向。
完整代码
cpp
struct ListNode* reverseList(struct ListNode* head) {
if(head == NULL)
{
return head;
}
ListNode *n1, *n2, *n3;
n1 = NULL; // 前驱节点
n2 = head; // 当前节点
n3 = head->next; // 后继节点(提前保存,防止断链)
while(n2)
{
// 翻转当前节点的指向
n2->next = n1;
// 三个指针整体后移
n1 = n2;
n2 = n3;
if(n3)
{
n3 = n3->next;
}
}
return n1;
}
三个指针的分工
n1:记录当前节点的前驱节点,也就是反转后当前节点要指向的目标。初始为NULL,因为原头节点反转后会变成尾节点。n2:当前正在处理的节点,从原头节点开始逐个向后遍历。n3:提前保存当前节点的后继节点,相当于 "备份后路"。修改n2->next后原后继会丢失,必须提前存下来。
执行流程模拟(以 1→2→3→NULL 为例)
- 初始:
n1=NULL,n2=1,n3=2 - 第一轮:节点 1 的 next 指向 NULL;n1 移到 1,n2 移到 2,n3 移到 3
- 第二轮:节点 2 的 next 指向 1;n1 移到 2,n2 移到 3,n3 移到 NULL
- 第三轮:节点 3 的 next 指向 2;n1 移到 3,n2 移到 NULL,循环结束
- 此时 n1 指向原链表最后一个节点,也就是反转后的新头节点,直接返回即可。
拓展:递归解法
递归写法代码更简洁,核心思想是 "先递归到链表尾部,再从后往前逐个翻转"。
cpp
struct ListNode* reverseList(struct ListNode* head) {
// 终止条件:空链表或只有一个节点,无需反转
if (head == NULL || head->next == NULL) {
return head;
}
// 先反转子链表,得到子链表的新头
ListNode* newHead = reverseList(head->next);
// 把当前节点接到子链表尾部
head->next->next = head;
// 当前节点变为尾节点,置空防止成环
head->next = NULL;
return newHead;
}
2.3 链表的中间结点(LeetCode 876)
题目描述
给你单链表的头结点 head,请你找出并返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

题意分析
单链表不能随机访问,朴素做法是「先遍历一遍统计长度,再从头走长度的一半」,但需要两次遍历。 这道题的最优解是快慢指针(龟兔赛跑算法),只需一次遍历就能找到中点,也是单链表最经典的算法思想之一。
核心算法:快慢指针
利用两个指针的速度差实现定位:
- 慢指针
slow:一次走 1 步 - 快指针
fast:一次走 2 步
因为快指针速度是慢指针的 2 倍,当快指针走到链表末尾时,慢指针走过的路程刚好是一半,正好停在中间位置。
完整代码
cpp
struct ListNode* middleNode(struct ListNode* head) {
ListNode *Fast, *Slow;
Fast = Slow = head;
while(Fast && Fast->next)
{
Slow = Slow->next;
Fast = Fast->next->next;
}
return Slow;
}
循环条件深度解析
while(Fast && Fast->next) 是这道题最核心的细节,有两个关键点:
- 顺序不能反 :利用 C 语言的短路求值特性,先判断
Fast是否为空;如果Fast已经是 NULL,程序不会再访问Fast->next,完美避免空指针崩溃。 - 决定了中点落点:这个条件会让快指针最终停在「最后一个节点的下一位(NULL)」,因此偶数长度时,慢指针正好落在第二个中间节点上,完全符合题目要求。
场景验证
- 奇数长度(5 个节点):快指针停在最后一个节点,慢指针停在第 3 个节点(正中间)
- 偶数长度(6 个节点):快指针停在 NULL,慢指针停在第 4 个节点(第二个中间节点)
拓展与延伸
如果题目要求返回第一个中间节点,只需修改循环条件:
cpp
while(Fast->next && Fast->next->next)
快慢指针的应用远不止找中点,它还是解决「判断链表是否有环」「找环的入口节点」「找链表倒数第 k 个节点」等经典题的核心思想,是单链表必须掌握的算法套路。
三、单链表解题通用方法论
做完这三道题,我们可以提炼出单链表应用题的通用解题框架:
-
头节点变化的两种处理方案 只要题目可能修改头节点(删除、反转、排序),要么用「新链表 + 新头指针」,要么用「虚拟头节点」,两种思路都能避免单独处理头节点的冗余逻辑。
-
指针修改的铁则 凡是要修改节点的
next指针,先问自己一句:修改之后,原后继节点还找得到吗?如果找不到,就必须先用临时指针保存下来,防止断链。 -
边界场景必测清单 写完代码后,务必用这几个场景验证:空链表、单个节点、两个节点、目标节点在头部、目标节点在尾部、所有节点都符合删除条件。能覆盖这几个场景,代码基本不会出大问题。
-
画图辅助调试 链表题的指针跳转很抽象,遇到逻辑混乱时,在纸上画出节点和箭头,手动模拟每一步指针的移动,比盯着代码死想高效得多。
写在最后
这三道题是单链表的「基石题」,覆盖了删除、反转、快慢指针三大核心考点。把这三道题的每一行代码、每一个边界条件都吃透,再去学习合并有序链表、环形链表、相交链表等进阶题目时,就会发现核心逻辑都是相通的。
数据结构的学习没有捷径,多写、多画、多踩坑、多复盘,慢慢就能建立起对指针操作的直觉。