链表专题 刷题笔记
专题标签:链表 - 指针操作 / 递归专题核心考点:链表的遍历、指针修改、结构操作(反转 / 合并 / 拆分)、特殊场景(环 / 相交 / 随机指针)
题目 1:相交链表(双指针解法)(难度:简单)
1、题目核心定义
-
相交定义:两个链表存在「地址相同的公共节点链」(尾部完全重合),而非"节点值相同"或"位置对应";
-
输入:两个无环链表的头节点 headA、headB;
-
输出:相交节点(无相交则返回 null)。
2、双指针解法核心逻辑(底层原理)
核心思想:强制让两个指针走相同总路程(m+n,m、n为两链表长度),同步移动下要么中途相遇(相交),要么终点碰头(均为null,不相交),以此抵消长度差(隐式对齐)。
两种最终场景:
-
场景1(相交):指针会在「公共节点链入口」相遇(总路程未到m+n时);
-
场景2(不相交):指针会在「总路程刚好m+n时」同时指向null(无公共节点可走)。
- 场景 1:相交(前提:存在地址相同的公共节点)
- 子场景 1:两链表「不相交节点数相同」→ 双指针同步移动,无需走满 m+n 就相遇在公共节点;
- 子场景 2:两链表「不相交节点数不同」→ 双指针走满 m+n 前,必会在公共节点相遇;
- 核心:只要有公共节点,指针必然在「公共节点入口」碰头,不会走满全程;
- 场景 2:不相交(本质:无任何公共节点,平行关系)
- 子场景 1:两链表长度不同 → 双指针走满 m+n 总路程,同时指向
null; - 子场景 2:两链表长度相同 → 双指针同步遍历完自身链表,同时指向
null; - 核心:无公共节点时 ,指针最终必同时为
null,不会中途相遇;
- 子场景 1:两链表长度不同 → 双指针走满 m+n 总路程,同时指向
- 关键规则 :
- 循环条件:判断
pA == pB(指针本身相等),绝对不能用.next判断 → 否则不相交场景会触发无限循环; - 相交判定:仅看「节点引用(地址)」,而非
val(避免值同节点不同的误判)。
- 循环条件:判断
3、标准模板代码(Java版,面试直接写)
全程仅分两种场景,且前置必做边界判断:
-
前置边界 :先判断
headA/headB是否为空 → 任一为空则直接返回null(空链表无相交可能,无需进入核心逻辑);// 1. 链表节点定义(题目已给,无需手写,了解即可)
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}// 2. 核心解题方法
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 边界条件:任一链表为空,直接无相交
if (headA == null || headB == null) {
return null;
}// 初始化双指针,分别指向两链表头 ListNode pA = headA; ListNode pB = headB; // 循环条件:指针未相遇(相遇则返回,包括均为null的情况) while (pA != pB) { // 指针不为空则后移,为空则跳转到另一链表头(补全路程差) pA = (pA != null) ? pA.next : headB; pB = (pB != null) ? pB.next : headA; } // 最终:pA == pB,要么是相交节点,要么是null return pA; }}
4、代码关键细节
-
循环条件:判断
pA == pB(指针本身相等),绝对不能用.next判断 → 否则不相交场景会触发无限循环; -
指针跳转逻辑:必须是「为空时才跳转」,而非"走完自己链表就跳转"。比如pA遍历完A链表(到null)后再跳B,而非没到null就跳,避免漏走A的最后一个节点,导致路程计算错误。
-
避免无限循环误区:无需担心循环无法终止。因为两指针总路程固定为m+n,最多遍历m+n步后,要么相遇(相交节点),要么同时为null(不相交),必然触发pA==pB的终止条件。
-
链表有环的边界:该解法仅适用于「无环链表」。若链表可能有环,需先判断链表是否有环、找到环入口,再结合长度差逻辑判断相交,直接套用此模板会出错。
5、复杂度分析(面试必说)
-
时间复杂度:O(m + n),最坏情况需遍历两链表各一次(总路程m+n);
-
空间复杂度:O(1),仅用2个指针变量,无额外空间开销(最优解)。
6、典型场景验证(快速理解)
场景1:相交(长度不等)
A:A1→A2→C3→C4(m=4);B:B1→C3→C4(n=3);公共节点C3→C4
指针移动路径:
pA:A1→A2→C3→C4→null→B1→C3(第7步相遇C3);
pB:B1→C3→C4→null→A1→A2→C3(第7步相遇C3);
总路程:4+3=7,刚好在公共节点入口相遇。
场景2:不相交(长度不等)
A:A1→A2→A3(m=3);B:B1→B2(n=2);无公共节点
指针移动路径:
pA:A1→A2→A3→null→B1→B2→null(第6步);
pB:B1→B2→null→A1→A2→A3→null(第6步);
总路程:3+2=5?实际移动6步(到null),两指针同时为null,返回null。
场景3:相交/不相交(长度相等)
-
相交:同步移动直接在公共节点相遇;
-
不相交:同步移动同时到null,返回null。
7.面试答题话术
"我采用双指针解法,核心逻辑是让两个指针走相同总路程m+n,抵消长度差:
-
初始化双指针分别指向两链表头,边界条件:任一为空则无相交;
-
指针不为空则后移,为空则跳转到另一链表头,循环直到两指针相等;
-
最终两指针要么指向相交节点,要么均为null(不相交)。
该解法时间O(m+n)、空间O(1),是最优解,且逻辑能覆盖所有场景。"
题目 2:反转链表(双指针解法)(难度:简单)
1、题目核心定义
- 反转定义:将链表的「节点指向关系完全逆序」(原头节点变为尾节点,原尾节点变为头节点,所有节点的
next指针指向原前驱节点); - 输入:无环单链表的头节点
head; - 输出:反转后的链表头节点(原链表的尾节点)。
2、双指针解法核心逻辑(底层原理)
核心思想:用 3 个指针(前一个、当前、临时)遍历链表,逐个反转节点的指向,通过 "先保存后续节点→再反转当前指向→后移指针" 的步骤,实现线性时间内的原地反转。
两种最终场景:
- 场景 1(链表非空):遍历完所有节点后,
pre指针指向原尾节点(反转后的头节点); - 场景 2(链表为空 / 只有 1 个节点):直接返回原头节点(无需反转)。
3、标准模板代码(Java 版,面试直接写)
// 1. 链表节点定义(题目已给,无需手写,了解即可)
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
// 2. 核心解题方法
public class Solution {
public ListNode reverseList(ListNode head) {
// 边界条件:空链表或只有1个节点,直接返回head
if (head == null || head.next == null) {
return head;
}
// 初始化3个指针:pre(前一个节点)、cur(当前节点)、temp(临时保存后续节点)
ListNode pre = null;
ListNode cur = head;
ListNode temp = null;
// 循环条件:cur != null(遍历所有节点,直到cur走到null)
while (cur != null) {
temp = cur.next; // 1. 保存后续节点,避免断链
cur.next = pre; // 2. 反转当前节点的指向(指向pre)
pre = cur; // 3. pre后移,指向当前节点
cur = temp; // 4. cur后移,指向保存的后续节点
}
// 循环结束,pre是反转后的头节点(原尾节点)
return pre;
}
}
4、代码关键细节
- 循环条件:必须是
cur != null(而非cur.next != null)→ 若用cur.next != null,会漏掉最后一个节点的反转,导致链表断链; - 指针操作顺序:严格遵循 「保存 temp→反转 cur.next→后移 pre→后移 cur」→ 若先反转再保存 temp,会丢失后续节点,链表直接断链;
- 边界条件处理:必须先判断「空链表 / 只有 1 个节点」→ 避免无意义的遍历,同时覆盖特殊输入场景;
- 无环约束:该解法仅适用于「无环单链表」→ 若链表有环,需先处理环的逻辑,直接套用会陷入无限循环。
5、复杂度分析(面试必说)
-
时间复杂度:
O(n)n是链表的节点总数;- 只需遍历链表一次 (
while循环执行n次),每个节点仅做 4 步简单赋值操作,时间复杂度为线性的O(n); - 最好 / 最坏情况复杂度一致(必须遍历所有节点才能完成反转)。
-
空间复杂度:
O(1)- 仅使用
pre、cur、temp3 个指针变量,均为 "指向节点的引用",未新建任何ListNode实例; - 不管链表长度如何,空间开销固定为 3 个指针(常数级),是空间最优解(对比递归解法
O(n)的栈开销)。
- 仅使用
6、典型场景验证(快速理解)
以链表1→2→3→4→5为例,指针移动路径:
| 循环次数 | pre | cur | temp | 操作后链表状态 |
|---|---|---|---|---|
| 初始 | null | 1 | null | 1→2→3→4→5 |
| 1 | null | 1 | 2 | 1(null) ← 2→3→4→5 |
| 2 | 1 | 2 | 3 | 1←2 ← 3→4→5 |
| 3 | 2 | 3 | 4 | 1←2←3 ← 4→5 |
| 4 | 3 | 4 | 5 | 1←2←3←4 ← 5 |
| 5 | 4 | 5 | null | 1←2←3←4←5(null) |
| 循环结束 | 5 | null | null | 返回 pre=5(反转后头节点) |
7、面试答题话术
"我采用双指针迭代法 反转链表,核心逻辑是用 3 个指针逐个反转节点指向:
- 先处理边界条件:空链表或只有 1 个节点直接返回;
- 初始化 pre(前节点)、cur(当前节点)、temp(临时保存后续节点),遍历链表时先保存后续节点,再反转当前指向,最后后移指针;
- 循环条件是 cur != null,确保遍历所有节点;
- 最终 pre 就是反转后的头节点。
该解法时间复杂度 O (n)、空间复杂度 O (1),是原地反转的最优解,同时覆盖所有无环单链表场景。"
题目3:回文链表(双指针找中点 + 反转法)(难度:简单)
1、题目核心定义
- 回文定义:链表正向遍历与反向遍历的元素序列完全一致;
- 输入:无环单链表的头节点 head;
- 输出:布尔值(true = 是回文,false = 不是回文)。
2、核心逻辑(底层原理)
核心思想:"找中点→反转后半段→比较两段→恢复原链表"
- 双指针找中点:slow 走 1 步、fast 走 2 步,fast 到终点时 slow 指向链表中间节点;
- 反转后半段:以 slow.next 为头,反转后半段链表;
- 比较两段:前半段(head 开始)与反转后的后半段逐一比较值;
- 恢复原链表:将反转的后半段再反转回去(避免修改原链表)。
两种关键场景:
- 场景 1(链表长度为偶):slow 指向 "前半段最后一个节点",后半段长度 = 前半段;
- 场景 2(链表长度为奇):slow 指向 "中间节点",后半段长度 = 前半段 - 1(中间节点不影响回文)。
3、标准模板代码(Java 版,面试直接写)
// 1. 链表节点定义(题目已给,了解即可)
class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
next = null;
}
}
// 2. 核心解题方法
public class Solution {
public boolean isPalindrome(ListNode head) {
// 边界条件:空链表/只有1个节点,直接是回文
if (head == null || head.next == null) {
return true;
}
// 步骤1:双指针找中间节点(slow最终指向中间)
ListNode slow = head;
ListNode fast = head;
// 循环条件:必须先判fast.next≠null,再判fast.next.next(避免空指针)
while (fast.next != null && fast.next.next != null) {
slow = slow.next; // slow走1步
fast = fast.next.next; // fast走2步
}
// 步骤2:反转后半段链表(后半段头是slow.next)
ListNode secondHalfHead = reverseList(slow.next);
// 步骤3:比较前半段与反转后的后半段
ListNode p1 = head; // 前半段指针
ListNode p2 = secondHalfHead; // 反转后半段指针
boolean isPalin = true; // 回文标记
// 循环条件:p2≠null即可(后半段长度≤前半段)
while (isPalin && p2 != null) {
if (p1.val != p2.val) {
isPalin = false;
}
p1 = p1.next;
p2 = p2.next;
}
// 步骤4:恢复原链表(将后半段反转回去)
slow.next = reverseList(secondHalfHead);
return isPalin;
}
// 【复用反转链表模板】双指针法反转链表
private ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode pre = null;
ListNode cur = head;
ListNode temp = null;
while (cur != null) { // 循环条件必须是cur≠null(覆盖所有节点)
temp = cur.next; // 1. 保存后续节点
cur.next = pre; // 2. 反转当前指向
pre = cur; // 3. pre后移
cur = temp; // 4. cur后移
}
return pre; // pre是反转后的头节点
}
}
4、代码关键细节
- 找中点的循环条件:必须是
fast.next != null && fast.next.next != null- 先判
fast.next再判fast.next.next,避免空指针; - 若用
fast != null会导致 slow 多走一步,中点错误。
- 先判
- 反转链表的循环条件:必须是
cur != null,否则会漏掉最后一个节点的反转。 - 恢复原链表:必须将反转的后半段再反转回去(避免修改原链表的结构)。
- 比较的循环条件:只需判
p2 != null(后半段长度≤前半段,p2 走完则比较完成)。
5、复杂度分析(面试必说)
- 时间复杂度:O (n)
- 找中点:O (n/2);反转后半段:O (n/2);比较两段:O (n/2);恢复链表:O (n/2);
- 总操作数为线性级,时间复杂度 O (n)。
- 空间复杂度:O (1)
- 仅使用 slow、fast、p1、p2 等有限指针变量,未额外开辟空间,是空间最优解。
6、典型场景验证(快速理解)
以链表1→2→3→2→1为例:
- 找中点:slow 最终指向
3,后半段是2→1; - 反转后半段:
2→1变为1→2; - 比较:前半段
1→2→3与反转后半段1→2,值一致; - 恢复原链表:将
1→2反转回2→1,原链表结构不变。
7、面试答题话术
"我采用双指针找中点 + 后半段反转的方法判断回文链表:
- 先处理边界条件:空链表或只有 1 个节点直接返回 true;
- 用 slow/fast 双指针找中点,循环条件要先判 fast.next 再判 fast.next.next 避免空指针;
- 反转后半段链表(复用双指针反转模板),然后比较前半段与反转后的后半段;
- 最后将后半段反转回原状态,保证不修改原链表。该方法时间复杂度 O (n)、空间复杂度 O (1),是空间最优的回文链表判断方法。"
题目4:环形链表(双指针)
1.核心定义
- 环形链表:单链表中某个节点的
next指针指向链表中已存在的节点 (而非null),形成闭环; - 核心问题:
- 判断链表是否存在环;
- 输入:无环 / 有环单链表的头节点
head; - 输出:基础版返回布尔值(
true= 有环,false= 无环)
2、核心逻辑(底层原理)
2.1 基础版(判环):龟兔赛跑算法
核心思想:快慢指针遍历链表,有环则必相遇,无环则快指针先到终点:
- 慢指针(
slow):每次走 1 步; - 快指针(
fast):每次走 2 步; - 关键规则:指针比较的是内存地址(而非节点值),地址相同 = 指向同一个节点 = 有环。
3、标准模板代码
3.1 基础版(仅判环)
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
// 边界条件:空链表/只有1个节点,无环
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head;
// 循环条件:快指针能走2步(防空指针)
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针走1步
fast = fast.next.next; // 快指针走2步
// 地址相同=相遇=有环
if (slow == fast) {
return true;
}
}
// 退出循环=快指针到终点=无环
return false;
}
}
4、代码关键细节
4.1 空指针防护核心
fast != null && fast.next != null 是双层防护:
fast != null:防止fast为null时执行fast.next报空指针;fast.next != null:防止fast.next为null时执行fast.next.next报空指针;
- 只要满足其中一个不成立,说明快指针到终点,链表无环。
4.2 易踩坑点
- 初始值错误:若
slow/fast都初始化为head,需先移动指针再判断相遇(避免初始相等跳过循环); - 比较对象错误:必须比较
slow == fast(地址),而非slow.val == fast.val(值);
5、复杂度分析
| 场景 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 基础版(判环) | O(n) | O(1) | 最多遍历链表 1 次 |
6、典型场景验证
6.1 有环链表(1→2→3→4→2)
- 基础版:快慢指针相遇在 3(地址相同),返回
true;
6.2 无环链表(1→2→3→null)
- 基础版:快指针走到 3 后,
fast.next=null,退出循环返回false;
7、面试答题话术
基础版(判环)
"我采用快慢指针(龟兔赛跑)算法判断环:慢指针走 1 步、快指针走 2 步,若链表有环,快慢指针必相遇(地址相同);若无环,快指针会先到终点(fast/fast.next 为 null)。该方法时间复杂度 O (n)、空间复杂度 O (1),是最优解。"
总结
- 环形链表核心:快慢指针的地址比较是判环关键,值比较无意义;
- 空指针防护:
fast != null && fast.next != null是遍历安全的核心; - 功能区分:基础版仅判环,进阶版需记录相遇节点 + 同速指针找入口;
- 性能优势:快慢指针法是环形链表问题的最优解(空间 O (1)),优于哈希表法(空间 O (n))。
题目5:环形链表(寻找入环节点)
1.核心定义
- 核心问题:在单链表中判断是否存在环,若存在则返回环的入口节点 ,无环则返回
null; - 输入:单链表头节点
head(可能无环 / 有环); - 输出:环的入口节点(无环返回
null); - 关键规则:指针比较的是节点内存地址 (而非
val值),地址相同 = 指向同一个节点。
2、核心逻辑(底层原理)
2.1 核心思想(一步到位版)
将 "判环" 和 "找入口" 逻辑嵌套,利用快慢指针的双重特性:
- 判环阶段:慢指针(
slow)走 1 步、快指针(fast)走 2 步,有环则必相遇(地址相同); - 找入口阶段:相遇后将慢指针重置到链表头 ,快慢指针同速走 1 步,再次相遇时即为环入口(数学推导:链表头到入口距离 = 相遇点绕环到入口距离)。
2.2 数学推导(简化版)
设:链表头到入口距离 a,入口到相遇点距离 b,环长度 L。
- 慢指针路程:
a + b; - 快指针路程:
a + b + n*L(n为绕环圈数); - 由
2*(a+b) = a+b+n*L得:a = n*L - b→ 头指针走a步 = 相遇点指针走n*L - b步(即绕环到入口)。
3、标准模板代码(简洁版)
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
// 边界条件:空链表直接无环
if (head == null) {
return null;
}
ListNode slow = head; // 慢指针(龟)
ListNode fast = head; // 快指针(兔)
// 外层循环:快慢指针判环(防空指针)
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针走1步
fast = fast.next.next; // 快指针走2步
// 相遇=有环,进入找入口逻辑
if (slow == fast) {
slow = head; // 慢指针重置到链表头
// 嵌套循环:同速走,相遇即入口
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow; // 返回环入口节点
}
}
// 退出外层循环=无环,返回null
return null;
}
}
4、代码关键细节
4.1 空指针防护
fast != null && fast.next != null 是双层防护:
fast != null:防止fast为null时执行fast.next报空指针;fast.next != null:防止fast.next为null时执行fast.next.next报空指针;
- 只要满足一个不成立,说明快指针到终点,链表无环。
4.2 核心优化点(简洁版专属)
- 无中间变量:省去
meetNode,相遇后直接复用slow/fast指针,代码更精简; - 逻辑嵌套:判环和找入口合并在一个循环中,无冗余分支;
- 指针重置:相遇后必须将
slow重置为head,且嵌套循环中快慢指针均走 1 步(核心规则)。
4.3 易踩坑点
- 初始值:
slow/fast均初始化为head,需先移动指针再判断相遇(避免初始相等跳过循环); - 比较对象:必须用
slow == fast(地址),而非slow.val == fast.val(值); - 边界条件:
head == null即可覆盖空链表场景,无需额外判断head.next == null(外层循环已处理)。
5、复杂度分析4
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 判环 + 找入口总遍历≤2n 次 |
| 空间复杂度 | O(1) | 仅使用 2 个指针,无额外空间 |
6、典型场景验证
6.1 有环链表(1→2→3→4→2)
- 判环阶段:快慢指针相遇在 4(地址相同);
- 找入口阶段:
slow重置为 1,同速走后相遇在 2(环入口),返回节点 2。
6.2 无环链表(1→2→3→null)
- 外层循环:快指针走到 3 后,
fast.next=null,退出循环返回null。
7、面试答题话术
"我采用快慢指针的紧凑版写法解决环形链表找入口问题:
- 先通过快慢指针(慢 1 步、快 2 步)判环,若相遇则说明有环;
- 相遇后将慢指针重置到链表头,快慢指针同速走 1 步,再次相遇的节点即为环入口;
- 该方法通过数学推导保证逻辑正确性,时间复杂度 O (n)、空间复杂度 O (1),是最优解,且代码精简无冗余。"
总结
- 简洁版核心优势:代码短、无中间变量、逻辑紧凑,是面试最优写法;
- 核心规则不变:判环靠快慢指针相遇(地址比较),找入口靠 "头指针 + 相遇点指针同速走";
- 关键细节:空指针防护、指针重置、同速走 1 步,是避免错误的核心;
- 性能等价:与拆分版相比,执行效率和复杂度完全一致,仅代码结构更精简。