LeetCode 206. 反转链表 & 92. 反转链表 II(迭代、递归都要掌握)

2020年7月23日 周四 天气晴 【不悲叹过去,不荒废现在,不惧怕未来】



1. 反转链表(LeetCode 206)

LeetCode 206. 反转链表,这道题一般有两种解法:迭代和递归,我觉得两种方法都特别经典,最好都能掌握。
在这里插入图片描述

解法1. 迭代

迭代一次的具体过程:

  1. 建立两个指针 pre = NULL,cur = head
  2. 记录 cur 的下个结点为 tmp
  3. 将 cur 反向指向 pre
  4. pre 和 cur 均前进一步(即:pre = cur; cur = tmp)

这样便完成了一次局部反转,重复这个过程,直到 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;
    }
};

解法2. 递归

递归的思想稍微复杂一点,这里会大量借鉴大佬 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;

在这里插入图片描述

这样整个链表就反转过来了!

我们不需要去考虑递归中间繁杂的过程,只要处理好最后面(或最前面)一小步,中间的过程只不过是在循环进行这一小步而已,这就是分治的思想。(秒,实在是妙~)


2. 反转链表 II(LeetCode 92)

LeetCode 92. 反转链表 II
在这里插入图片描述

解法1. 递归

2.1.1 反转链表前 N 个结点

在实现反转链表的一部分之前,先来看一下如何反转链表前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 连接上。
在这里插入图片描述

2.1.2 反转链表的一部分

现在解决最开始提出的问题,给一个索引区间 [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(要保证区间长度不变)。

因此,完整代码如下:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* successor = nullptr; // 后驱结点
    ListNode* reverseBetween(ListNode* head, int m, int n) {
   		// base case
        if(m==1)
        	// 相当于反转前 n 个元素
            return reverse_n(head,n);
            
        // 前进到反转的起点触发 base case
        head->next = reverseBetween(head->next,m-1,n-1);
        return head;
    }

	// 反转以 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;
    }
};

解法2. 迭代

迭代需要借助双指针和哑结点(这两个方法太有用了,感觉做链表题不会的时候就想想它们,有奇效!!!),按照题目的意思一步一步的做,细心一点也不难,只是理解了递归法之后就会觉得迭代法十分麻烦,远不如递归来的简洁清爽~

迭代法代码如下:

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;
    }
};
简洁版本
class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        //思路:head(不变)表示需要反转的头节点,pre(不变)表示需要反转头节点的前驱节点
        ListNode* dummy = new ListNode(0);
        dummy->next = head;
        ListNode* pre = dummy;
        // 快进到需要反转的部分
        for(int i=1;i<left;++i) pre = pre->next;
        head = pre->next;
        for(int i=left;i<right;++i){	
            // 核心操作(共4步),执行完以下4步,反转一次
            ListNode* cur = head->next;
            head->next = cur->next;
            cur->next = pre->next;
            pre->next = cur;

		    //比如1->2->3->4->5,m=1,n=5
		    //第一次反转:1(head) 2(cur) 3 4 5 反转为 2 1 3 4 5
		    //第二次反转:2 1(head) 3(cur) 4 5 反转为 3 2 1 4 5
		    //第三次发转:3 2 1(head) 4(cur) 5 反转为 4 3 2 1 5
		    //第四次反转:4 3 2 1(head) 5(cur) 反转为 5 4 3 2 1
        }
        return dummy->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

说明:本文内容部分搬运至互联网,侵删。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值