算法通关村 | 第二关 | 青铜篇(链表反转)

        终于闯过了第一关了 ,在座的各位是否有那么一丢丢的收获呢,反正我是有的(doge),如果我有什么解释错的或还有优化的方法,欢迎各位佬们拷打~

算法通关村 | 第二关 | 青铜篇(链表反转)_第1张图片

         从本文开始,我们就正式进入第二关了,本关的主题是 “链表反转”  ,包括了关于链表反转的一系列问题,如普通的单链表直接反转指定区间的反转两两交换链表的结点等。


链表的基础知识前面都已经了解,本关就不在BB了,话不多说,直接进入正题!

先来个图,镇个场子。

算法通关村 | 第二关 | 青铜篇(链表反转)_第2张图片

         大一眼看,反转链表在牛客网的面试高频榜单排名榜首,可见这道题目的热度和面试官对这道题的考察度,所以我们必须要了解这道热门题目,以备不时之需。

为什么链表反转那么重要?

        因为反转链表涉及节点的增、删等操作,能非常有效考察笔者的思维能力和代码驾驭能力。另外很多题目也都要用它来做基础,例如我们上面说的指定区间反转链表K个一组翻转。还有一些在内部的某个过程用到了反转,例如两个链表生成相加链表。

        可见了解了链表反转我们就有了可以解决更多问题的基础,这也是为什么算法这个东西为什么越学越少的原因,比如我们了解的底层的实现,那各种的链表问题其实就是基础的各种延伸,我们甚至可以自己想算法题,自己解。

至于那些特别不好理解的算法,那就是给大佬们的舞台了,像俺这样的小菜鸡看半天也没思路!

算法通关村 | 第二关 | 青铜篇(链表反转)_第3张图片

 扯远了,回归正题,我们来继续学习 链表反转


先看题,题目链接:206. 反转链表 - 力扣(LeetCode)

题目介绍

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

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

如下图:

算法通关村 | 第二关 | 青铜篇(链表反转)_第4张图片

思路(直接操作链表)

对于这道题而言,定义一个新链表显然是不合理的,麻烦而且浪费空间,其实只需要改变链表的next指针的指向,直接将链表反转 ,如上图所示。

具体的移动如下:

算法通关村 | 第二关 | 青铜篇(链表反转)_第5张图片

 即:

  1. 先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
  2. 然后开始反转,首先要把 curnext 节点用tmp指针保存一下。
  3. 循环走代码,继续移动pre和cur指针。
  4. 最后,cur 指向null,循环结束,此时return pre指针就指向了新的头结点。

 代码如下:

// 理解了上面的图代码很好理解
public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        ListNode temp = null;
        while(cur != null){
            temp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = temp;
        }
        return pre;
    }
  另一种思路(虚拟头结点)

        除了上面的一种解法,还有一种是使用虚拟头结点的方法,这种方法相对较简单,因为如果使用了虚拟头结点,其实大大简化了链表的操作,等于是链表的插入和删除操作了。如果面试的时候面试官让用的的话,那自然是好的,如果不让,那我们也会常规的解法(根本不怕doge)。

上个图,理解了该图就知道的大概该如何思考了:

算法通关村 | 第二关 | 青铜篇(链表反转)_第6张图片

代码如下:

// 增加虚头结点,使用头插法实现链表翻转
public ListNode reverseList(ListNode head) {
        ListNode dummy = new ListNode(-1);
        dummy.next = null;
        ListNode cur = head;
        while(cur != null){
            ListNode tmp = cur.next;
            cur.next = dummy.next;
            dummy.next = cur;
            cur = tmp;
        }
        return dummy.next;
    }
再次提醒

        我们在创建虚拟头结点是使用的是其 .next 而不是结点本身,如 dummy.next = null; 所以括号内的内容对编码并没有什么太大的影响。

小结

        以上两种方法是解决链表反转的重要的思路,尤其的高频且高效,为了便于记忆我们可以把它俩理解为,带虚拟头结点和不带虚拟头结点的方法。


补充(递归正向)

还有一种方法就是递归,对于链表反转这个问题来说,递归的方法还比较好理解。感兴趣的佬们可以看看。

思路如下:

  • 这里的 reverse() 方法就是递归函数,它接受两个参数 pre 和 cur,分别表示前一个节点和当前节点。
  • 首先,如果当前节点为空,表示链表已经遍历完成,我们直接返回 prev,即反转后的链表头节点。
  • 否则,我们通过 temp 保存当前节点的下一个节点,然后将当前节点的 next 指针指向 prev,实现了当前节点的反转。
  • 接着,我们递归调用 reverse() 方法,传入 cur 作为新的 prev,传入 temp 作为新的 cur,这样就实现了从后向前递归地反转链表。

具体代码如下:

class Solution {
    public ListNode reverseList(ListNode head) {
        return reverse(null,head);
    }
    private ListNode reverse(ListNode pre , ListNode cur){
        if(cur == null){
            return pre;
        }
        ListNode temp = null;
        temp = cur.next;// 保存下一个节点
        cur.next = pre;// 反转操作
        return reverse(cur, temp);
    }
}

使用栈

大致思路是把链表的所有元素都压入栈中,然后创建一个虚拟头结点指向cur,再就是循环出栈并添加到新链表中。

代码如下:

public ListNode reverseList(ListNode head) {
        if(head == null) {return null;}
        if(head.next == null) {return head;}
        Stack stack = new Stack<>();
        ListNode dummy = new ListNode(-1);
        ListNode cur = head;
        while(cur != null){
            stack.push(cur);
            cur = cur.next;
        }
        cur = dummy;
        while (!stack.isEmpty()) {
        ListNode node = stack.pop();
        cur.next = node;
        cur = cur.next;
    }
    // 最后一个元素的next要赋值为空
    cur.next = null;
    return dummy.next;
    }

 算法通关村 | 第二关 | 青铜篇(链表反转)_第7张图片

 

后续会进行跟新链表反转相关问题! 加油啊,hxd们~

你可能感兴趣的:(算法通关村,链表,算法,笔记)