链表核心技巧
链表核心技巧
链表是面试高频考点,本文通过可视化演示和经典题目,帮你彻底掌握链表的核心操作。
一、链表节点可视化
在开始刷题之前,我们先建立直觉——用彩色方块模拟链表节点的变化过程。
1.1 基本链表结构
每个节点包含 val(值)和 next(指向下一节点的指针)。Java 中定义:
public class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}1.2 常用颜色约定(本文全篇统一)
二、核心操作详解
2.1 反转链表
反转链表是所有链表技巧的基础,两种写法必须烂熟于心。
迭代思路: 用三个指针 prev、cur、next,逐个把 next 指针掉头。
递归思路: 假设后半段已经反转好,只处理当前节点与下一节点的关系。
操作流程如下图:
2.2 合并两个有序链表
使用哑节点(dummy node)简化边界处理,双指针比较两个链表的头节点,小的接入结果链表。
2.3 环检测(Floyd 判圈算法)
快指针每次走 2 步,慢指针每次走 1 步。若有环,两者必然相遇。
2.4 找环入口(LC 142)
相遇后,将其中一个指针重置到 head,两者同步前进,再次相遇即为环入口。数学原理:设链表头到环入口距离为 a,相遇点到环入口距离为 b,则 a = c - b(c 为环长),两指针同速必在入口相遇。
三、LeetCode 精选题
LC 206 — 反转链表
题目: 给你单链表的头节点 head,请你反转链表,并返回反转后的链表。
步骤演进可视化
Java 代码
// 迭代解法
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next; // 保存下一节点
cur.next = prev; // 反转指针
prev = cur; // prev 前进
cur = next; // cur 前进
}
return prev; // prev 即为新头节点
}
// 递归解法
public ListNode reverseListRecursive(ListNode head) {
if (head == null || head.next == null) return head;
ListNode newHead = reverseListRecursive(head.next); // 递归反转后半段
head.next.next = head; // 让下一节点指回当前节点
head.next = null; // 断开当前节点的原指针
return newHead;
}复杂度: 时间 O(n),空间 O(1)(迭代)/ O(n)(递归调用栈)
LC 21 — 合并两个有序链表
题目: 将两个升序链表合并为一个新的升序链表。
双指针可视化
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
cur.next = l1;
l1 = l1.next;
} else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
cur.next = (l1 != null) ? l1 : l2; // 接上剩余部分
return dummy.next;
}LC 141 / 142 — 环形链表
LC 141: 判断链表是否有环。
LC 142: 找到环的入口节点。
快慢指针可视化
// LC 141:是否有环
public boolean hasCycle(ListNode head) {
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;
}
// LC 142:找环入口
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) {
// 相遇后,将 fast 重置到 head,同速前进
fast = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow; // 即为环入口
}
}
return null;
}关键公式: 设 head 到环入口距离为
a,相遇点到入口距离为b(沿环方向),环长为c。
相遇时:2(a+b) = a + b + kc,化简得a = kc - b。
所以从 head 和相遇点同速出发,恰好在环入口会合。
LC 160 — 相交链表
题目: 找出两条链表的相交起始节点。
双指针图解
两个指针 pA、pB 分别从两链表头出发,走到末尾后切换到另一链表头继续走。两者路径总长相同,必在交点或 null 处相遇。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = (pA != null) ? pA.next : headB;
pB = (pB != null) ? pB.next : headA;
}
return pA; // 相交节点,或 null(不相交)
}妙处:
pA走完 A 后走 B,pB走完 B 后走 A,总路径均为lenA + lenB,在交点处步数一致。
LC 25 — K 个一组翻转链表(进阶)
题目: 每 K 个节点为一组进行翻转,不足 K 个的保留原顺序。
分组可视化
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode pre = dummy;
while (true) {
// 检查剩余节点是否够 k 个
ListNode check = pre;
for (int i = 0; i < k; i++) {
check = check.next;
if (check == null) return dummy.next; // 不足 k 个,直接返回
}
// 翻转 [pre.next, check] 这 k 个节点
ListNode tail = pre.next; // 翻转后 tail 成为该组末尾
ListNode cur = pre.next;
ListNode prev = null;
for (int i = 0; i < k; i++) {
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
// 重新连接
pre.next = prev; // 连接组头
tail.next = cur; // 连接下一组
pre = tail; // pre 移到当前组末尾
}
}四、总结与模板
| 技巧 | 适用场景 | 核心要点 |
|---|---|---|
| 哑节点(dummy) | 头节点可能改变 | dummy.next 为结果头 |
| 快慢指针 | 找中点、检测环、找倒数第K | 速度比 2:1 |
| 双指针交替 | 链表相交、对齐长度 | 走完切换到另一链表 |
| 递归 | 反转、合并、K组翻转 | 明确返回值含义 |
| 前后指针 | 删除节点、K步间隔 | 先让 fast 走 K 步 |
万能调试模板
// 打印链表(调试用)
void printList(ListNode head) {
StringBuilder sb = new StringBuilder();
while (head != null) {
sb.append(head.val).append(" -> ");
head = head.next;
}
sb.append("null");
System.out.println(sb);
}想要更系统的算法提升?
如果你觉得本文有帮助,欢迎关注我的知识星球,里面有:
- 300+ 道 LeetCode 题解(含图解 + 多语言代码)
- 大厂面试真题汇总与解析
- 每周直播算法讲解,答疑互动
- 专属刷题打卡群,一起坚持
扫码加入,一起搞定算法面试!
