🔥你好我是fengxin_rou这是我的个人主页 fengxin_rou的主页
❄️欢迎查看我的专栏我的专栏
《Java后端学习》、《JAVASE基础》、《JUC并发》、《redis》、《JVM虚拟机》、《MYSQL》、《黑马点评》、《rabbitmq》、《JavaWeb+AI的talis学习系统》、《苍穹外卖》

🎯 本文目标:深入剖析三道经典链表题目,掌握链表操作的核心技巧,提升算法面试通过率。
目录
[1. 单向链表结构](#1. 单向链表结构)
[2. 链表操作的关键点](#2. 链表操作的关键点)
[3. 复杂度分析基础](#3. 复杂度分析基础)
[题目一:删除链表的倒数第N个结点(LeetCode 19)](#题目一:删除链表的倒数第N个结点(LeetCode 19))
[题目二:两两交换链表中的节点(LeetCode 24)](#题目二:两两交换链表中的节点(LeetCode 24))
[题目三:随机链表的复制(LeetCode 138)](#题目三:随机链表的复制(LeetCode 138))
[1. 哑节点的使用](#1. 哑节点的使用)
[2. 双指针技巧](#2. 双指针技巧)
[3. 哈希表辅助](#3. 哈希表辅助)
[4. 递归与迭代的选择](#4. 递归与迭代的选择)
[1. 学习路径建议](#1. 学习路径建议)
[2. 推荐刷题顺序](#2. 推荐刷题顺序)
[3. 常见错误与避免](#3. 常见错误与避免)
引言
链表作为数据结构的基础,在算法面试中占据着举足轻重的地位。与数组相比,链表具有动态大小、插入删除高效等优点,但也带来了指针操作复杂、边界情况多等挑战。本文将聚焦三道LeetCode经典链表题目:
- 删除链表的倒数第N个结点(LeetCode 19)
- 两两交换链表中的节点(LeetCode 24)
- 随机链表的复制(LeetCode 138)
这三道题目涵盖了链表操作的三大核心技巧:双指针定位 、节点交换 和结构复制。掌握它们不仅能解决具体问题,更能建立起解决链表问题的通用思维框架。
链表基础概念回顾
在深入具体题目之前,让我们先回顾链表的基本概念:
1. 单向链表结构
public class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
2. 链表操作的关键点
- 指针操作:修改节点间的连接关系
- 边界处理:空链表、单节点、头尾节点特殊情况
- 哑节点技巧:简化头节点处理逻辑
- 双指针技巧:快慢指针、前后指针
3. 复杂度分析基础
- 时间复杂度:通常为O(n),单次遍历
- 空间复杂度:原地操作O(1),使用辅助结构O(n)
题目一:删除链表的倒数第N个结点(LeetCode 19)
题目描述
给你一个链表,删除链表的倒数第n个结点,并且返回链表的头结点。
示例1:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
示例2:
输入:head = [1], n = 1
输出:[]
解题思路:双指针法
核心思想
使用两个指针right和left,让right先移动n步,然后两个指针同时移动,当right到达末尾时,left正好在倒数第n个节点的前一个位置。

图1:删除链表倒数第N个节点算法流程图
为什么需要哑节点?
如果没有哑节点,当删除头节点时(如head = [1], n = 1),需要特殊处理。使用哑节点可以统一处理所有情况。
算法步骤
- 创建哑节点
dummy,指向head right指针从dummy开始移动n+1步left指针从dummy开始移动right和left同时移动,直到right为nullleft.next就是要删除的节点,修改指针跳过它- 返回
dummy.next
代码实现与解析
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 创建哑节点,简化头节点处理
ListNode dummy = new ListNode(0, head);
// right指针先移动n+1步
ListNode right = dummy;
for (int i = 0; i <= n; i++) {
right = right.next;
}
// left指针从dummy开始
ListNode left = dummy;
// 同时移动,直到right到达末尾
while (right != null) {
left = left.next;
right = right.next;
}
// 删除倒数第n个节点
left.next = left.next.next;
return dummy.next;
}
}
代码详解
- 第5行 :
ListNode dummy = new ListNode(0, head);创建哑节点,值为0,next指向原头节点 - 第8-10行 :
right指针移动n+1步,确保当right为null时,left指向待删除节点的前一个位置 - 第13-17行 :双指针同步移动,保持间距为
n+1 - 第20行:关键操作 - 跳过待删除节点
复杂度分析
- 时间复杂度:O(L),其中L是链表长度,只需一次遍历
- 空间复杂度:O(1),只使用了常数个指针变量
边界情况处理
- 删除头节点 :
head = [1], n = 1,哑节点完美解决 - 删除尾节点 :
head = [1,2], n = 1,正常流程 - n等于链表长度:删除头节点,同样适用
题目二:两两交换链表中的节点(LeetCode 24)
题目描述
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例2:
输入:head = []
输出:[]
解题思路:迭代法
核心思想
使用三个指针node0、node1、node2,每次交换一对节点。关键是要保持与前一组的连接。

图2:两两交换链表节点算法流程图
指针定义
node0:当前交换对的前一个节点(初始为dummy)node1:当前交换对的第一个节点(初始为head)node2:当前交换对的第二个节点(node1.next)node3:下一组的第一个节点(node2.next)
交换过程
交换前:node0 -> node1 -> node2 -> node3
交换后:node0 -> node2 -> node1 -> node3
算法步骤
- 创建哑节点
dummy,指向head node0 = dummy,node1 = head- 当
node1和node1.next都不为null时:node2 = node1.nextnode3 = node2.next- 执行交换:
node0.next = node2,node2.next = node1,node1.next = node3 - 更新指针:
node0 = node1,node1 = node3
- 返回
dummy.next
代码实现与解析
class Solution {
public ListNode swapPairs(ListNode head) {
// 创建哑节点
ListNode dummy = new ListNode(0, head);
// node0指向哑节点,node1指向头节点
ListNode node0 = dummy;
ListNode node1 = head;
// 当node1和node1.next都不为空时继续交换
while (node1 != null && node1.next != null) {
ListNode node2 = node1.next;
ListNode node3 = node2.next;
// 执行交换:0->2, 2->1, 1->3
node0.next = node2;
node2.next = node1;
node1.next = node3;
// 为下一轮交换做准备
node0 = node1;
node1 = node3;
}
return dummy.next;
}
}
代码详解
- 第5行:哑节点处理空链表和单节点情况
- 第11行:循环条件确保有足够的节点进行交换
- 第15-17行:交换操作的关键三步
- 第20-21行:指针更新,准备下一轮交换
复杂度分析
- 时间复杂度:O(n),遍历整个链表
- 空间复杂度:O(1),只使用常数个指针
递归解法对比
class Solution {
public ListNode swapPairs(ListNode head) {
// 递归终止条件
if (head == null || head.next == null) {
return head;
}
// 保存第二个节点
ListNode second = head.next;
// 递归处理剩余部分
head.next = swapPairs(second.next);
// 交换当前两个节点
second.next = head;
return second;
}
}
递归特点:
- 代码更简洁,逻辑清晰
- 但空间复杂度为O(n)(递归栈)
- 实际面试中可能要求迭代解法
题目三:随机链表的复制(LeetCode 138)
题目描述
给你一个长度为n的链表,每个节点包含一个额外增加的随机指针random,该指针可以指向链表中的任何节点或空节点。
构造这个链表的深拷贝。深拷贝应该正好由n个全新节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的next指针和random指针也都应指向复制链表中的新节点。
示例1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
解题思路:哈希表映射法
核心思想
使用哈希表建立原节点到新节点的映射关系,然后通过映射关系构建next和random指针。

图3:随机链表复制算法流程图
两步构建法
- 第一步:遍历原链表,创建所有新节点,建立映射关系
- 第二步:再次遍历原链表,通过映射关系设置新节点的next和random
为什么需要两步?
如果一步完成,当设置某个节点的random时,它指向的节点可能还没有被创建。
代码实现与解析
class Solution {
public Node copyRandomList(Node head) {
if (head == null) return null;
// 第一步:建立映射关系
HashMap<Node, Node> map = new HashMap<>();
Node cur = head;
while (cur != null) {
map.put(cur, new Node(cur.val));
cur = cur.next;
}
// 第二步:构建next和random指针
cur = head;
while (cur != null) {
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
}
代码详解
- 第5行:空链表直接返回
- 第8-12行:第一次遍历,创建所有新节点,建立映射
- 第15-19行:第二次遍历,通过映射设置next和random
- 第16行 :
map.get(cur.next)当cur.next为null时返回null,完美处理边界 - 第17行 :同理,
map.get(cur.random)处理random为null的情况
复杂度分析
- 时间复杂度:O(n),两次遍历
- 空间复杂度:O(n),哈希表存储映射关系
原地修改法(O(1)空间)
更高级的解法是在原链表中插入新节点,然后分离:
class Solution {
public Node copyRandomList(Node head) {
if (head == null) return null;
// 第一步:在原节点后插入新节点
Node cur = head;
while (cur != null) {
Node newNode = new Node(cur.val);
newNode.next = cur.next;
cur.next = newNode;
cur = newNode.next;
}
// 第二步:设置新节点的random
cur = head;
while (cur != null) {
if (cur.random != null) {
cur.next.random = cur.random.next;
}
cur = cur.next.next;
}
// 第三步:分离两个链表
Node newHead = head.next;
cur = head;
while (cur != null) {
Node temp = cur.next;
cur.next = temp.next;
if (temp.next != null) {
temp.next = temp.next.next;
}
cur = cur.next;
}
return newHead;
}
}
空间复杂度:O(1),原地操作
链表操作通用技巧总结
1. 哑节点的使用
作用:简化头节点处理,避免特殊判断
适用场景:
- 删除头节点的题目(如题目1)
- 需要返回新头节点的题目(如题目2)
代码模式:
ListNode dummy = new ListNode(0, head);
// ... 操作 ...
return dummy.next;
2. 双指针技巧
快慢指针 :检测环、找中点
前后指针:保持固定间距(如题目1)
代码模式:
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
3. 哈希表辅助
作用:建立节点映射关系,简化复杂指针操作
适用场景:
- 需要记住节点对应关系的题目(如题目3)
- 需要随机访问节点的题目
4. 递归与迭代的选择
| 特性 | 递归 | 迭代 |
|---|---|---|
| 代码简洁度 | 更简洁 | 稍复杂 |
| 空间复杂度 | O(n)递归栈 | O(1) |
| 实际性能 | 可能栈溢出 | 更稳定 |
| 面试要求 | 通常都可 | 可能要求迭代 |
实战演练:如何系统学习链表问题
1. 学习路径建议
- 基础阶段:掌握链表基本操作(增删改查)
- 技巧阶段:学习哑节点、双指针、哈希表等技巧
- 题目阶段:按难度刷题,先易后难
- 总结阶段:归纳解题模板,形成思维框架
2. 推荐刷题顺序
- 简单题:设计链表、合并两个有序链表
- 中等题:本文三道题、环形链表、相交链表
- 困难题:合并K个升序链表、反转链表II
3. 常见错误与避免
- 指针丢失:操作前保存下一个节点
- 空指针异常:检查节点是否为null
- 循环引用:注意修改指针的顺序
- 边界遗漏:空链表、单节点、头尾节点
总结与最佳实践
核心收获
- 题目1:掌握了双指针定位倒数第k个节点的技巧
- 题目2:学会了节点交换的标准操作流程
- 题目3:理解了复杂链表复制的哈希表映射方法
最佳实践
- 画图辅助:复杂操作一定要画图理解
- 分步实现:将复杂问题拆解为简单步骤
- 边界检查:始终考虑空链表、单节点等特殊情况
- 复杂度分析:养成分析时间和空间复杂度的习惯
面试技巧
- 先沟通:理解题意,确认输入输出
- 再设计:阐述思路,分析复杂度
- 后编码:写出清晰、有注释的代码
- 最后测试:用示例和边界情况测试
常见问题解答(FAQ)
Q1:为什么删除倒数第N个节点要用哑节点?
A:因为可能删除头节点,哑节点可以统一处理所有情况,避免特殊判断。
Q2:两两交换节点时,如何保持与前面节点的连接?
A :使用node0指针始终指向当前交换对的前一个节点,每次更新node0 = node1。
Q3:随机链表复制时,为什么不能一步到位?
A :因为设置random指针时,目标节点可能还未创建。两步法确保所有节点都已创建。
Q4:递归解法什么时候会栈溢出?
A:当链表很长时(如10000个节点),递归深度达到10000,可能导致栈溢出。迭代解法更安全。
Q5:如何快速判断链表问题应该用什么技巧?
A:
- 删除节点 → 考虑哑节点 + 双指针
- 交换节点 → 考虑迭代法 + 指针操作
- 复制结构 → 考虑哈希表映射
