反转链表递归迭代

Posted rotk2015

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了反转链表递归迭代相关的知识,希望对你有一定的参考价值。

  1. 讲解的题目包括,LeetCode206、92、 25

  2. 首先是经典的LeetCode206,反转整个链表。解法可分为迭代递归两种方式。而递归解法,根据返回函数的先后顺序(即处理当前递归栈与下一递归栈的先后顺序),又可分为top-down,bottom-up两种方式。本题十分经典,仔细体会,可以加深对迭代、递归的理解。

    public ListNode reverseList(ListNode head) {
        // return iterateSolution(head);
        // return recursionTopDownSolution(head);
        return recursionBottomUpSolution(head, null);
    }
    
  3. 迭代解法,实质上是在迭代遍历链表的同时,采用头插法建立新链表,这样,链表自然就是链表反转后的结果。那么,考虑一下,每次头插时,都需要同时维护三个变量的信息,故又称三指针法

    private ListNode iterateSolution(ListNode head) {
        ListNode pre = null, cur = head, nxt;
        while(cur != null){
            nxt = cur.next;
            cur.next = pre;
            pre = cur;
            cur = nxt;
        }
        return pre;
    }
    
  4. 先谈点我对递归的个人理解吧。其实递归做的无非就是压栈、出栈。那么,栈有什么特点呢?先入后出。也就是说,如果我们想先处理底层的,再处理顶层的,即,先处理下一步,再处理当前步,那么完全可以使用递归的方式解决。

    正如之前所说,递归先进行了一层层的压栈、再反向进行了一层层的出栈。压栈的过程可看作弹簧的展开,而出栈的过程可看作弹簧的收回。那么,如果在弹簧展开的过程中,就将问题解决,这种方式即为bottom-up,自底向上的,也就相当于。已压栈部分的子问题已得到解决,继续压栈只是为了解决剩余部分问题这种方式,说白了就是迭代

    那么,如果我们把解决问题的时机推后一些呢? 即,在弹簧收回的时候,再解决问题。这就相当于,已压栈部分的子问题并未得到解决,我们要先把问题缓存起来,解决了后边的问题之后,再回过头来得到当前问题的解。这种方式为top-down,自顶向下的。因为在我们解决当前步问题时,可以认为下一步以后的所有问题已经解决完。

  5. 递归解法:自底向上。可以仔细对比一下,与迭代解法的异同点。此外,注意 return 语句与处理当前步的先后顺序,以及递归终止条件、返回值

    private ListNode recursionBottomUpSolution(ListNode cur, ListNode pre) {
        if(cur == null)
            return pre;
        ListNode nxt = cur.next;
        cur.next = pre;
        return recursionBottomUpSolution(nxt, cur);
    }
    
  6. 递归解法:自顶向下。(这里使用的 head.next,next 有”藕断丝连“意)同样,注意 return 语句与处理当前步的先后顺序,以及递归终止条件、返回值。(递归函数内部的head.next = null 可以省略,只需在调用 recursionTopDownSolution 的外部函数里,将 head 实参的 next 修改为 null 即可)

    private ListNode recursionTopDownSolution(ListNode head) {
        if(head.next == null)
            return head;
        ListNode newHead = recursionTopDownSolution(head.next);
        head.next.next = head;
        head.next = null;
        return newHead;
    }
    
  7. 然后是LeetCode92,反转部分链表。由于部分反转包括了从一开始就反转的情况,故 dummyHead 十分有用。此处仅列举 top-down 的递归解法以及迭代解法。

    两者思路是一致的,由于是部分反转,因此我们要先遍历至左边界(left)的前一个节点(因为是单向链表,回头很麻烦),然后剩下的就是反转有限长度的链表问题,那么,只需在LeetCode206基础上,加个计数循环判定即可。

    public ListNode reverseBetween(ListNode head, int left, int right) {
        return recursionSolution(head, left, right);
        // return iterateSolution(head, left, right);
    }
    
  8. 迭代解法

    private ListNode iterateSolution(ListNode head, int left, int right) {
        // one-pass
        ListNode dummyHead = new ListNode(0);
        // dummyHead is useful when left = 1
        dummyHead.next = head;
        ListNode lefTail = dummyHead;
        for(int i=1; i<left; i++)
            lefTail = lefTail.next;
        ListNode midTail = lefTail.next;
        ListNode rigHead = midTail.next;
        while(right-- > left){
            midTail.next = rigHead.next;
            rigHead.next = lefTail.next;
            lefTail.next = rigHead;
    
            rigHead = midTail.next;
        }
        return dummyHead.next;
    }
    
  9. 递归top-down解法:这里要注意的是,由于是部分反转,故对于可能存在的右边界节点的下一个节点要用已反转的部分链表的尾结点的 next 保存其信息

    private ListNode recursionSolution(ListNode head, int left, int right) {
        // reverseBottomUp can be totally one-pass
        // reverseTopDown is one-pass + (right-left)
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
    
        ListNode lefTail = dummyHead;
        for(int i=1; i<left; i++)
            lefTail = lefTail.next;
    
        lefTail.next = reverseTopDown(lefTail.next, right-left);
        return dummyHead.next;
    }
    private ListNode reverseTopDown(ListNode head, int res){
        // the reverseBottomUp is the same as iterateSolution
        if(res == 0)
            return head;
        ListNode newHead = reverseTopDown(head.next, --res);
        ListNode tmp = head.next.next;
        head.next.next = head;
        head.next = tmp;
        return newHead;
    }
    
  10. 接着是LeetCode25每k个一组反转链表,本题又可看作是在LeetCode92的基础上,增加了点限制条件演化而来。由于题目要求,必须满足k个才能反转,故每次要预先遍历试探,判断是否满足k个。

  11. 这里先介绍一个核心函数 reverseList,基本作用是,头插法反转链表,并返回反转后新链表的头

    private ListNode reverseList(ListNode head, ListNode nxtHead){
        // | head...| nxtHead...
        ListNode tmp, base;
        base = nxtHead;
        while(head != nxtHead){
            tmp = head.next;
            head.next = base;
            base = head;
            head = tmp;
        }
        return base;
    }
    
  12. 递归解法:recurSolution 提交了下一层的新头,而 head.next 的赋值,又将当前层与下一层相连接(需知head在反转后的位置已到了尾部)(其实,该赋值语句功能与reverseList的功能有重合——因为头插是在nxtHead基础上开始的,而此处列出仅仅是为了保证当不足k个无法进行反转链表时,逻辑的正常性)。

    private ListNode recurSolution(ListNode head, int k){
        // T:O(2n) cause we have to make sure there are k nodes
        ListNode cur = head;
        int i = 0;
        while(i++ < k){
            if(cur == null)
                return head;
            cur = cur.next;
            // what if kth is NULL ?
        }
        // | head......newHead | cur...
        ListNode newHead = reverseList(head, cur);
        head.next = recurSolution(cur, k);
        return newHead;
    }
    
  13. 迭代解法

    private ListNode iterateSolution(ListNode head, int k){
        // still 2-pass
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;
        ListNode lefTail = dummyHead;
        ListNode cur = head;
        while(head != null){
            int i = 0;
            while(i++ < k){
                if(cur == null)
                    return dummyHead.next;
                cur = cur.next;
            }
            lefTail.next = reverseList(head, cur);
            lefTail = head;
            head = cur;
        }
        return dummyHead.next;
    }
    
  14. 无论是列出的迭代解或是递归解,都需要先探明是否满足k个节点,然而,完全可以使用另一种更大胆的方式,即直接进行k个节点的反转,而当反转过程中发现不够k个节点时,回头再反转一次即可。这种方式的时间复杂度相对更低

以上是关于反转链表递归迭代的主要内容,如果未能解决你的问题,请参考以下文章

翻转链表 (无傀儡节点,递归+迭代)

反转链表递归迭代

反转链表递归迭代

反转链表递归迭代

反转链表递归迭代

206. 反转链表简单迭代递归