学算法刷LeetCode【剑指offer专题】:24.反转链表

314 阅读4分钟

题目描述

image.png

解题思路

迭代法

迭代法核心就是遍历链表,然后将指针反转即可。主意更改当前节点指针的时候,需要将它的下一个节点的地址先保存起来,以免丢失。

  • 初始化三个变量,cur 当前节点, 当前节点的前一个节点,prevnext 当前节点的下一个节点

    let cur = head, prev = null, next;
    
  • 遍历链表,直到尾节点 null,改变指针指向,注意保存 next 的指针,并开始下一轮循环, 最后返回结果。

    while(cur){
        // 保存  next 指针
        next = cur.next;
    
        // 改变指针
        cur.next = prev;
    
        // 开始下一轮
        prev = cur;
        cur = next;
    }
    

完整代码

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {   
    if(!head) return head;
    let prev = null, cur = head,next;
    while(cur){
        next = cur.next;
        cur.next = prev;
        prev = cur;
        cur = next;
    }
    return prev;
};

复杂度分析

时间复杂度: O(n) 需要遍历链表 n 个元素

空间复杂度: O(1) 只有三个临时变量 prev cur next 需要空间,为常数大小的额外空间。

递归法

我个人理解递归法的方法就是画树。递归的过程和栈的入栈和出栈一样,通过递归函数一层一层入栈,然后出栈的时候计算(或者执行某些代码),然后将结果代入下一步,直到遇到终止条件为止,最后将结果返回。

image.png

为了讲解简单,我们拿翻转 [1, 2, 3]举例,最后结果是 [3, 2, 1], 当然,链表最终都是指向 null 的。为了直观,我写成 1 -> 2 -> 3 -> null

第一步:画树

我们知道,要翻转 1 -> 2 -> 3 -> null,可以像二叉树的形式拆解。

  • (拆解1)可以先分解成 1 ->2 -> 3 -> null,理解为要翻转整个链表,需要先翻转 2 -> 3 -> null

  • (拆解2) 再将 2 -> 3 -> null 拆解成 2 ->3 -> null ,理解为继而需要翻转 3 -> null

  • (拆解3)再将 3 -> null 拆解成 3 ->null

由此,我们只需要将拆解的步骤从后向前改变指针,并将新的链表的头指针返回即可。

第二步: 考虑终止条件

如果 headnull 或者 head.nextnull, 我们直接将 head 返回即可。 3 -> null 翻转之后还是 3 -> null,也是 拆解1 的解。

if(!head || !head.next) return head;

第三步: 递推

如果 head.next 不为 null 的话,就将后续节点反转。

let newHead = reverseList(head.next);

第三步: 改变指针

如何翻转后续节点呢?改变指针指向即可。2 ->3 -> null怎么翻转,即拆解2。此时,head2 ->, head.next3

  1. 此时,3 的指针还指向 null : 3 -> null, 我们让它指向 2: head.next.next = head

  2. 再将 2 的指针指向 null 即可: head.next = null。如果它不是原链表的头节点,这个值会被后面的值覆盖掉,直到遇到头节点为止。

经历过以上步骤之后,现在 2 ->3 -> null 已经翻转成了 3 -> 2 -> null

接下来,1 ->2 -> 3 -> null 的翻转就是重复上述的 1) 2) 步骤。此时,2 -> 3 -> null 已经翻转成 3 -> 2 -> null,所以直接翻转 1 ->2 -> 3 -> null。 此时,head1 ->, head.next2

head.next.next = head;
head.next = null;

最终翻转结果为 null <- 1 <- 2 <- 3, 我们倒过来看就是 3 -> 2 -> 1 -> null,即最终的结果。记得最后要将结果返回。

完整代码

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {

    if(!head || !head.next) return head;

    let newHead = reverseList(head.next);
    
    head.next.next = head; 
    head.next = null;
    
    return newHead       

};

复杂度分析

时间复杂度: O(n) 这里链表有 n 个,每个链表都需要遍历一遍。

空间复杂度: O(n) 递归深度为 n, 可以理解为需要的栈的空间为 O(n)

总结

反转链表可以使用迭代法和递归法。

迭代法需要三个临时变量(prevcurnext),修改指针的时候,需要保存当前节点的下一个指针 next = cur.next,以免后续节点丢失,然后递进节点 prev = cur; cur = next,最后返回结果即可。

递归需要拆解,想清楚拆解的步骤即可。画树是为了理解,然后将拆解的步骤一步一步往前推即可。