文章目录
-
- 前言
- 一、基础认知:链表节点是引用,不是值
- [二、206. 反转链表](#二、206. 反转链表)
- [三、234. 回文链表](#三、234. 回文链表)
- [四、141/142. 环形链表(检测 + 找入口)](#四、141/142. 环形链表(检测 + 找入口))
-
- [141. 判断是否有环](#141. 判断是否有环)
- [142. 找环的入口](#142. 找环的入口)
- [五、160. 相交链表](#五、160. 相交链表)
- [六、21. 合并两个有序链表](#六、21. 合并两个有序链表)
- 七、错误总结
前言
这篇文章整理链表算法的六道经典题,帮你建立从"能写出来"到"不犯低级错误"的认知闭环。
刷链表题最大的坑不是思路,是细节:引用和值傻傻分不清、循环中途空指针、计数差一个......这些错误我都踩过,全记在这里。
一、基础认知:链表节点是引用,不是值
在讲题之前,必须先搞清一件事。
java
ListNode a = node;
ListNode b = node;
a.next = null; // b.next 也变了!
a 和 b 指向同一块内存,改一个等于改另一个。
链表操作的所有诡异 bug,90% 来源于没意识到"两个变量指向同一对象"。
这个认知贯穿后面所有题。
二、206. 反转链表
思路 :迭代,维护 headR(已反转部分的头)和 head(待处理部分的头)。
我犯的错:
java
// ❌ 错误写法
ListNode tmp = headR;
headR = head;
headR.next = tmp;
head = head.next; // 此时 head.next 已经是 tmp 了,不是原来的下一个!
headR = head 之后两个变量指向同一对象,修改 headR.next 同时也改了 head.next。
正确写法:先把 next 存起来。
java
public ListNode reverseList(ListNode head) {
ListNode headR = null;
while (head != null) {
ListNode next = head.next; // 先存
head.next = headR;
headR = head;
head = next;
}
return headR;
}
规律:凡是要改某个节点的 next,先把原来的 next 存下来。
三、234. 回文链表
思路:找中点 → 反转后半段 → 双指针对比。
我犯的两个错:
错误1:计数从 1 开始
java
int cnt = 1; // ❌ 应该是 0
while (true) {
if (now == null) break;
now = now.next;
cnt++;
}
循环结束时 now == null,说明已经走完了,cnt 多加了 1。
错误2:用地址比较回文
java
if (head != headR) return false; // ❌ 比较的是地址,不是值
反转后的节点是原节点,地址不可能相同,永远返回 false。
java
if (head.val != headR.val) return false; // ✅
完整代码:
java
public boolean isPalindrome(ListNode head) {
int cnt = 0;
ListNode now = head;
while (now != null) {
now = now.next;
cnt++;
}
int half = cnt / 2;
ListNode headR = null;
while (half > 0) {
ListNode next = head.next;
head.next = headR;
headR = head;
head = next;
half--;
}
if (cnt % 2 == 1) head = head.next; // 奇数跳过中间节点
while (head != null) {
if (head.val != headR.val) return false;
head = head.next;
headR = headR.next;
}
return true;
}
四、141/142. 环形链表(检测 + 找入口)
141. 判断是否有环
思路:快慢指针,快指针每次走 2 步,慢指针走 1 步,有环必相遇。
我犯的错 :while 只在每轮开头检查一次条件,循环体中途不管。
java
while (fut.next != null) {
fut = fut.next;
if (fut.next == now) return true; // fut 可能刚变成 null,取 .next 直接空指针
fut = fut.next; // ❌ 没有判断 fut 是否为 null
}
中途 fut 变成 null 后,再执行 fut.next 就崩了。必须加中途检查:
java
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) return false;
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
while和for的条件都只管每轮入口,管不了循环体中途。
142. 找环的入口
数学结论:快慢指针相遇后,一个指针从 head 出发,另一个从相遇点出发,同速前进,再次相遇即为入口。
设链表头到入口距离 = a
入口到相遇点距离 = b
相遇点回到入口距离 = c
快指针路程 = 2 × 慢指针路程
a + b + k(b+c) = 2(a + b)
→ a = c + (k-1)(b+c)
所以从 head 走 a 步 = 从相遇点走 c 步(加若干整圈),两者在入口相遇。
java
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
ListNode ptr = head;
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
return null;
}
五、160. 相交链表
思路:两个指针分别走完自己链表后走另一条,路程相同时必然在交点相遇(或同时到 null)。
关键 :相交判断用 ==(地址相同),不用 .val 比较。
A 链表长度 a + c
B 链表长度 b + c
指针 pA 走完后走 B,总路程 = a + c + b
指针 pB 走完后走 A,总路程 = b + c + a
路程相等,在交点处地址相同。
java
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
六、21. 合并两个有序链表
我的笨办法:while 循环每次比较两个头节点的值,把较小的接到结果链表上,最后再把剩余链表拼上去。逻辑没错,但要维护一个 dummy 头节点加三段循环,代码很啰嗦。
递归写法:换一个角度------合并两个链表,就是"选出当前最小的节点,它的 next 等于剩下两段继续合并的结果"。
mergeTwoLists(l1, l2)
= 取 min(l1, l2),其 next = mergeTwoLists(另一个, min.next)
递归终止条件:某个链表为 null,直接返回另一个。
java
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if (list1 == null) return list2;
if (list2 == null) return list1;
if (list1.val < list2.val) {
list1.next = mergeTwoLists(list1.next, list2);
return list1;
} else {
list2.next = mergeTwoLists(list1, list2.next);
return list2;
}
}
递归的本质:把"对整体的操作"转化成"对当前节点的操作 + 对剩余部分的同样操作"。链表天然适合递归,因为每个节点结构相同。
两种写法对比:
迭代:需要 dummy 头、三段循环、手动拼尾,代码 ~20 行
递归:终止条件 + 一个判断,代码 ~10 行,但有函数调用栈开销
七、错误总结
┌─────────────────────────────────────────────────────┐
│ 错误类型 │ 出现题目 │ 根因 │
├─────────────────────────────────────────────────────┤
│ 修改 next 前没存 │ 206/234 │ 两变量同一对象 │
│ cnt 初始值为 1 │ 234 │ 循环边界差一 │
│ 用 == 比较值 │ 234 │ 混淆地址和值比较 │
│ 循环中途空指针 │ 141 │ while 只管入口 │
└─────────────────────────────────────────────────────┘
链表题的本质是指针操作,核心心法只有一条:动 next 之前,先把它存起来。
下一篇:链表算法下篇------合并、排序、删除节点系列