Preface
单链表的反转,按K反转等各种考点变体。
处理单链表时, 尽量采用递归实现:
1 因为递归代码简洁+优美,节省时间,并且不容易出错,是面试优选策略。
2 在面试时间比较宝贵的情况下,尽量预留时间展示自己的才能。
1 反转单链表(基础)
单链表结构定义如下:
1
2
3
4
5
6
|
// Definition for singly-linked list.
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
|
方法一: 递归实现(尾插法)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Solution {
public:
//尾插法
ListNode* reverseList(ListNode* head) {
if(head == NULL || head->next == NULL) {
return head;
}
auto tail = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return tail; // became new head
}
};
|
方法二: 循环实现(头插法)
双指针,头插法:
1
2
3
4
5
6
7
8
9
10
11
|
ListNode* reverseList(ListNode *head) {
ListNode *pNewHead = NULL;
while(head != NULL){
auto pop = head;
head = head->next;
//插入新链表头部
pop->next = pNewHead;
pNewHead = pop;
}
return pNewHead; //链表的尾元素
}
|
2 变体一: 递归实现-从 m 到 n 中间节点的反转
上面我们使用递归,实现了反转整个链表的算法;
稍微改造下,只反转前k个元素(即递归深度到第k个元素,就可以返回)。
递归实现(从head 开始, 只反转前k个元素):
1
2
3
4
5
6
7
8
9
10
|
ListNode* reverseK(ListNode *head, int k) {
if (k <= 1) {
return head;
}
auto last = reverseK(head->next, k-1);
auto tail = head->next->next; //the tail list
head->next->next = head;
head->next = tail;
return last; //链表的尾元素
}
|
反转中间一部分[m,n], 再分析一下:
一种特殊情况是,m=1, 也就是反转前 n 个元素。
当 m = 2 ,我们把第一个节点去掉,就可以转化为上面的特殊情况了;
总结一下: 推广到一般的情况(m>1),都可以转换为 m=1 的特殊情况处理。
1
2
3
4
5
6
7
8
|
// 反转中间一部分[m,n] 递归实现
ListNode* reverseBetween(ListNode *head, int m, int n) {
if (m == 1) {
return reverseK(head, n);
}
head->next = reverseBetween(head->next, m - 1, n - 1);
return head;
}
|
3 变体二: 按K个元素一组反转 (最后一组不足K个不反转)
我们先考虑下特殊情况:
1 链表元素总个数n 小于 k, 不做反转直接返回即可。
2 如果等于k个, 那么把当前链表按k反转,并返回新的head节点。
3 如果多余k个,我们先按照情形2处理前k个元素,剩下的(n-k)的链表当成一个子问题,执行步骤1,2,3.
最后这个子问题解决后,返回它的头指针 tailHead,
我们再把当前块的尾指针 head->next = tailHead 即可。
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
|
// golang implements
func reverseKGroup(head *ListNode, k int) *ListNode {
p := head
for i := 0; i < k; i++ {
// 不足k个直接返回
if p == nil {
return head
}
p = p.Next
}
newHead := reverseK(head, k)
// 超过K个节点部分, 又是一个按K反转的问题
head.Next = reverseKGroup(p, k)
return newHead
}
// 反转链表前 K 个元素
func reverseK(head *ListNode, k int) *ListNode {
if k == 1 {
return head
}
rear := reverseK(head.Next, k-1)
head.Next.Next = head
head.Next = nil
return rear
}
|
这个问题还有一个变体(从后往前分组), 这个题是 Shopee(2019年)的面试题:
解决办法:
1 先把整个链表全部反转,然后从新的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
44
45
46
47
|
/**[逆序] 从后往前, 每k个反转一次(不足k个不反转)(循环实现)
输入:1->2->3->4->5->6->7 k = 3
输出: 1->4->3->2->7->6->5
输入:1->2->3->4->5->6->7->8 k = 3
输出: 1->2->5->4->3->8->7->6
*/
ListNode* reverse(ListNode *head) {
if (head == NULL || head->next == NULL) {
return head;
}
ListNode *last = reverse(head->next);
head->next->next = head;
head->next = NULL;
return last; //became header
}
ListNode* reverseKGroup(ListNode *head, int k) {
ListNode * rhead = reverse(head);
int count = 0;
ListNode *cur = rhead;
ListNode *remain_list = rhead;
ListNode *newHead = NULL;
while (cur != NULL) {
++count;
ListNode *pre = cur;
cur = cur->next;
if (count == k) {
// 头插法
pre->next = newHead;
newHead = remain_list;
//move next
remain_list = cur;
count = 0;
}
}
//不足k个的部分,采用头插法
while (remain_list != NULL) {
ListNode *pop = remain_list;
remain_list = remain_list->next;
//头插法
pop->next = newHead;
newHead = pop;
}
return newHead;
}
|
总结
正所谓万变不离其宗,我们先分析问题的特殊场景,然后推广到一般;
把一般情况转化为一个个特殊场景的子问题,最后使用递归就可以完美解决。