不知道你有没有这种感觉:链表题做多了以后,会觉得这玩意儿其实不是考“数据结构”,是考“你手里有几根指针,心里有没有一张图”。我在刷力扣hot100的链表专题时最大的体会是,链表题的套路密度非常高,翻来覆去就那么几件事:反转、找中点、删节点、合并、判断环。真正难的其实不是某个算法,而是写代码的时候能不能保证“没丢节点、没指错边、边界没崩”。这篇刷题笔记我先聚焦最基础也最高频的一批:反转类、双指针类、哑节点类,以及一道把多个套路串起来的综合题。适合刚开始按专题刷hot100的读者,也适合刷过一遍但总觉得链表题不稳的人,专门用来把底层方法理清楚。
1. 链表题在hot100里的存在感,本质上是因为它考的是“操作感”
数组题考的是思维模型,动态规划考的是状态定义,链表题考的是操作。LeetCode上链表类题目的数量对比数组、哈希表、DP来说并不算特别多,但它出现在hot100里的密度相当高,而且很多题目是“看起来不难,一写就错”的类型。
1.1 链表结构给命题人提供了两个天然考点
第一个天然考点是指针/引用的操作准确度。单链表只能从头往后走,想回头就得靠额外指针或递归栈,想让一个节点从链表里摘出去,就得改前一个节点的next,而前一个节点怎么找到,本身就值得一考。
第二个天然考点是边界条件的敏感度。操作空链表、单节点链表、两个节点链表,循环条件写错一个符号就会产生空指针解引用,或者漏掉最后一个节点。这种边界错误恰恰是刷题时最能暴露代码功底的地方。
还有一个容易被忽略的点:链表可以在O(1)时间内完成插入和删除(前提是已经定位到前驱节点),这跟数组是相反的。很多实际场景里的“缓存淘汰”“任务队列”“撤销栈”都会用到这种结构特性,所以面试官天然喜欢拿链表来问“你会不会改结构”。
1.2 hot100里链表题的分布,其实非常集中
我把自己刷过的hot100链表相关题目整理了一下,发现可以按模式归类,而不是按难度归类:
| 常见链表题 | 核心考点 | 归类模式 |
|---|---|---|
| 206. 反转链表 | 迭代/递归反转 | 反转类 |
| 92. 反转链表 II | 区间内反转 + 边界接续 | 反转类 |
| 24. 两两交换链表中的节点 | 成对反转 + 递归/迭代 | 反转类 |
| 25. K 个一组翻转链表 | 分组反转 | 反转类(进阶) |
| 141. 环形链表 | 快慢指针判环 | 双指针类 |
| 142. 环形链表 II | 快慢指针找入环点 | 双指针类 |
| 19. 删除链表的倒数第 N 个结点 | 间隔指针/哑节点 | 双指针类 |
| 160. 相交链表 | 双指针走对方链 | 双指针类 |
| 21. 合并两个有序链表 | 哑节点 + 比较移动 | 哑节点类 |
| 23. 合并 K 个升序链表 | 多路归并 / 优先队列 | 哑节点类(进阶) |
| 2. 两数相加 | 模拟竖式加法 + 进位 | 哑节点类 |
| 234. 回文链表 | 快慢找中 + 反转后半 | 综合类 |
| 148. 排序链表 | 归并排序 + 找中点 | 综合类 |
| 138. 随机链表的复制 | 哈希表 / 原地复制 | 综合类 |
| 146. LRU 缓存 | 哈希表 + 双向链表 | 综合类(进阶) |
这张表基本就是我刷链表专题的路线图。本篇文章先吃透前三类,第四类综合题里选“回文链表”作为代表作拆解一遍,因为它的每一步都是前面基础套路的组合。排序链表、复制随机链表、LRU这类结构更重的题目,放到后面的笔记里展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 反转类题目的肌肉记忆:先写熟“三行核心代码”再谈递归
反转链表是hot100里最基础的一道题,同时又是无数复杂题的子过程。比如后面要说的回文链表、反转链表II、K个一组翻转,全都依赖单链表反转这个原子操作。这个操作必须写到不需要过脑子的程度。
2.1 迭代反转:为什么必须准备三个指针
单链表无法原地回头,所以反转过程必须用指针记录三样东西:前一个节点、当前节点、下一个节点。这就是pre/cur/nxt三件套。
cpp复制ListNode* reverseList(ListNode* head) {
ListNode* pre = nullptr;
ListNode* cur = head;
while (cur) {
ListNode* nxt = cur->next; // 先保住后路
cur->next = pre; // 把箭头掉头
pre = cur; // pre 前移
cur = nxt; // cur 前移
}
return pre;
}
这段代码里面有个顺序问题,我第一次刷的时候犯过混:cur->next = pre 这行一旦执行,cur后面的链表就暂时找不到了,所以必须提前把nxt存下来。循环最后返回的是pre而不是cur,因为循环结束时cur已经走到nullptr,pre才是原来的尾节点、反转后的新头。
从小白到熟练,我建议在纸上把1->2->3->null的每轮指针变化都画一遍。画三轮你就能理解为什么必须有三根指针,而不是去背代码。画完图再回来看代码,会觉得每行都有明确的存在理由。
2.2 递归反转:一句话解释head->next->next = head
递归版反转代码很短,但很多人看不懂:
cpp复制ListNode* reverseList(ListNode* head) {
if (!head || !head->next) return head;
ListNode* newHead = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return newHead;
}
理解它的关键是把目光放在“
