链表类力扣刷题总结
目录
3. 删除链表的倒数第 N 个节点(LeetCode 19)
(六)困难拓展类:合并 K 个升序链表(LeetCode 23)
一、链表基础原理
链表是一种线性数据结构,由 ** 节点(Node)** 组成,每个节点包含两部分:
- 数据域:存储节点的值(如
int val)。- 指针域:存储下一个节点的引用(如
ListNode next)。
链表的分类
- 单链表:每个节点只有一个指向下一节点的指针,尾节点指向
null。- 双链表:每个节点有两个指针(
prev指向前一节点,next指向后一节点)。- 循环链表:尾节点指向头节点,形成 “环”。
链表的特性
- 插入 / 删除高效:已知位置时,时间复杂度为
O(1)(仅需修改指针)。- 访问低效:访问第
k个节点需遍历,时间复杂度O(n)。
二、核心解题方法
1. 双指针法
- 快慢指针:快指针速度是慢指针的 2 倍(或其他倍数),用于:
- 找链表中点(如回文链表、排序链表的归并)。
- 判断环形链表(相遇则有环)。
- 找环形链表的入环点(相遇后,一个指针回
head,同速移动)。
- 前后指针:两个指针一前一后,用于:
- 反转链表(
prev和curr配合修改指针方向)。 - 删除倒数第 N 个节点(快指针先走
N步,再一起走)。
- 反转链表(
2. 递归法
递归的本质是将大问题拆解为相同逻辑的子问题,适用于:
- 反转链表(反转剩余部分,再连接当前节点)。
- 合并有序链表(比较头节点,递归合并剩余部分)。
3. 哈希表法
用于记录节点的 “访问状态” 或 “映射关系”,适用于:
- 判断环形链表(记录已访问节点,重复则有环)。
- 相交链表(记录 A 链表节点,遍历 B 时判断是否存在)。
- 随机链表的复制(记录原节点到新节点的映射,处理
random指针)。
4. 哑节点(虚拟头节点)
当链表的头节点操作需要特殊处理时(如合并链表时头节点不确定、删除头节点),创建一个 val 无意义的节点作为头节点的前驱,简化逻辑。通常是dummy~
三、经典题型分类与代码实现
(一)基础操作类
1. 反转链表(LeetCode 206)
问题:反转单链表(如 1->2->3->null → 3->2->1->null)。
方法:双指针或递归。
注:此处的null也可以是其他节点;用于反转部分节点
// 双指针法
public ListNode reverseList(ListNode head) {
// prev通常是链表尾的指向
ListNode prev = null;
// 当前节点,后面逐渐后移,至null
ListNode curr = head;
while (curr != null) {
//保存当前节点的下一个节点,避免后续当前节点的移动
ListNode nextTemp = curr.next;
//将当前指向prev表尾
curr.next = prev;
// prev指针左移/前移
prev = curr;
//当前指针后移,进行下一轮交换
curr = nextTemp;
}
//最终prev从最右端移到了最左端,成为了头节点
return prev;
}
// 递归法
public ListNode reverseList(ListNode head) {
// 递归终止条件
if (head == null || head.next == null) return head;
//1->2->3->4->null
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
/**
递归处理 “剩余部分”
ListNode newHead = reverseList(head.next);
以链表 1->2->3->4->null 为例,递归调用会不断深入:
第一次调用:head=1,处理 head.next=2 → 进入下一层递归。
第二次调用:head=2,处理 head.next=3 → 进入下一层递归。
第三次调用:head=3,处理 head.next=4 → 进入下一层递归。
第四次调用:head=4,满足 head.next == null → 触发终止条件,返回 4(此时 newHead=4)。
步骤 3:调整 “当前节点” 与 “后继” 的指针
head.next.next = head;
head.next = null;
回到第三次调用(head=3,newHead=4):
head.next 是 4,所以 head.next.next = head → 等价于 4.next = 3,此时链表变为 3<->4。
head.next = null → 断开 3->4 的旧连接,最终 3 的 next 为 null,链表变为 4->3->null。
返回 newHead=4 → 回到第二次调用。
回到第二次调用(head=2,newHead=4):
head.next 是 3,所以 3.next = 2 → 链表变为 4->3->2。
head.next = null → 断开 2->3 的旧连接,2 的 next 为 null,链表变为 4->3->2->null。
返回 newHead=4 → 回到第一次调用。
回到第一次调用(head=1,newHead=4):
head.next 是 2,所以 2.next = 1 → 链表变为 4->3->2->1。
head.next = null → 断开 1->2 的旧连接,1 的 next 为 null,链表变为 4->3->2->1->null。
返回 newHead=4 → 最终整个链表完成反转。
**/
2. 合并两个有序链表(LeetCode 21)
问题:将两个升序链表合并为一个升序链表。
方法:双指针 + 哑节点。
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 设置哑节点,因为两个头节点都要被处理
ListNode dummy = new ListNode(0);
// 设置当前节点,后续不断后移
ListNode prev = dummy;
// 当两个链表都不为空时继续合并
while (list1 != null && list2 != null) {
// 将值更小的节点优先加入,同时移动结果链表和当前链表
if (list1.val <= list2.val) {
// 实现连接
prev.next = list1;
list1 = list1.next;
} else {
prev.next = list2;
list2 = list2.next;
}
prev = prev.next;
}
// 哪个表空了就直接连上,注意是在循环结束后
prev.next = list1 != null ? list1 : list2;
// 返回哑节点的next,刚好是头节点
return dummy.next;
}
3. 删除链表的倒数第 N 个节点(LeetCode 19)
问题:删除链表的倒数第 n 个节点(如 1->2->3->4->5,n=2 → 1->2->3->5)。
方法:快慢指针 + 哑节点(快指针先走 n 步,再一起走,找到待删除节点的前驱)。
public ListNode removeNthFromEnd(ListNode head, int n) {
// 哑节点构造的简化写法,和 “先 new 哑节点再设置dummy.next = head” 效果完全一致
// ListNode dummy = new ListNode(0); dummy.next = head;
ListNode dummy = new ListNode(0, head);
// 初始化,都先在哑节点启动
ListNode fast = dummy, slow = dummy;
// 快节点先走n+1步(此处如果是i<n就说多走n步,while循环条件需要改为fast.next!=null)
for (int i = 0; i <= n; i++) fast = fast.next;
while (fast != null) {
// 两个指针同时移动,最终fast到达null,slow比fast落后n+1个位置,在需要删除的节点前驱
fast = fast.next;
slow = slow.next;
}
// 此处对第n个节点进行删除
slow.next = slow.next.next;
//返回头节点
return dummy.next;
}
(二)环与相交类
1. 环形链表(LeetCode 141)
问题:判断链表是否有环。
方法:快慢指针(快指针走 2 步,慢指针走 1 步,相遇则有环)。
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) return false;
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;
}
注:哈希表法也可以解决
public ListNode removeNthFromEnd(ListNode head, int n) {
if (head == null) {
return null;
}
// 第一步:遍历链表,用Map记录每个节点的索引和节点本身
Map<Integer, ListNode> indexToNode = new HashMap<>();
ListNode current = head;
int length = 0; // 链表总长度
while (current != null) {
indexToNode.put(length, current); // 索引从0开始
current = current.next;
length++;
}
// 第二步:计算要删除的节点的索引(倒数第n个 = 正数第 length - n 个)
int deleteIndex = length - n;
// 特殊情况:删除的是头节点(索引0)
if (deleteIndex == 0) {
return head.next;
}
// 第三步:找到目标节点的前一个节点,删除目标节点
ListNode prevNode = indexToNode.get(deleteIndex - 1); // 前一个节点的索引
prevNode.next = prevNode.next.next; // 跳过目标节点
return head;
2. 环形链表 II(LeetCode 142)
问题:返回环形链表的入环第一个节点。
方法:快慢指针相遇后,一个指针回 head,同速移动,相遇处为入环点。
public ListNode detectCycle(ListNode head) {
if (head == null || head.next == null) return null;
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { // 相遇,存在环
ListNode ptr = head;
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr; // 入环点
}
}
return null;
}
3. 相交链表(LeetCode 160)
问题:找到两个单链表相交的起始节点(不相交返回 null)。
方法:双指针(A 走完走 B,B 走完走 A,相遇则相交)。
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
(三)回文与对称类
回文链表(LeetCode 234)
问题:判断链表是否为回文(如 1->2->2->1 是回文,1->2 不是)。
方法:找中点 + 反转后半段 + 比较。
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) return true;
// 找中点
ListNode slow = head, fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
// 反转后半段
ListNode reversedHalf = reverseList(slow.next);
// 比较前半段和反转后的后半段
ListNode p1 = head, p2 = reversedHalf;
while (p2 != null) {
if (p1.val != p2.val) return false;
p1 = p1.next;
p2 = p2.next;
}
return true;
}
private ListNode reverseList(ListNode head) {
ListNode prev = null, curr = head;
while (curr != null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
(四)排序与复杂重组类
1. 排序链表(LeetCode 148)
问题:对链表进行升序排序。
方法:归并排序(找中点 + 递归合并)。
public ListNode sortList(ListNode head) {
if (head == null || head.next == null) return head;
// 找中点
ListNode slow = head, fast = head.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode mid = slow.next;
slow.next = null; // 拆分链表
// 递归排序左右两部分
ListNode left = sortList(head);
ListNode right = sortList(mid);
// 合并
ListNode dummy = new ListNode(0);
ListNode prev = dummy;
while (left != null && right != null) {
if (left.val <= right.val) {
prev.next = left;
left = left.next;
} else {
prev.next = right;
right = right.next;
}
prev = prev.next;
}
prev.next = left != null ? left : right;
return dummy.next;
}
2. K 个一组翻转链表(LeetCode 25)
问题:每 k 个节点一组进行翻转(如 1->2->3->4->5,k=2 → 2->1->4->3->5)。
方法:分组处理,每组内部反转,再拼接。
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0, head);
ListNode prev = dummy;
while (true) {
// 检查剩余节点是否足够k个
ListNode tail = prev;
for (int i = 0; i < k; i++) {
tail = tail.next;
if (tail == null) return dummy.next;
}
ListNode nextGroup = tail.next;
// 反转当前k个节点
ListNode[] reversed = reverse(prev.next, tail);
ListNode newHead = reversed[0], newTail = reversed[1];
// 拼接
prev.next = newHead;
newTail.next = nextGroup;
prev = newTail;
}
}
// 反转从head到tail的链表,返回新的头和尾
private ListNode[] reverse(ListNode head, ListNode tail) {
ListNode prev = tail.next, curr = head;
while (prev != tail) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return new ListNode[]{tail, head};
}
3. 随机链表的复制(LeetCode 138)
问题:复制一个带有 random 指针的链表(random 可能指向任意节点或 null)。
方法:哈希表记录原节点到新节点的映射,先复制 next 指针,再处理 random 指针。
public Node copyRandomList(Node head) {
if (head == null) return null;
Map<Node, Node> map = new HashMap<>();
Node curr = head;
// 复制节点,存储映射
while (curr != null) {
map.put(curr, new Node(curr.val));
curr = curr.next;
}
curr = head;
// 处理next和random指针
while (curr != null) {
map.get(curr).next = map.get(curr.next);
map.get(curr).random = map.get(curr.random);
curr = curr.next;
}
return map.get(head);
}
(五)数值计算类
两数相加(LeetCode 2)
问题:两个非空链表表示两个非负整数(每位数字逆序存储),求它们的和。
方法:模拟加法,注意进位,哑节点存储结果。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
int carry = 0;
while (l1 != null || l2 != null || carry != 0) {
int sum = carry;
if (l1 != null) {
sum += l1.val;
l1 = l1.next;
}
if (l2 != null) {
sum += l2.val;
l2 = l2.next;
}
carry = sum / 10;
curr.next = new ListNode(sum % 10);
curr = curr.next;
}
return dummy.next;
}
(六)困难拓展类:合并 K 个升序链表(LeetCode 23)
问题:合并 k 个升序链表,返回合并后的升序链表。
方法:优先队列(最小堆)或分治合并。
// 方法1:优先队列(最小堆)
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
PriorityQueue<ListNode> pq = new PriorityQueue<>((a, b) -> a.val - b.val);
for (ListNode list : lists) {
if (list != null) pq.offer(list);
}
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while (!pq.isEmpty()) {
ListNode node = pq.poll();
curr.next = node;
curr = curr.next;
if (node.next != null) pq.offer(node.next);
}
return dummy.next;
}
// 方法2:分治合并
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) return null;
return merge(lists, 0, lists.length - 1);
}
private ListNode merge(ListNode[] lists, int left, int right) {
if (left == right) return lists[left];
int mid = left + (right - left) / 2;
ListNode l1 = merge(lists, left, mid);
ListNode l2 = merge(lists, mid + 1, right);
return mergeTwoLists(l1, l2);
}
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode prev = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
prev = prev.next;
}
prev.next = l1 != null ? l1 : l2;
return dummy.next;
}
总结
链表题的核心在于指针的灵活操作。需掌握 “双指针、递归、哈希表、哑节点” 这四类方法,将大部分题型拆解为 “模板化” 的逻辑。
- 环和相交问题:快慢指针的 “相遇逻辑”。
- 反转和合并:双指针 + 哑节点的 “拼接模板”。
- 复杂重组(如 K 个一组、排序):将问题拆分为 “子问题”(递归或分组),再逐一解决。

更多推荐


所有评论(0)