• 反转链表I和II(迭代和递归)


    前言

    有句话叫做:如果面试官跟你看顺眼的话,就给你出一道反转链表,否则就出一道 hard。
    所以反转链表不能不会吧,要不面试官想要你都没有机会了。

    206. 反转链表

    class Solution {
        public ListNode reverseList(ListNode head) {
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    迭代

    迭代就是遍历链表,需要有个 cur 指针在链表上游走。
    先思考一般情况,假设遍历到某一节点,应该做什么?

    如上图所示,应该修改其 next 指针,指向前一个节点。由于单链表的性质,无法从当前节点获取前一个节点的指针,所以需要有个 pre 指针记录当前节点的前一个节点。把 cur 的指针修改后,就无法获取其后一个节点了,因此需要有个 next 指针保存 cur 的下一个节点。更新 precur 指针,让它们后移一位即可。

    class Solution {
        public ListNode reverseList(ListNode head) {
            // pre记录cur的前一个节点,cur是当前遍历到的节点
            ListNode pre = null, cur = head;
    
            // 遍历链表一般用while循环
            while(cur != null){
                // 保存cur的下一个节点
                ListNode next = cur.next;
                // 修改cur指针,指向它前一个节点
                cur.next = pre;
                // 更新pre、cur, 同时后移一位
                pre = cur;
                cur = next;
            }
    
            // 此时cur为null, pre指向最后一个节点
            return pre;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    递归

    递归有两个性质:

    1. 边界条件
    2. 转化为子问题调用自身

    先思考第二个问题,如何转化为子问题调用自身?
    转化为子问题也就是缩小问题的规模,如果 head 指向的链表有 n 个节点,那么 head.next 指向的链表就有 n - 1 个节点,因此 head.next 就是一个子问题。为了更好地复用递归函,需要明确递归函数的定义,这里递归函数的定义是,输入一个单链表的头指针,返回该链表反转后的链表的头指针。

    调用 head.next 的示意图如下:

    根据递归函数的定义,它计算后返回值的情况如下所示:

    接下来,就是要处理 head 和 reverseList(head.next) 的关系了,这个例子中就是 1 号节点和后面部分的关系。这里有两步,第一步,让 2 号节点指向 1 号节点;第二步,让 1 号节点指向 null。第一步翻译成代码就是head.next.next = head,第二步翻译成代码就是head.next = null

    现在,第二个问题就处理完了,代码如下

    class Solution {
        public ListNode reverseList(ListNode head) {
            // 1、边界条件
            // if(){
    
            // }
    
            // 2、转化为子问题调用自身
            ListNode last = reverseList(head.next);
            // 修改head和后面部分指针的关系
            head.next.next = head;
            head.next = null;
            return last;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    对于递归函数,我觉得第二部分是核心,边界条件是特殊情况,可以先不考虑。自己去想可能会遗漏一些情况,这时如果允许执行测试用例,可以借助执行测试用例来帮助我们补全边界条件。

    比如,这里我暂时不清楚边界条件,先执行测试用例,看看它会报什么错,然后根据它的错误提示来写边界条件。

    直接执行上面的代码,会报这样的错

    这样就知道 head 不能为空,加上条件

    // 1、边界条件
    if(head == null){
        return head;
    }
    
    • 1
    • 2
    • 3
    • 4

    再执行代码,会报如下的错

    这样就知道 head.next 不能为空,加上条件

    // 1、边界条件
    if(head == null || head.next == null){
    	return head;
    }
    
    • 1
    • 2
    • 3
    • 4

    完整代码

    class Solution {
        public ListNode reverseList(ListNode head) {
            // 1、边界条件
            if(head == null || head.next == null){
                return head;
            }
    
            // 2、转化为子问题调用自身
            ListNode last = reverseList(head.next);
            // 修改head和后面部分指针的关系
            head.next.next = head;
            head.next = null;
            return last;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    92. 反转链表 II

    迭代

    希望复用反转单链表的代码,这样就需要把需要反转的部分单独抽取出来,反转好了之后再拼接回去。因此需要有 4 个指针,leftNode 和 rightNode 分别指向需要反转部分的首尾节点,pre 指针指向 leftNode 的前一个,cur 指针指向 rightNode 的后一个,pre 和 cur 的作用是为了保存断开链表和拼接链表的位置。4 个指针的位置关系如下图所示。
    在这里插入图片描述
    确定 4 个指针的位置并不复杂,循环指定步数即可。确定好 4 个指针的位置后,就是断开 pre 和 leftNode、rightNode 和 cur,调用之前的反转单链表将中间部分反转,最后再拼接回去即可。

    完整代码

    class Solution {
        public ListNode reverseBetween(ListNode head, int left, int right) {
            // 虚拟头节点
            ListNode dummy = new ListNode(-1, head);
            ListNode pre = dummy, cur = null;
            ListNode leftNode = null, rightNode = head;
    
            // 从虚拟头节点走left-1步,来到left前一个节点
            for(int i = 0; i < left - 1; i++){
                pre = pre.next;
            }
            // left节点在pre节点下一个
            leftNode = pre.next;
            // 从头节点走right-1步,来到right节点
            for(int i = 0; i < right - 1; i++){
                rightNode = rightNode.next;
            }
            // cur节点在right节点下一个
            cur = rightNode.next;
    
            // 断开指针
            pre.next = null;
            rightNode.next = null;
            // 反转between
            reverseList(leftNode);
            // 拼接指针
            pre.next = rightNode;
            leftNode.next = cur;
    
            return dummy.next;
        }
    
        // 反转单链表
        private ListNode reverseList(ListNode head){
            ListNode pre = null, cur = head;
            while(cur != null){
                ListNode next = cur.next;
                cur.next = pre;
                pre = cur;
                cur = next;
            }
            return pre;
        }
    }
    
    • 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

    递归

    首先需要实现反转前 n 个节点的函数 ListNode reverseN(ListNode head, int n)

    比如要反转如下链表的前 3 个节点
    在这里插入图片描述
    那就是要反转以 head.next 开头的链表的前 2 个节点
    在这里插入图片描述
    如果reverseN函数写的是正确的,那么按照它的定义,返回值如下
    在这里插入图片描述
    这里又涉及到 head 和 reverseN(head.next, n - 1) 指针关系的处理,和递归反转单链表是类似的,第一步是让 2 号节点指向 1 号节点,第二步略有不同,反转单链表是直接让1号节点指向 null,而这里应该让 1 号节点指向 4 号节点,也就是不需要反转的部分。因此,就需要一个指针记录 4 号节点的位置。

    reverseN代码如下

    	// 记录不用反转的第一个节点
        ListNode suc = null;
    
        private ListNode reverseN(ListNode head, int n){
            // 1、边界条件
            if(n == 1){
                suc = head.next;
                return head;
            }
            // 2、转化为子问题调用自身
            ListNode last = reverseN(head.next, n - 1);
            // 修改head和后面部分指针的关系
            head.next.next = head;
            head.next = suc;
            return last;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    接下来实现reverseBetween
    当 left 等于 1 的时候,就转化成了reverseN的问题。
    对于 head,反转 left 和 right;对于 head.next,应该反转 left - 1 和 right - 1;对于head.next.next,应该反转 left - 2 和 right - 2…

    所以,reverseBetween函数代码如下

    public ListNode reverseBetween(ListNode head, int left, int right) {
            if(left == 1){
                return reverseN(head, right);
            }
            head.next = reverseBetween(head.next, left - 1, right - 1);
            return head;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    完整代码

    class Solution {
    
        public ListNode reverseBetween(ListNode head, int left, int right) {
            // 当left==1, 就转化成立reverseN问题
            if(left == 1){
                return reverseN(head, right);
            }
            head.next = reverseBetween(head.next, left - 1, right - 1);
            return head;
        }
    
        // 记录不用反转的第一个节点
        ListNode suc = null;
    
        private ListNode reverseN(ListNode head, int n){
            // 1、边界条件
            if(n == 1){
                suc = head.next;
                return head;
            }
            // 2、转化为子问题调用自身
            ListNode last = reverseN(head.next, n - 1);
            // 修改head和后面部分指针的关系
            head.next.next = head;
            head.next = suc;
            return last;
        }
    }
    
    • 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

    参考资料

    递归魔法:反转单链表

  • 相关阅读:
    k8s笔记——kubernetes中的三种IP
    在ubuntu上使用wireshark对some/ip协议进行抓包
    step5 lasso 回归 实战
    Linux软件安装方式 - Tarball&RPM&YUM
    PX4模块设计之三十四:ControlAllocator模块
    P3177 [HAOI2015] 树上染色
    Linux下通过开源软件fail2ban进行远程登录防护
    C语言实验十 函数(二)
    Oracle中LEFT JOIN后AND与WHERE的异同
    图解算法,原理逐步揭开「GitHub 热点速览」
  • 原文地址:https://blog.csdn.net/m0_46283220/article/details/126596838