本文将详细讲解 LeetCode 中两道经典的链表节点交换问题:

  • 第 24 题:两两交换链表中的节点
  • 第 25 题:K 个一组翻转链表

这两道题都属于链表操作的经典题目,考察了对链表指针操作的掌握程度,以及递归或迭代思想的应用。

24. 两两交换链表中的节点

题目描述

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例:

1
2
输入:head = [1,2,3,4]
输出:[2,1,4,3]

提示:

  • 链表中节点的数目在范围 [0, 100]
  • 0 <= Node.val <= 100

解题思路

这道题的最优解法是使用 迭代 方法,通过维护几个关键指针来实现两两节点的交换。

核心思路:

  1. 使用一个虚拟头节点(dummy node)简化边界情况处理
  2. 维护一个当前指针 curr,用于遍历链表
  3. 每次交换 curr 后面的两个节点
  4. 更新指针位置,继续下一轮交换

算法步骤:

  1. 创建虚拟头节点 dummy,使其指向原链表头
  2. 初始化当前指针 curr 指向 dummy
  3. curr 后面有至少两个节点时:
    • 保存要交换的两个节点 node1node2
    • 执行交换操作:node1.next = node2.nextnode2.next = node1
    • 更新 curr.next 指向交换后的新头节点 node2
    • curr 移动到 node1,准备下一轮交换
  4. 返回虚拟头节点的下一个节点

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Definition for singly-linked list.
* 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; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null) {
return null;
}
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode curr = dummy;
while (curr != null && curr.next != null && curr.next.next != null) {
ListNode node1 = curr.next, node2 = curr.next.next;
node1.next = node2.next;
node2.next = node1;
curr.next = node2;
curr = node1;
}
return dummy.next;
}
}

代码讲解

  1. 边界处理:如果链表为空,直接返回 null
  2. 创建虚拟头节点dummy 节点的 next 指向原链表头,用于简化边界情况
  3. 初始化当前指针curr 指向 dummy,作为当前交换位置的前一个节点
  4. 循环条件:当 curr 后面有至少两个节点时,继续交换
  5. 保存节点node1 是当前要交换的第一个节点,node2 是第二个节点
  6. 交换操作
    • node1.next = node2.next:将 node1 指向 node2 原来的下一个节点
    • node2.next = node1:将 node2 指向 node1,完成两个节点的交换
  7. 更新指针
    • curr.next = node2:将当前指针的下一个节点指向交换后的新头节点 node2
    • curr = node1:将当前指针移动到 node1,准备下一轮交换
  8. 返回结果:返回虚拟头节点的下一个节点,即交换后的链表头

复杂度分析

  • 时间复杂度:O(n),其中 n 是链表的长度。需要遍历链表一次
  • 空间复杂度:O(1),只使用了常数级别的额外空间

25. K 个一组翻转链表

题目描述

给你链表的头节点 head,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例:

1
2
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]

提示:

  • 链表中的节点数目在范围 [0, 5000]
  • 0 <= Node.val <= 1000
  • 1 <= k <= 5000

解题思路

这道题是第 24 题的升级版,需要以 k 个节点为一组进行翻转。同样可以使用 迭代 方法来解决。

核心思路:

  1. 使用虚拟头节点简化边界情况
  2. 维护三个指针:prev(当前组的前一个节点)、curr(当前节点)、succ(当前组的后一个节点)
  3. 先检查剩余节点是否足够 k
  4. 如果足够,对当前组的 k 个节点进行翻转
  5. 更新指针位置,继续下一组翻转

算法步骤:

  1. 创建虚拟头节点 dummy,使其指向原链表头
  2. 初始化 prev 指向 dummycurr 指向原链表头
  3. 循环处理链表:
    • 检查剩余节点是否足够 k
    • 如果不足,跳出循环
    • 如果足够,找到当前组的尾节点和下一组的头节点
    • 翻转当前组的 k 个节点
    • 更新指针位置,连接翻转后的链表
  4. 返回虚拟头节点的下一个节点

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Definition for singly-linked list.
* 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; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null) {
return null;
}
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode prev = dummy, curr = head;
while (curr!= null) {
ListNode succ = prev;
int cnt = 0;
while (cnt < k) {
if (succ.next == null) {
break;
}
succ = succ.next;
cnt++;
}
if (cnt != k) {
break;
}
succ = succ.next;

ListNode start = prev;
ListNode end = prev.next;
prev = prev.next;
curr = curr.next;
while (curr != succ) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
start.next = prev;
end.next = succ;
prev = end;
curr = prev.next;
}
return dummy.next;
}
}

代码讲解

  1. 边界处理:如果链表为空,直接返回 null
  2. 创建虚拟头节点dummy 节点的 next 指向原链表头
  3. 初始化指针prev 指向 dummycurr 指向原链表头
  4. 检查剩余节点
    • 使用 succ 指针从 prev 开始移动 k
    • 如果移动过程中遇到 null,说明剩余节点不足 k 个,跳出循环
  5. 记录位置
    • succ 指向当前组的下一个节点
    • start 是当前组的前一个节点
    • end 是当前组翻转前的头节点(翻转后的尾节点)
  6. 翻转当前组
    • 使用 prevcurr 指针进行翻转
    • 每次将 curr 的下一个节点指向 prev,然后更新 prevcurr
  7. 连接链表
    • start.next = prev:将当前组的前一个节点指向翻转后的头节点
    • end.next = succ:将翻转后的尾节点指向原当前组的下一个节点
  8. 更新指针
    • prev = end:将 prev 指向翻转后的尾节点
    • curr = prev.next:将 curr 指向下一组的头节点
  9. 返回结果:返回虚拟头节点的下一个节点

复杂度分析

  • 时间复杂度:O(n),其中 n 是链表的长度。每个节点最多被访问两次
  • 空间复杂度:O(1),只使用了常数级别的额外空间

总结

这两道题都是链表操作的经典题目,考察了对链表指针的熟练掌握程度。它们的共同特点是:

  1. 使用 虚拟头节点 简化边界情况处理
  2. 维护多个指针来跟踪当前操作的位置
  3. 进行节点的实际交换,而不是仅仅改变节点的值
  4. 保持指针的正确指向,避免链表断裂