链表
面试手撕专用
java
import java.util.*;
// 链表节点(面试必须自己写)
class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
}
public class Main { // 注意:ACM 类名必须是 Main!!!
public static void main(String[] args) {
// 1. 输入
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) nums[i] = sc.nextInt();
// 2. 构建链表
ListNode head = buildList(nums);
// 3. 调用你的算法
//ListNode newHead = sortList(head);
// 4. 输出结果(自测看结果)
printList(newHead);
}
// 构建链表
public static ListNode buildList(int[] nums) {
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
for (int num : nums) {
cur.next = new ListNode(num);
cur = cur.next;
}
return dummy.next;
}
// 打印链表(自测用)
public static void printList(ListNode head) {
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
}
}
2. 两数相加
注意分别处理 【相同数位上的两数之和 val1 + val2,并加上上一轮新产生的进位值 carry:sum = val1 + val2 + carry】 与 【这一轮新产生的进位值 carry = carry / 10】。
并且当两链表 l1 和 l2 都遍历完后,记得额外处理最后的一次进位。例如:99+9=108,这里需要单独处理百位最后的1。
一句话理解:
- 链表是倒着存 的:
2→4→3代表 342 - 按位相加,处理进位,最后输出新链表
核心思路
- 同时遍历两个链表,对应位相加 + 进位
- 每一位结果:
sum % 10 - 新进位:
sum / 10 - 链表走完但还有进位,要多补一个节点
注意:
- 用ret 虚拟头节点方便返回结果
- 循环条件:
**l1不空 || l2不空 || 有进位** - 每一位求和、算进位、建节点
- 最后返回
**ret.next**
java
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
if(l1==null) return l2;
if(l2==null) return l1;
int next=0,sum=0;
ListNode ret=new ListNode(-1);
ListNode cur=ret;
//加完为止(避免单独处理进位)
while(l1!=null||l2!=null||next!=0){
//不为空
int a=l1==null?0:l1.val;
int b=l2==null?0:l2.val;
sum=next+a+b;
//加节点
ListNode node=new ListNode(sum%10);
next=sum/10;
cur.next=node;
cur=cur.next;
//不为空,才下一位
if(l1!=null) l1=l1.next;
if(l2!=null) l2=l2.next;
}
return ret.next;
}
}
24. 两两交换链表中的节点
非递归版
不能只交换值,必须改指针!
我们用 虚拟头节点 dummy,让所有节点处理方式统一。
假设当前结构:dummy -> 1 -> 2 -> 3 -> 4
要交换 1 和 2,只需要 3 步:
dummy.next = 21.next = 2.next2.next = 1
然后把 dummy 移动到下一组的前一个节点(即 1),继续循环。
思路:
cur 指前
first 是第一个
second 是第二个
cur 指向 second
first 指向 second 下一个
second 指向 first
cur 跳到 first
java
class Solution {
public ListNode swapPairs(ListNode head) {
//0个或者1个反转不了
if(head==null||head.next==null) return head;
//虚拟头,更方便
ListNode dummy=new ListNode(-1);
dummy.next=head;
ListNode cur=dummy;
//有两个节点,才能交换
while(cur.next!=null&&cur.next.next!=null){
//dummy->first->second
ListNode first=cur.next;
ListNode second=cur.next.next;
//交换
first.next=second.next;
second.next=first;
cur.next=second;
//下一个: dummy->second->first
//原来的第一个 就是后面一个了
cur=first;
}
return dummy.next;
}
}
递归版
java
class Solution {
public ListNode swapPairs(ListNode head) {
//0个或者1个反转不了
if(head==null||head.next==null) return head;
ListNode dummy=new ListNode(-1);
dummy.next=head;
ListNode cur=dummy;
while(cur.next!=null&&cur.next.next!=null){
//node1->node2
ListNode node1=cur.next; //第一个
ListNode node2=cur.next.next;//第二个
//交换后 node2->node1
cur.next=node2;
node1.next=node2.next;
node2.next=node1;
cur=node1; //下一个cur(node1已经交换到后面一个去了)
}
return dummy.next;
}
}
分类1
876. 链表的中间结点
给你一个链表,返回中间结点
- 偶数个结点:返回第二个中间结点
- 奇数个结点:返回正中间
例子:
1->2->3->4->5` → 返回 `3
1->2->3->4->5->6` → 返回 `4
java
class Solution {
//快慢指针
public ListNode middleNode1(ListNode head) {
ListNode slow=head;
ListNode fast=head;
//多走一步
while(fast!=null&&fast.next!=null){
slow=slow.next; //走一步
fast=fast.next.next; //走两步
}
return slow;
}
}
206. 反转链表
非递归版本:

🧠 核心思路(就 3 步)
你只要记住一句话:每个节点的 next 指向前一个,从头到尾改一遍,最后返回新头。
具体 3 步
prev = 前一个节点(一开始是 null)
cur = 当前节点(从 head 开始)
循环:
先保存下一个节点 next = cur.next
让当前节点指向前一个 curr.next = prev
prev 往前走(变成当前节点)
cur 往前走(变成刚才保存的 next)
java
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null) return null;
ListNode prev=null;
ListNode cur=head;
//当前需要反转的节点不为空,就继续
while(cur!=null){
ListNode next=cur.next; //记录下一个
cur.next=prev; //反转
//下一组
prev=cur;
cur=next;
}
//cur此时为空,prev是新头
return prev;
}
}
递归版本:
java
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null||head.next==null){
return head;
}
ListNode newHead=reverseList(head.next);
head.next.next=head;
head.next=null;
return newHead;
}
}
234. 回文链表(中点/反转)
核心思路
找中点:快慢指针找链表中点
反转后半段:把链表后半段反转
一一比较:前半段 和 反转后的后半段 逐个比对
全相同 = 回文
有不同 = 不是
为什么必须用 **while(right != null)** 判断,不能用 left?
核心:
**因为反转后的右半段,一定比左半段短(或相等)**用短的那边做循环判断,永远不会空指针!
\1. 先看链表结构
回文链表分两种:
① 偶数长度:1 → 2 → 2 → 1
中点是第 2 个节点
- 左半段:
1 → 2 - 右半段:
2 → 1左右一样长
② 奇数长度:1 → 2 → 3 → 2 → 1
中点是第 3 个节点
- 左半段:
1 → 2 → 3 - 右半段:
1 → 2右半段 更短!
java
class Solution {
public boolean isPalindrome(ListNode head) {
if(head==null) return false;
//找中点
ListNode slow=head;
ListNode fast=head;
while(fast!=null&&fast.next!=null){
slow=slow.next;
fast=fast.next.next;
}
//两个链表天然断开
//反转右半部分
ListNode right=reverse(slow);
//开始对比
ListNode left=head;
//使用右半部分判断,不会空指针异常
// 左半部分>=右(中点方法)
while(right!=null){
if(right.val!=left.val) return false;
right=right.next;
left=left.next;
}
return true;
}
public ListNode reverse(ListNode node){
if(node==null) return null;
ListNode cur=node,prev=null;
while(cur!=null){
ListNode next=cur.next;
cur.next=prev;
prev=cur;
cur=next;
}
return prev; //这个是新的头
}
}
143. 重排链表(中点/反转/交叉)
1. 找中点(快慢指针)
把链表切成前半段和后半段
slow 走一步
fast 走两步
最后 slow 停在前半段最后一个节点
2. 反转后半段
把后半段倒过来,方便从后往前取节点
3. 交叉合并
一个取前半,一个取后半,交替拼接
前半段:1 → 2 →
反转后半段:4 → 3 →
合并:1 → 4 → 2 → 3
疑惑?
为什么循环条件不一样?
① 找中点(876)
希望 fast 走到链表最后一位 这样 slow 才能走到正中间。
所以循环条件要允许 fast 走到最后(多走一点,条件不要太远 )**:**fast != null && fast.next != null
② 切链表(重排)
我们不希望 slow 走到中间而是希望 slow 停在左半段最后一个方便把链表切成两半。
**所以要提前一步停下:**fast.next != null && fast.next.next != null
java
class Solution {
public void reorderList(ListNode head) {
if(head==null) return;
ListNode slow=head;
ListNode fast=head;
//1.找中点
while(fast.next!=null&&fast.next.next!=null){
slow=slow.next; //走一步
fast=fast.next.next; //走两步
}
//slow在前半段最后一个点 下一个点是后半段的第一个点
//2.反转后半段
ListNode cur=slow.next,prev=null;
slow.next=null; //断开
while(cur!=null){ //当前需要反转的不为空
ListNode next=cur.next;
cur.next=prev;
prev=cur;
cur=next;
}
//prev就是反转后新的头
//3.交叉合并
ListNode l1=head;
ListNode l2=prev;
while(l2!=null){
//记录下一个需要交叉节点
ListNode n1=l1.next;
ListNode n2=l2.next;
//交叉
l1.next=l2;
l2.next=n1;
//下一个
l1=n1;
l2=n2;
}
}
}
92. 反转链表 II(反转指定区间)
核心思路:
找到区间 → 反转区间 → 拼接回去和 K 个一组反转逻辑几乎一样
java
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy=new ListNode(-1);
dummy.next=head;
ListNode prev=dummy; //记录前一个
//链表开始的上一个节点
for(int i=1;i<left;i++) prev=prev.next;
ListNode start=prev.next;//开始位置
ListNode end=prev; //记录前一个
for(int i=left;i<=right;i++) end=end.next;//结束位置
ListNode next=end.next; //记录后一个
//反转
ListNode cur=start,pre=null;
while(cur!=null){
ListNode ne=cur.next; //下一个需要反转的节点
cur.next=pre;
//下一组
pre=cur;
cur=ne;
}
//拼接前后(反转后pre变头,start变尾巴)
prev.next=pre;
start.next=next;
return dummy.next;
}
}
25. K 个一组翻转链表
🧠 核心思路(超级大白话)
- 把链表每 K 个分成一组
- 每组内部反转
- 反转完拼接回原链表
- 不够 K 个的不反转
完整步骤
- 创建虚拟头 dummy,方便接链表
- 用 prev 记录每组的前一个节点(上一组的尾巴)
- 检查够不够 K 个,不够就结束
- 反转这 K 个节点
- 把反转后的组拼接回去(拼前+拼后(记录后))
- 移动 prev(更新上一组尾巴),继续下一组
java
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy=new ListNode(-1);
dummy.next=head;//连接
ListNode prev=dummy; //上一个的尾巴(初始化是虚拟节点)
//一直下去
while(true){
//找到一组链表的头尾巴
ListNode start=prev.next; //上一个的尾巴就是下一个的开始
ListNode end=prev;
//找k个
for(int i=0;i<k;i++){
end=end.next;
if(end==null) return dummy.next; //不足k组
}
//反转
ListNode next=end.next; //记录下一组的开始
ListNode cur=start,pre=null;
//结束条件:不再是原来的null,而是当前需要反转的节点不是下一组的开始
while(cur!=next){
ListNode ne=cur.next;//记录下一个需要反转的
cur.next=pre; //指向前一个
//下一个
pre=cur;
cur=ne;
}
//pre就是反转后的头 start就是反转后的尾
prev.next=pre; //连接前面
start.next=next;//连接后面
prev=start;//给下一组记录上一组尾巴
}
}
}
分类2
203. 移除链表元素
1. 核心思路
链表删除节点的关键是找到「待删除节点的前驱节点」,但如果要删除的是头节点,没有前驱节点,处理起来会很麻烦。用虚拟头节点(dummy) 指向真实头节点,就能让「头节点」和「中间节点」的删除逻辑完全统一:
- 初始化虚拟头节点
dummy,让dummy.next = head; - 用
cur指针从dummy开始遍历(始终指向当前节点的前驱); - 若
cur.next.val == val,则删除cur.next(cur.next = curr.next.next); - 否则,
cur后移; - 最终返回
dummy.next(新的头节点)。
19. 删除链表的倒数第 N 个结点
-
注意 先创建虚拟头节点
dummy,且dummy.next = head。防止当链表头节点head为待删除节点时,删除该节点后链表头head为空的情况(边界情况) -
- 如果我们能得到倒数第n个节点的前驱节点而不是倒数第n个节点,那么删除操作会更加方便。因此我们可以考虑在初始时创建 快慢指针
fast和slow,并将这两个指针指向哑节点dummy,其余操作不变。这样一来,当fast遍历到链表末尾时,slow的下一个节点就是我们需要删除的节点。
- 如果我们能得到倒数第n个节点的前驱节点而不是倒数第n个节点,那么删除操作会更加方便。因此我们可以考虑在初始时创建 快慢指针
-
快指针先走n步,然后快指针和慢指针再每次各走一步
-
删除倒数第n个节点:
slow.Next = slow.Next.Next,注意不是 slow.Next = fast -
最后返回虚拟头节点的后继节点:dummy.Next
java
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head==null) return null;
ListNode dummy=new ListNode(-1);
dummy.next=head;
ListNode slow=dummy;
ListNode fast=dummy;
//快指针先走n步
for(int i=0;i<n;i++) fast=fast.next;
//再同时走(fast刚好走到尾,slow少走一个)
while(fast.next!=null){
fast=fast.next;
slow=slow.next;
}
//slow就是中点的上个。删除
slow.next=slow.next.next;
return dummy.next;
}
}
计算长度的方法
java
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head==null) return null;
int len=Len(head);
ListNode dummy=new ListNode(-1);
dummy.next=head;
//总共是len个,倒数第n个,就是正向第len-n
//所以找 len-n+1个,要删的前一个
ListNode cur=dummy;
for(int i=1;i<len-n+1;i++) cur=cur.next;
cur.next=cur.next.next;
return dummy.next;
}
public int Len(ListNode node){
int cnt=0;
while(node!=null){
node=node.next;
cnt++;
}
return cnt;
}
}
类似题目有:
分类3
21. 合并两个有序链表
两种方法:递归(不做演示)和 迭代 。
- dummy 虚拟头结点 → 避免空指针
- 循环条件 :
l1 != null && l2 != null - 最后直接接剩余链表,不用再遍历
java
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
//出现空链表
if(list1==null) return list2;
if(list2==null) return list1;
ListNode l1=list1,l2=list2;
ListNode dummy=new ListNode(-1);
ListNode cur=dummy; //负责拼接
//其中一个为空,就结束
while(l1!=null&&l2!=null){
int cnt1=l1.val;
int cnt2=l2.val;
//连接小的
if(cnt1<cnt2){
cur.next=l1;
l1=l1.next;
}else{
cur.next=l2;
l2=l2.next;
}
cur=cur.next; //下一个
}
//处理尾巴
cur.next=l1==null?l2:l1;
// if(l1==null) cur.next=l2;
// if(l2==null) cur.next=l1;
return dummy.next;
}
}
23. 合并 K 个升序链表(小堆)
最简单暴力但最优的方法:
- 把所有链表的头节点放进一个 ** 小根堆(优先队列)** 里
- 堆会自动把最小的节点放堆顶
- 每次取出最小的节点接到结果链表
- 取出后,如果这个节点后面还有节点,把它的 next 再放进堆里
- 重复到堆空为止
👉 一句话总结:用小根堆每次拿最小的拼接!
优点:
- 时间复杂度低:O(N logK)(N 总节点数,K 链表数)
- 代码短、逻辑简单
java
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists==null) return null;
//升序,小堆
PriorityQueue<ListNode> pq=new PriorityQueue<>((a,b)->a.val-b.val);
//插入每个链表头节点
for(ListNode node:lists) {
if(node!=null) pq.offer(node); //不为空才插入
}
ListNode dummy=new ListNode(-1);
ListNode cur=dummy;
//堆不为空,还有节点
while(pq.size()>0){
//获取堆顶,删掉
ListNode node=pq.poll();
cur.next=node;
cur=cur.next;
if(node.next!=null) //不为空才插入
pq.add(node.next); //插入它的下一个元素
}
return dummy.next;
}
}
分治思路
java
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return mergeKLists(lists, 0, lists.length);
}
// 合并从 lists[i] 到 lists[j-1] 的链表
private ListNode mergeKLists(ListNode[] lists, int i, int j) {
int m = j - i;
if (m == 0) {
return null; // 注意输入的 lists 可能是空的
}
if (m == 1) {
return lists[i]; // 无需合并,直接返回
}
ListNode left = mergeKLists(lists, i, i + m / 2); // 合并左半部分
ListNode right = mergeKLists(lists, i + m / 2, j); // 合并右半部分
return mergeTwoLists(left, right); // 最后把左半和右半合并
}
// 21. 合并两个有序链表
private ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(); // 用哨兵节点简化代码逻辑
ListNode cur = dummy; // cur 指向新链表的末尾
while (list1 != null && list2 != null) {
if (list1.val < list2.val) {
cur.next = list1; // 把 list1 加到新链表中
list1 = list1.next;
} else { // 注:相等的情况加哪个节点都是可以的
cur.next = list2; // 把 list2 加到新链表中
list2 = list2.next;
}
cur = cur.next;
}
cur.next = list1 != null ? list1 : list2; // 拼接剩余链表
return dummy.next;
}
}
148. 排序链表(中点分两份/合并)
排序链表(最优解法:归并排序)
核心思路(3 步走)
找中点(快慢指针),快指针,先一步!
切两半 (断开链表),slow 停在左边尾
递归排序左右 → 合并两个有序链表
java
class Solution {
public ListNode sortList(ListNode head) {
// (空 或 只有一个节点) 无需拆分
if(head==null||head.next==null) return head;
//找中点
ListNode slow=head;
ListNode fast=head.next;
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
}
//断开
ListNode head2=slow.next;
slow.next=null;
//递归排序分两半部分进行排序
ListNode left=sortList(head);
ListNode right=sortList(head2);
//合并两个有序链表
ListNode ret=mergeSortList(left,right);
return ret;
}
public ListNode mergeSortList(ListNode l1,ListNode l2){
ListNode dummy=new ListNode(-1);
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?l2:l1;
return dummy.next;
}
}
分类4
141. 环形链表(判环)
判断快慢指针是否相遇(快指针两步,慢指针一步)
快指针走两步,慢指针走一步
- 如果有环 :快慢指针一定会相遇
- 如果没环 :快指针会走到
null
一句话:相遇 = 有环,不相遇 = 没环
java
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow=head;
ListNode fast=head;
//如果没环,一定可以走到终点
while(fast!=null&&fast.next!=null){
fast=fast.next.next;
slow=slow.next;
//如果有环,一定在里面相遇
if(fast==slow) return true;
}
return false;
}
}
142. 环形链表 II(找环入口)
方法一:快慢指针(最优 O (1) 空间,面试首选)
核心思路(死记结论)
- 快指针走 2 步,慢指针走 1 步
- 相遇 = 有环
- 相遇后,慢指针回到头节点
- 两个指针都每次走 1 步
- 再次相遇的点 = 环的入口
java
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){
fast=fast.next.next; //走两步
slow=slow.next; //一步
//相遇之后
if(slow==fast){
slow=head; //会头
//快慢开始各一步
while(slow!=fast){
slow=slow.next;
fast=fast.next;
}
return slow; //返回相遇点
}
}
return null;
}
}
疑惑:为什么第二次相遇就是入口?
java
头节点 ----(a)----> 环入口 ----(b)----> 相遇点
↓ ↑
---------(c)-------
- a:头节点 → 环入口
- b:环入口 → 第一次相遇点
- c:第一次相遇点 → 环入口
- 环总长 = b + c
- 慢指针 slow **:每次走 1 步路程 =** a + b
- 快指针 fast:每次走 2 步路程 = *a + b + n(b+c)**(多绕了 n 圈)
【起点到入口的距离】 = 【相遇点到入口的距离】
java
2 × (a + b) = a + b + n × (b + c)
a + b = n × (b + c)
a + b = b + c //当n等于1时
a = c //所以第二次:起点到环入口距离=相遇点到环入口距离
哈希表的方法
方法二:HashSet(最简单,好理解)
- 遍历链表,把走过的节点都放进 HashSet
- 如果某个节点已经在 set 里
- → 这个节点就是环的入口
java
public class Solution {
//哈希表方法
public ListNode detectCycle(ListNode head) {
if(head==null) return null;
ListNode cur=head;
Set<ListNode> vis=new HashSet<>();
while(cur!=null){
//出现重复添加->入环点
if(vis.contains(cur)) return cur;
else vis.add(cur);//没有就添加
cur=cur.next;
}
return null;
}
}
160. 相交链表(找交点)
思路
cur1从 A 走,cur2从 B 走- 谁走到头,就换到另一条链表开头
- 两人走的路程一样:A + 交 + B = B + 交 + A
- 一定会在交点相遇
- 不相交就一起走到 null
java
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode cur1=headA;
ListNode cur2=headB;
//不相等,就继续走
//1.相遇 2.无交点走到尾巴,也相等null
while(cur1!=cur2){
//先同时走,走到尾,就从另外一个链表头开始走
if(cur1==null) cur1=headB;
else cur1=cur1.next;
if(cur2==null) cur2=headA;
else cur2=cur2.next;
}
//相遇就是交点 A+C+B=B+C+A
return cur1;
}
}