2020年7月23日 周四 天气晴 【不悲叹过去,不荒废现在,不惧怕未来】
LeetCode 206. 反转链表,这道题一般有两种解法:迭代和递归,我觉得两种方法都特别经典,最好都能掌握。
迭代一次的具体过程:
这样便完成了一次局部反转,重复这个过程,直到 cur 指向NULL,返回当前的 pre ,即为所求。
代码如下:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *pre = NULL, *cur = head;
while(cur){
ListNode *tmp = cur->next;
curr->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
};
递归的思想稍微复杂一点,这里会大量借鉴算法大佬 labuladong 的文章来说(十分感谢 labuladong 大佬),先给上递归的代码:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
// base case
if(head==NULL || head->next==NULL)
return head;
ListNode* last= reverseList(head->next);
head->next->next = head;
head->next = NULL;
return last;
}
};
对于递归算法,最重要的就是明确递归函数的定义。具体来说,reverseList 函数定义是这样的:
输入一个结点 head,将「以 head 为起点」的链表反转,并返回反转之后的头结点。
明白了函数的定义,在来看这个问题。比如说我们想反转这个链表:
会在这行代码进行递归:
ListNode* res = reverseList(head->next);
不要跳进递归(按照 labuladong 大佬的话说:你的脑袋能压几个栈呀?),而是要根据刚才的函数定义,来弄清楚这段代码会产生什么结果:
reverseList(head->next) 执行完成后,整个链表就成了这样:
并且根据函数定义,reverseList 函数会返回反转之后的头结点,我们用变量 last 来接收。
现在我们要考虑的就是:“我子结点下的所有结点都已经反转好了,现在就剩我和我的子结点没有完成最后的反转了,所以反转一下我和我的子结点。”
来看下面这行代码:
head->next->next = head;
接下来:
head->next = NULL;
return last;
这样整个链表就反转过来了!
我们不需要去考虑递归中间繁杂的过程,只要处理好最后面(或最前面)一小步,中间的过程只不过是在循环进行这一小步而已,这就是分治的思想。(秒,实在是妙~)
在实现反转链表的一部分之前,先来看一下如何反转链表前N个结点:
// 将链表的前 n 个节点反转(n <= 链表长度)
ListNode* reverse_n(ListNode* head, int n)
比如说对于下图链表,执行 reverse_n(head, 3):
解决思路和反转整个链表差不多,只要稍加修改即可:
ListNode* successor = nullptr; // 后驱结点
// 反转以 head 为起点的 n 个结点,返回新的头结点
ListNode* reverse_n(ListNode* head, int n){
// base case
if(n==1){
// 记录第 n + 1 个结点
successor = head->next;
return head;
}
// 以 head->next 为起点,需要反转前 n - 1 个结点
ListNode* last = reverse_n(head->next,n-1);
head->next->next = head;
// 让反转之后的 head 结点和后面的结点连起来
head->next = successor;
return last;
}
具体的区别:
1、base case 变为 n == 1,反转一个元素,就是它本身,同时要记录后驱节点。
2、刚才我们直接把 head->next 设置为 null,因为整个链表反转后原来的 head 变成了整个链表的最后一个节点。但现在 head 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 successor(第 n + 1 个节点),反转之后将 head 连接上。
现在解决最开始提出的问题,给一个索引区间 [m,n](索引从 1 开始),仅仅反转区间中的链表元素。
ListNode* reverseBetween(ListNode* head, int begin, int end)
首先,如果 m == 1,就相当于反转链表开头的 n 个元素嘛,也就是我们刚才实现的功能:
ListNode* reverseBetween(ListNode* head, int m, int n){
// base case
if(m==1)
// 相当于反转前 n 个元素
return reverse_n(head,n);
// ...
}
如果 m != 1 怎么办?如果把 head->next 的索引视为 1 的话,那么相对于 head->next,反转的区间应该是从第 m - 1 个元素开始的,相应的,区间结束位置也变成了 n - 1(要保证区间长度不变)。
因此,完整代码如下:
ListNode* reverseBetween(ListNode* head, int m, int n){
// base case
if(m==1)
// 相当于反转前 n 个元素
return reverse_n(head,n);
// 前进到反转的起点触发 base case
head->next = reverse_list(head->next,m-1,n-1);
return head;
}
迭代需要借助双指针和哑结点(这两个方法太有用了,感觉做链表题不会的时候就想想它们,有奇效!!!),按照题目的意思一步一步的做,细心一点也不难,只是理解了递归法之后就会觉得迭代法十分麻烦,远不如递归来的简洁清爽~
迭代法代码如下:
class Solution {
public:
ListNode* reverseBetween(ListNode* head, int m, int n) {
if(!head || m==n) return head;
int cnt = 0;
ListNode* first_end = nullptr, *second_head = nullptr;
ListNode* pre = new ListNode(-1), *cur = head, *res = pre; //定义哑结点res,用于返回最后的结果
pre->next = head;
//定位 m 的位置,存下 first_end 和 second_head 用于之后链表的拼接
while(cur){
if(++cnt==m){
first_end = pre;
second_head = cur;
pre = cur;
cur = cur->next;
break;
}
pre = cur;
cur = cur->next;
}
//局部反转,直到位置 n
while(cur){
ListNode* t = cur->next;
cur->next = pre;
pre = cur;
cur = t;
if(++cnt==n){
//完成拼接
first_end->next = pre;
second_head->next = cur;
break;
}
}
return res->next;
}
};
https://leetcode-cn.com/problems/reverse-linked-list/solution/tu-jie-liu-cheng-python3die-dai-xiang-jie-by-han-h/
https://zhuanlan.zhihu.com/p/107759633
说明:本文内容部分搬运至互联网,侵删。