链表专题Ⅱ—反转基础

206. 反转链表

内容概览

题目 说明
带虚拟头节点的链表反转 通关
不带虚拟节点的链表反转 通关

题目

【LeetCode 206】:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

参考资料

链表3.透彻理解链表反转以及拓展问题
链表6:大厂如何考链表反转

【基础】建立虚拟头节点辅助反转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 带有虚拟节点的反转
public ListNode reverseList(ListNode head) {

// 接上虚拟节点解决问题
ListNode L = new ListNode(-1);
L.next = head;

// 断链操作,为头插法做铺垫
L.next = null;

// 当前指针 p
ListNode p = head;
while (p != null) {
// 后继指针
ListNode q = p.next; // 保存后继
p.next = L.next;
L.next = p;
p = q;
}
return L.next;
}

【推荐】直接操作链表实现反转

面试常考:直接操作指针进行反转

反转前后的结构和指针位置:

算法思想

  • cur 指向旧链表的首节点
  • prev 表示已经调整好的新链表的表头
  • next 表示下一个需要调整的节点
  • curprev 是两个表的表头,移动过程中 cur 经过一次中间状态后,又重新变成两个链表的表头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public ListNode reverseList(ListNode head) {

// pre 表示已经调整好的新链表的表头
ListNode prev = null;
// cur 指向旧链表的首节点
ListNode cur = head;

while (cur != null) {
// next 表示下一个需要调整的节点
ListNode next = cur.next;
// 反转逻辑
cur.next = prev;
prev = cur;
// 转向下一个待处理的节点
cur = next;
}
return prev;
}

递归反转

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
/**
* 以链表1->2->3->4->5举例
*
* @param head
* @return
*/
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
/*
直到当前节点的下一个节点为空时返回当前节点
由于5没有下一个节点了,所以此处返回节点5
*/
return head;
}
//递归传入下一个节点,目的是为了到达最后一个节点
ListNode newHead = reverseList(head.next);
/*
第一轮出栈,head为5,head.next为空,返回5
第二轮出栈,head为4,head.next为5,执行head.next.next=head也就是5.next=4,
把当前节点的子节点的子节点指向当前节点
此时链表为1->2->3->4<->5,由于4与5互相指向,所以此处要断开4.next=null
此时链表为1->2->3->4<-5
返回节点5
第三轮出栈,head为3,head.next为4,执行head.next.next=head也就是4.next=3,
此时链表为1->2->3<->4<-5,由于3与4互相指向,所以此处要断开3.next=null
此时链表为1->2->3<-4<-5
返回节点5
第四轮出栈,head为2,head.next为3,执行head.next.next=head也就是3.next=2,
此时链表为1->2<->3<-4<-5,由于2与3互相指向,所以此处要断开2.next=null
此时链表为1->2<-3<-4<-5
返回节点5
第五轮出栈,head为1,head.next为2,执行head.next.next=head也就是2.next=1,
此时链表为1<->2<-3<-4<-5,由于1与2互相指向,所以此处要断开1.next=null
此时链表为1<-2<-3<-4<-5
返回节点5
出栈完成,最终头节点5->4->3->2->1
*/
head.next.next = head;
head.next = null;
return newHead;
}

链表专题Ⅱ—反转强化

通关进度

题目 说明
指定区间反转 通关
两两交换链表中的节点 通关
单链表加 1 通关
链表加法 通关

指定区间反转

问题

【LeetCode 92】:给你单链表的头指针 head 和两个整数 leftright ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

穿针引线

以下图中黄色区域的链表反转为栗:

反转 leftright 部分以后,再拼接起来。此外,还需要记录 left 的前一个节点,和 right 的后一个节点。如图所示:

  1. 先将待反转的区域反转
  2. prenext 指针指向反转以后的链表头节点,把反转以后的链表的尾节点的 next 指针指向 succ

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
52
53
public ListNode reverseBetween(ListNode head, int left, int right) {

ListNode dummyNode = new ListNode(0);
dummyNode.next = head;

ListNode pre = dummyNode;

// step 1:从虚拟节点开始走 left - 1 步,来到 left 节点的前一个节点
for (int i = 0; i < left - 1; i++) {
pre = pre.next;
}

// step 2:从 pre 再走 right - left + 1 步,来到 right 节点
ListNode rightNode = pre;
for (int i = 0; i < right - left + 1; i++) {
rightNode = rightNode.next;
}

// step 3:截取链表
ListNode leftNode = pre.next;
ListNode curr = rightNode.next;

// 断链
pre.next = null;
rightNode.next = null;

// step 4:反转子区间的链表
reverseLinkedList(leftNode);

// step 5:将反转后的子区间链表接入原始链表
pre.next = rightNode;
leftNode.next = curr;

return dummyNode.next;
}

public void reverseLinkedList(ListNode head) { // 头插法反转

ListNode L = new ListNode(0);
L.next = head;

ListNode p = head;
// 断链
L.next = null;

while (p != null) {
ListNode q = p.next;
p.next = L.next;
L.next = p;
p = q;
}

}

头插法

算法思想

在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置

使用三个指针变量 precurrnext 来记录反转的过程中需要的变量,它们的意义如下:

  • curr:指向待反转区域的第一个节点 left
  • next:永远指向 curr 的下一个节点,循环过程中,curr 变化以后 next 会变化;
  • pre:永远指向待反转区域的第一个节点 left 的前一个节点,在循环过程中不变。

第 1 步,我们使用 ①、②、③ 标注「穿针引线」的步骤。

  • 先将 curr 的下一个节点记录为 next
  • 执行操作 ①:把 curr 的下一个节点指向 next 的下一个节点;
  • 执行操作 ②:把 next 的下一个节点指向 pre 的下一个节点;
  • 执行操作 ③:把 pre 的下一个节点指向 next

第 1 步完成以后「拉直」的效果如下:

第 2 步,同理。同样需要注意 「穿针引线」操作的先后顺序。

第 2 步完成以后「拉直」的效果如下:

第 3 步,同理。

第 3 步完成以后「拉直」的效果如下:

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
public ListNode reverseBetween(ListNode head, int left, int right) {

ListNode dummyNode = new ListNode(0);
dummyNode.next = head;
ListNode pre = dummyNode;

// `pre`:永远指向待反转区域的第一个节点 `left` 的前一个节点,在循环过程中不变。
for (int i = 0; i < left - 1; i++) {
pre = pre.next;
}

// `curr`:指向待反转区域的第一个节点 `left`;
ListNode cur = pre.next;

// `next`:永远指向 `curr` 的下一个节点,循环过程中,`curr` 变化以后 `next` 会变化;
ListNode next;

for (int i = 0; i < right - left; i++) {

// 先将 `curr` 的下一个节点记录为 `next`;
next = cur.next;
// 执行操作 ①:把 `curr` 的下一个节点指向 `next` 的下一个节点;
cur.next = next.next;
// 执行操作 ②:把 `next` 的下一个节点指向 `pre` 的下一个节点;
next.next = pre.next;
// 执行操作 ③:把 `pre` 的下一个节点指向 `next`。
pre.next = next;
}

return dummyNode.next;
}

两两交换链表中的节点

问题

【LeetCode 24】:给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

迭代

创建哑结点 dummyHead,令 dummyHead.next = head。令 temp 表示当前到达的节点,初始时 temp = dummyHead。每次需要交换 temp 后面的两个节点。

如果 temp 的后面没有节点或者只有一个节点,则没有更多的节点需要交换,因此结束交换。否则,获得 temp 后面的两个节点 node1node2,通过更新节点的指针关系实现两两交换节点。

具体而言,交换之前的节点关系是 temp -> node1 -> node2,交换之后的节点关系要变成 temp -> node2 -> node1,因此需要进行如下操作。

1
2
3
temp.next = node2
node1.next = node2.next
node2.next = node1

完成上述操作之后,节点关系即变成 temp -> node2 -> node1。再令 temp = node1,对链表中的其余节点进行两两交换,直到全部节点都被两两交换。

两两交换链表中的节点之后,新的链表的头节点是 dummyHead.next,返回新的链表的头节点即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public ListNode swapPairs(ListNode head) {

ListNode dummyHead = new ListNode(0);
dummyHead.next = head;
// `temp` 表示当前到达的节点
ListNode temp = dummyHead;

while (temp.next != null && temp.next.next != null) {
ListNode node1 = temp.next;
ListNode node2 = temp.next.next;
temp.next = node2;
node1.next = node2.next;
node2.next = node1;
temp = node1;
}

return dummyHead.next;
}

头插法

算法思想

计算链表长度 len,根据链表长度 len 划分交换次数,然后进行 len/2 次链表交换操作即可

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
// 头插法
public ListNode swapPairs(ListNode head) {

ListNode dummyHead = new ListNode(0);
dummyHead.next = head;

// 计算长度
int length = 0;
ListNode cur = head;
while (cur != null) {
length++;
cur = cur.next;
}
// tail 始终指向当前组反转后的最后一个节点
// 比如 [1,2,3,4] --> [2,1,4,3],tail 便记录节点元素 1
ListNode tail = dummyHead;
ListNode next = dummyHead;

// 外循环控制反转次数
for (int i = 0; i < length / 2; i++) {
// 初始化 cur 的位置
cur = tail.next;
// 临时保存当前组反转后的最后一个节点
ListNode temp = cur;
// tail 作为上一组反转链表的最后一个节点开始断链
tail.next = null;

// 内循环控制每组反转
for (int j = 0; j < 2; j++) { // 头插法
next = cur.next;
cur.next = tail.next;
tail.next = cur;
cur = next;
}
// 更新 tail
tail = temp;
// 恢复链表
tail.next = cur;
}

return dummyHead.next;
}

单链表加 1

问题

【LeetCode 369】:用一个非空单链表表示一个非负整数,然后将这个整数加一。你可以假设这个整数除了 0 本身,没有任何前导的 0。这个整数的各个数位按照 高位在链表头部、低位在链表尾部 的顺序排列。

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

算法思想

加法计算是低位至高位,而本题是高位在链表头部,低位在链表尾部。本题可以采用栈或者链表反转实现。

基于栈

遍历链表然后入栈,从栈中弹出栈顶数字 digit,然后考虑进位相加

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
public ListNode plusOne(ListNode head) {

Stack<Integer> stack = new Stack<>();

// 将链表元素入栈
while (head != null) {
stack.push(head.val);
head = head.next;
}

// 记录进位
int carry = 0;
ListNode dummyNode = new ListNode(0);
int adder = 1;

while (!stack.isEmpty() || carry > 0) {
// 弹出数字位
int digit = stack.empty() ? 0 : stack.pop();
int sum = digit + adder + carry;
// 进位
carry = sum >= 10 ? 1 : 0;
// 考虑进位后的 sum
sum = sum >= 10 ? sum - 10 : sum;
ListNode cur = new ListNode(sum);
cur.next = dummyNode.next;
dummyNode.next = cur;
adder = 0;
}

return dummyNode.next;
}

基于链表反转

将原始链表进行反转,然后进行(考虑进位的)加一操作,完成操作后重新反转链表得到结果

链表加法

问题

【LeetCode 445】:给你两个 非空 链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。

你可以假设除了数字 0 之外,这两个数字都不会以零开头。

算法思想

本题的主要难点在于链表中数位的顺序与我们做加法的顺序是相反的,为了逆序处理所有数位,我们可以使用栈:把所有数字压入栈中,再依次取出相加。计算过程中需要注意进位的情况。

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
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {

Deque<Integer> stack1 = new ArrayDeque<>();
Deque<Integer> stack2 = new ArrayDeque<>();

while (l1 != null) {
stack1.push(l1.val);
l1 = l1.next;
}
while (l2 != null) {
stack2.push(l2.val);
l2 = l2.next;
}
int carry = 0;
ListNode dummyNode = new ListNode(0);

while (!stack1.isEmpty() || !stack2.isEmpty() || carry > 0) {

int a = stack1.isEmpty() ? 0 : stack1.pop();
int b = stack2.isEmpty() ? 0 : stack2.poll();
int sum = a + b + carry;
// 记录进位
carry = sum / 10;
sum %= 10;
ListNode currNode = new ListNode(sum);
currNode.next = dummyNode.next;
dummyNode.next = currNode;
}

return dummyNode.next;
}

链表专题Ⅱ— K 组反转

题目 说明
K 组反转 通关

问题

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

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

头插法

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
public ListNode reverseKGroup(ListNode head, int k) {

ListNode dummyHead = new ListNode(0);
dummyHead.next = head;

// 计算长度
int length = 0;
ListNode cur = head;
while (cur != null) {
length++;
cur = cur.next;
}


// tail 始终指向当前组反转后的最后一个节点
// 比如 [1,2,3,4] --> [2,1,4,3],tail 便记录节点元素 1
ListNode tail = dummyHead;
ListNode next = dummyHead;

// 外循环控制反转次数
for (int i = 0; i < length / k; i++) {
// 初始化 cur 的位置
cur = tail.next;
// 临时保存当前组反转后的最后一个节点
ListNode temp = cur;
// tail 作为上一组反转链表的最后一个节点开始断链
tail.next = null;

// 内循环控制每组反转
for (int j = 0; j < k; j++) { // 头插法
next = cur.next;
cur.next = tail.next;
tail.next = cur;
cur = next;
}
// 更新 tail
tail = temp;
// 恢复链表
tail.next = cur;
}

return dummyHead.next;
}

穿针引线

由于分组反转,我们可以将其分成 已经反转正在反转未反转的三个部分,同时为处理好节点关系,我们新建虚拟节点。

之后直接遍历,根据是否为 K 个找到四个关键位置,并使用 prebeginendnext 进行标记,如图所示:

反转蓝色部分:

反转完成后,将链表恢复,然后调整指针指向:

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
public ListNode reverseKGroup(ListNode head, int k) {

ListNode dummyNode = new ListNode(0);
dummyNode.next = head;

ListNode pre = dummyNode;
ListNode end = dummyNode;

while (end.next != null) {
// 寻找待处理区间的尾部
for (int i = 0; i < k && end != null; i++) {
end = end.next;
}
if (end == null) {
break;
}
// 截取区间片段
ListNode begin = pre.next;
ListNode next = end.next;
end.next = null;
// 反转操作
pre.next = reverse(begin);
// 连接上区间反转的链表
begin.next = next;
// 调整 pre 的指向
pre = begin;
end = pre;
}
return dummyNode.next;
}

public ListNode reverse(ListNode head) {

ListNode L = new ListNode(0);
L.next = head;
ListNode curr = head;

L.next = null;
while(curr != null) {
ListNode next = curr.next;
curr.next = L.next;
L.next = curr;
curr = next;
}
return L.next;
}