剑指 Offer 24. 反转链表(双指针+递归)

Posted 徐同学呀

tags:

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

文章目录

题干

剑指 Offer 24. 反转链表:

定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。

示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

/**
 * Definition for singly-linked list.
 * public class ListNode 
 *     int val;
 *     ListNode next;
 *     ListNode(int x)  val = x; 
 * 
 */
class Solution 
    public ListNode reverseList(ListNode head) 
    

原题链接:https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/9pdjbm/

思路解析

假设链表:10–>9–>8–>7–>null

反转之后的链表:null<–10<–9<–8<–7

方法一:双指针遍历

定义两个指针,一前一后遍历链表,同时修改后一个指针的next指向前一个指针。

具体反转流程:

  1. 设置上一个指针pre,当前指针cur,初始pre=nullcur=head
  2. 先用一个tmp保存cur.next,即 tmp=cur.next
  3. 反转操作,cur的next指针指向pre,即 cur.next=pre
  4. pre向右移动,将 cur 赋值给 pre,即 pre=cur
  5. cur向右移动,tmp提前保存了原链表的next,所以将tmp赋值给cur,即 cur=tmp
  6. 循环2-5,直到cur=null为止

需要注意:2. 提前用tmp保存了cur.next,是因为3. 反转操作cur.next=pre会使得原链表断开,想继续向右遍历原链表必须提前保存下一个节点,然后最后赋值给cur。

复杂度分析:

时间复杂度 O(N): 遍历链表使用线性大小时间。
空间复杂度 O(1): 变量 precur 使用常数大小额外空间。

方法二:递归回溯

从头节点 head 递归遍历至尾节点 tail,然后再利用递归回溯的特点,从 tail 开始修改next指针指向左边的节点。

递归终止条件:

head=head.next 向右遍历。

递归的首要任务是找到终止条件:当链表本身为null,即head==null 为 true,或者遍历到尾节点,尾节点不为null,但是next为null,即 head==null 为false || head.next==null 为 true。

复杂度分析:

时间复杂度 O(N): 遍历链表使用线性大小时间。
空间复杂度 O(N): 遍历链表的递归深度达到 N ,系统使用 O(N) 大小额外空间。

方法三:借助栈or数组倒置

借助栈将链表节点顺序压入栈,然后再倒序出栈,并修改next反转。

借助可自动扩容的 ArrayList将链表节点顺序添加到ArrayList,然后倒序遍历,并修改next反转。

复杂度分析:

时间复杂度 O(2N): 两次遍历,时间复杂度为O(2N)。
空间复杂度 O(N): 借助栈或者数组,存储链表节点,所以需要额外的空间O(N)。

总结三种方法,双指针最优。 不推荐第三种借助其他数据结构的方法。

代码实现

方法一:双指针遍历

/**
 * Definition for singly-linked list.
 * public class ListNode 
 *     int val;
 *     ListNode next;
 *     ListNode(int x)  val = x; 
 * 
 */
class Solution 
    public ListNode reverseList(ListNode head) 
        // 10-->9-->8-->7-->null
        // null<--10<--9<--8<--7
        // 前驱指针
        ListNode pre = null;
        // 当前指针
        ListNode cur = head;
        while (cur != null) 
            // 提前保存当前节点的next,保证向右遍历不会中断
            ListNode tmp = cur.next;
            // 反转
            cur.next = pre;
            // pre向右移到cur
            pre = cur;
            // cur向右移到tmp
            cur = tmp;
        
        // 遍历到最后了,cur=null,那么pre就是尾节点返回
        return pre;
    

方法二:递归回溯

class Solution 
    public ListNode reverseList(ListNode head) 
        // 10-->9-->8-->7-->null
        // null<--10<--9<--8<--7
        //当递归到尾部,则停止返回
        if(head == null || head.next == null)
            return head;
        
        // 上半部分是 顺时针 
        // 递归遇到终止条件返回尾节点
        ListNode tail = reverseList3(head.next);
        // 下半部分就是 逆时针
        //回溯反转
        // 第一次返回时,head 为倒数 第二个节点,head.next 为尾节点
        // 那么从尾节点开始反转操作: 尾节点 head.next 的 next 指向 倒数第二个节点 head,即 head.next.next = head
        // 8<--7
        
        // 第二次返回时,head 就是 倒数第三个节点,head.next 为 倒数第二个节点
        // 那么反转操作:倒数第二个节点 head.next 的next指向倒数第三个节点 head,即 head.next.next = head
        // 9<--8<--7
        // 以此类推,直到回溯到原链表的头节点 head,反转操作 head的下一个节点的next指向 head,即 head.next.next = head
        // 一切非常顺利,但已运行死循环了,那是因为 原链表的头节点next指针没有修改,依然指向的是头节点的下一个节点,而头节点的next已经反转指向头节点,这样就形成了一个环
        // 10<-->9<--8<--7
        head.next.next = head;
        // 所以还需要修改 原链表头节点的next指向null。
        // null<--10<--9<--8<--7
        // 其实每个回溯过程都把当前节点的 next 和下一个节点主动断开,
        // 防止最后形成环
        head.next = null;
        // 最后都是返回尾节点
        return tail;
    

递归的另一种写法:

public ListNode reverseList(ListNode head) 
        // 10-->9-->8-->7-->null
        return recur(head, null);
    
    private ListNode recur(ListNode cur, ListNode pre) 
        // cur = 10, pre = null
        // cur = 9,  pre = 10
        // cur = 8,  pre = 9
        // cur = 7,  pre = 8

        // 终止条件
        if (cur == null) 
            // cur== null 可能一开始 链表就为null
            // 或者遍历到了 尾节点的 next
            // 那么尾节点就是 pre
            return pre;
        
        // cur=null, pre = 7
        // 最终返回 res = 7
        ListNode res = recur(cur.next, cur);
        // cur = 7, pre = 8
        // cur = 8,  pre = 9
        // cur = 9,  pre = 10
        // cur = 10, pre = null
        // 修改节点引用指向
        cur.next = pre;
        // 返回反转后链表的头节点
        return res;                  
    

参考致谢:https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/9p7s17/

以上是关于剑指 Offer 24. 反转链表(双指针+递归)的主要内容,如果未能解决你的问题,请参考以下文章

剑指 Offer 24. 反转链表(双指针+递归)

反转链表(剑指offer_24)多看多思多想

经典题目:反转链表(leetcode剑指offer24)

剑指Offer - 面试题24:反转链表

双100%解法剑指 Offer 24. 反转链表

双100%解法剑指 Offer 24. 反转链表