文章目录

题干

剑指 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=null​​​,​​cur=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): 变量 ​​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/​