剑指 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
指向前一个指针。
具体反转流程:
- 设置上一个指针
pre
,当前指针cur
,初始pre=null
,cur=head
- 先用一个
tmp
保存cur.next
,即tmp=cur.next
- 反转操作,
cur
的next指针指向pre
,即cur.next=pre
pre
向右移动,将cur
赋值给pre
,即pre=cur
cur
向右移动,tmp
提前保存了原链表的next
,所以将tmp
赋值给cur
,即cur=tmp
- 循环2-5,直到
cur=null
为止
需要注意:2. 提前用tmp
保存了cur.next
,是因为3. 反转操作cur.next=pre
会使得原链表断开,想继续向右遍历原链表必须提前保存下一个节点,然后最后赋值给cur。
复杂度分析:
时间复杂度 O(N): 遍历链表使用线性大小时间。
空间复杂度 O(1): 变量 pre
和 cur
使用常数大小额外空间。
方法二:递归回溯
从头节点 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. 反转链表(双指针+递归)的主要内容,如果未能解决你的问题,请参考以下文章