目录
[二、题目 1:基础反转链表(LeetCode 206)](#二、题目 1:基础反转链表(LeetCode 206))
[三、题目 2:局部反转链表 II(LeetCode 92)](#三、题目 2:局部反转链表 II(LeetCode 92))
[四、题目 3:K 个一组反转链表(LeetCode 25)](#四、题目 3:K 个一组反转链表(LeetCode 25))
链表反转是算法面试的高频考点,但很多人会被 "指针指向" 绕晕 ------ 其实只要用 "盒子、标签、纸条" 的比喻统一理论,不管是基础反转、局部反转还是 K 个一组反转,本质都是同一套逻辑。
一、核心理论:盒子、标签、纸条
链表的所有操作,都可以拆解为这三个角色的互动,记住它们的定位,反转问题就通了:
| 角色 | 本质 | 操作规则 |
|---|---|---|
| 盒子(节点) | 链表的 "实体" | 数量、内容全程不变(比如1、2、3这几个盒子),是所有操作的 "素材"; |
| 标签(指针) | 操作的 "工具" | 只负责 "贴在" 不同盒子上,移动标签≠修改盒子,只是换了操作目标; |
| 纸条(next) | 链表的 "连接关系" | 链表的顺序由纸条指向决定,反转的本质就是修改纸条的方向,不是修改盒子本身; |
盒子不变,标签挪,纸条改了链就变。
或者我们也可以把链表想象成放在地上的一排快递盒子。
-
盒子 (Object/Node):
-
这就是
new ListNode()创建出来的东西。 -
它一旦放在地上,位置通常是不动的(我们在脑海里不需要搬动盒子的物理位置,只需要改变它们的关系)。
-
-
标签 (Reference/Variable):
-
比如
head,curr,prev,next。 -
这是一张张贴纸。
-
curr = head的意思不是复制盒子,而是把写着curr的标签,贴到了和head标签所在的同一个盒子上。 -
重要原则:移动标签,不会影响盒子之间的连接关系。
-
-
绳子 (Next Pointer):
-
这就是
node.next。 -
每个盒子手里攥着一根绳子,拴着下一个盒子。
-
链表翻转的本质:就是把这根绳子剪断,拴到另一个盒子上(通常是后面那个盒子拴回前面那个)。
-
二、题目 1:基础反转链表(LeetCode 206)
题目要求
给你单链表的头节点head,反转整个链表,返回反转后的头节点。示例:输入1→2→3→4→5,输出5→4→3→2→1
解题思想
用两个标签(prev、cur)遍历所有盒子,逐个修改每个盒子的纸条方向(从 "指向下一个盒子" 改成 "指向前一个盒子")。
过程拆解
(示例:1→2→3→4→5)
-
初始状态:
prev标签:贴在null(无盒子);cur标签:贴在盒子 1;- 所有盒子的纸条:
1→2→3→4→5。
-
循环改纸条(共 5 轮):每轮做 3 件事:
- 暂存
cur盒子的纸条(避免丢失下一个盒子); - 把
cur盒子的纸条改成 "指向prev当前贴的盒子"; - 移动
prev和cur标签到下一个目标。
以第 1 轮(处理盒子 1)为例:
- 暂存盒子 1 的纸条(指向盒子 2);
- 盒子 1 的纸条→指向
prev(null); prev标签贴到盒子 1,cur标签贴到盒子 2。
- 暂存
-
循环结束:
cur标签贴到null(遍历完所有盒子);prev标签贴到盒子 5(反转后的新头)。
代码
java
public ListNode reverseList(ListNode head) {
// prev标签:初始贴null
ListNode prev = null;
// cur标签:初始贴头盒子
ListNode cur = head;
while (cur != null) {
// 暂存cur盒子的纸条(避免丢盒子)
ListNode nextBox = cur.next;
// 改cur盒子的纸条:指向prev当前贴的盒子
cur.next = prev;
// prev标签挪到cur当前的盒子
prev = cur;
// cur标签挪到暂存的下一个盒子
cur = nextBox;
}
// prev最终贴在新头盒子上
return prev;
}
三、题目 2:局部反转链表 II(LeetCode 92)
题目要求
给你链表头head和两个整数left、right,反转left到right位置的盒子,返回原链表。示例:输入1→2→3→4→5, left=2, right=4,输出1→4→3→2→5
解题思想
分 3 步:
- 用
p0标签定位 "反转区间的前一个盒子"(锚点); - 在区间内改盒子的纸条方向;
- 衔接 "区间前的盒子" 和 "反转后的区间"。
过程拆解
(示例:left=2,right=4)
-
定位锚点标签 p0:
- 初始
p0标签贴在哑节点盒子(val=0,纸条指向盒子 1); - 循环
left-1次(2-1=1 次),p0标签挪到盒子 1(反转区间的前一个盒子)。
- 初始
-
反转区间内的纸条(盒子 2、3、4):
prev标签贴null,cur标签贴盒子 2(p0的纸条指向);- 循环
right-left+1次(4-2+1=3 次),逐个改盒子 2、3、4 的纸条:- 盒子 2 的纸条→指向
null; - 盒子 3 的纸条→指向盒子 2;
- 盒子 4 的纸条→指向盒子 3。
- 盒子 2 的纸条→指向
-
衔接链表:
- 盒子 2 的纸条→指向盒子 5(原区间后的第一个盒子);
- 盒子 1 的纸条→指向盒子 4(反转后的区间头)。
代码(带理论注释)
java
public ListNode reverseBetween(ListNode head, int left, int right) {
// 哑节点盒子:避免头节点的特殊处理
ListNode dummy = new ListNode(0, head);
// p0标签:初始贴哑节点盒子
ListNode p0 = dummy;
// 定位p0到反转区间的前一个盒子
int count = left;
while (count > 1) {
p0 = p0.next; // p0标签挪到下一个盒子
count--;
}
// prev标签:初始贴null;cur标签:贴反转区间的第一个盒子
ListNode prev = null;
ListNode cur = p0.next;
// 反转区间内的盒子纸条
for (int i = 0; i < right - left + 1; i++) {
ListNode nextBox = cur.next; // 暂存cur的纸条
cur.next = prev; // 改cur的纸条指向prev
prev = cur; // prev标签挪到cur
cur = nextBox; // cur标签挪到暂存盒子
}
// 衔接:反转区间的新尾(原头)连后面的盒子
p0.next.next = cur;
// 衔接:p0连反转区间的新头
p0.next = prev;
return dummy.next;
}
四、题目 3:K 个一组反转链表(LeetCode 25)
题目要求
每k个盒子一组反转,不足k个则保持原顺序,返回修改后的链表。示例:输入1→2→3→4→5, k=2,输出2→1→4→3→5
解题思想
分 3 步循环执行:
- 分组:用
tail标签找到当前组的尾盒子(不足k个则终止); - 反转组内:改组内盒子的纸条方向;
- 衔接:当前组的前锚点连反转后的组头,组尾连下一组头。
过程拆解
(示例:k=2)
-
初始状态:
cur标签贴在哑节点盒子(纸条指向盒子 1)。
-
第 1 组(盒子 1、2):
- 找 tail:
cur标签挪 2 次,找到盒子 2(组尾); - 反转组内:盒子 1、2 的纸条→改成
2→1; - 衔接:哑节点的纸条→指向盒子 2,盒子 1 的纸条→指向盒子 3;
cur标签挪到盒子 1(下一组的前锚点)。
- 找 tail:
-
第 2 组(盒子 3、4):
- 找 tail:
cur标签挪 2 次,找到盒子 4(组尾); - 反转组内:盒子 3、4 的纸条→改成
4→3; - 衔接:盒子 1 的纸条→指向盒子 4,盒子 3 的纸条→指向盒子 5;
cur标签挪到盒子 3。
- 找 tail:
-
剩余盒子不足 k 个(盒子 5):
- 终止循环,返回结果。
代码(带理论注释)
java
public ListNode reverseKGroup(ListNode head, int k) {
// 哑节点盒子:统一组头的处理
ListNode dummy = new ListNode(0);
dummy.next = head;
// cur标签:初始贴哑节点(作为组的前锚点)
ListNode cur = dummy;
while (cur.next != null) {
// 找当前组的尾盒子tail
ListNode tail = cur;
for (int i = 0; i < k; i++) {
tail = tail.next;
if (tail == null) { // 不足k个,直接返回
return dummy.next;
}
}
// 暂存下一组的头盒子
ListNode nextGroupHead = tail.next;
// 反转当前组的盒子纸条,返回{新头, 新尾}
ListNode[] reversed = reverse(cur.next, tail);
ListNode newHead = reversed[0];
ListNode newTail = reversed[1];
// 衔接:前锚点连组新头,组新尾连下一组头
cur.next = newHead;
newTail.next = nextGroupHead;
// cur标签挪到组新尾(作为下一组的前锚点)
cur = newTail;
}
return dummy.next;
}
// 反转"head到tail"的盒子纸条,返回{新头, 新尾}
private ListNode[] reverse(ListNode head, ListNode tail) {
// prev标签:初始贴tail的下一个盒子(组外锚点)
ListNode prev = tail.next;
// cur标签:初始贴组头盒子
ListNode curr = head;
// 当prev追上tail,说明组内纸条改完了
while (prev != tail) {
ListNode nextBox = curr.next; // 暂存cur的纸条
curr.next = prev; // 改cur的纸条指向prev
prev = curr; // prev标签挪到cur
curr = nextBox; // cur标签挪到暂存盒子
}
// 反转后,原尾是新头,原头是新尾
return new ListNode[]{tail, head};
}
五、总结
不管是基础、局部还是 K 组反转,核心逻辑完全一致:
- 盒子(节点)始终是原实体,从不新增 / 删除;
- 用标签(指针)定位目标盒子,移动标签只是换操作对象;
- 反转的本质是修改盒子里的纸条(next)方向,链表的顺序由纸条决定。
