0
点赞
收藏
分享

微信扫一扫

【备战秋招】轻松拿捏大厂高频算法题 —— 反转链表篇


🍥 目录


🍔 反转链表

🥗 题目描述

🌮 解题分析

为了将给定的链表反转,首先需要指定当前遍历到的结点head,其次反转链表的本质是将 head 结点指向其前一个结点,因此存储 head 结点的前驱结点为 pre。既然 head 结点反转需要指前一个结点 pre ,那修改前 head 的下一个结点就存储在 next 中,保证遍历地进行。总结一下就是:

  • head:当前遍历的结点
  • pre:head 结点的前一个结点,head为头结点时值为 null。
  • next:反转 head 结点前的下一个结点

下面先讲讲如何反转一个链表,假定反转前首先初始状态如下:

image-20220412184424283

遍历的结点为 head ,反转的过程实际上是修改head.next为 head 的前一个元素pre,也就是执行head.next=pre
在这里插入图片描述
可以看到:head 结点的 next 指向已经被修改,覆盖了原来的 next 指向 , 此时就体现出 next 指针的重要性。将 pre 指向 head, head 指向其修改前的下一个元素 next ,进入下一次修改的循环:
image-20220412185345596
当 head 指向为 NULL 时,循环结束,此时的真正的头结点是 head 的前一个元素 pre ,因此将 pre 作为结果返回。

🧀 参考代码(Java语言)

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null) {
            return head;
        }
        // 指向 head 的前一个元素
        ListNode pre = null;
        // 循环结束的条件为 head == null
        while (head != null) {
            // 用 next 保存修改前的下一个元素位置
            ListNode next = head.next;
            // 修改
            head.next = pre;
            pre = head;
            head = next;
        }
        return pre;
    }
}

🍟 反转链表 II

🥗 题目描述

🌮 解题分析

反转链表代码中传入参数是一个链表的头结点,返回的是反转后的头结点。那只需要找到反转部分的头部结点作为参数传入后,返回的不就是反转后链表的头部结点了嘛!

但如果直接传入反转头结点进入函数中,那之后所有的结点都会被反转!因此将反转部分尾与其之后结点断开是一个不错的处理方式。

因此只需要做到下面几点就能够优雅地解决这道题目:

  • 反转部分头结点: h
  • 反转部分头结点前驱结点: preH
  • 反转部分尾结点: t
  • 反转部分尾结点后继结点: afterT

反转可能出现在头结点上,这种情况下 preH 的处理会相当的困难。可以使用一个链表中常见的方法:添加一个虚拟头结点preHead的方式,使得虚拟头结点做为原来头结点head的前驱结点,那么preHead.next就是需要返回的结果。

一图胜千言,下面这张图更好地阐述了上面的构思:

在这里插入图片描述

但仔细观察:反转之后 preH.nexth.next 指向明显出现错误,因此重新调整链表是不可缺少的一环。

根据上图不难发现:调整过程只需要将 preH.next 指向反转后的头结点 th.next 指向 afterT 就能够完成调整!
image-20220412195322766
当调整完成后,返回 preHead.next 即是新的头结点也是题目的答案。

🧀 参考代码(Java语言)

class Solution {
    // 上一题翻转链表的代码
    public ListNode reverseList(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode pre = null;
        while (head != null) {
            ListNode next = head.next;
            head.next = pre;
            pre = head;
            head = next;
        }
        return pre;
    }
	// 题目函数
    public ListNode reverseBetween(ListNode head, int left, int right) {
        // 虚拟头结点 preHead
        ListNode preHead = new ListNode();
        preHead.next = head;
        ListNode preH, h, t, tmp, afterT;
        // 初始化
        t = afterT = preH = tmp = h = preHead;
        // 位置参数,从0开始
        int idx = 0;
        while (tmp != null) {
            // tmp 指向 preH
            if (idx == left - 1) {
                h = tmp.next;
                preH = tmp;
            }
            // tmp 指向 t
            if (idx == right) {
                t = tmp;
                afterT = tmp.next;
                // 断开不需要翻转的部分
                tmp.next = null;
                break;
            }
            tmp = tmp.next;
            ++idx;
        }
        // 等价于 preH.next = reverseList(h);
        reverseList(h);
        preH.next = t;
        //修改h.next的指向完成尾部的拼接
        h.next = afterT;
        return preHead.next;
    }
}

🍕 K个一组反转链表

🥗 题目描述

🌮 解题分析

此时我们可以换换思路,用之前两道题的解题思路解决这道题目:每 K 组进行一次链表反转,是不是对应着反转 [ 1 , k ] , [ k + 1 , 2 k ] , [ 2 k + 1 , 3 k ] . . . [1,k],[k+1,2k],[2k+1,3k]... [1,k],[k+1,2k],[2k+1,3k]... 的链表元素呢,而上一题 反转链表 II 中处理的正好就是这种某段链表反转的问题!因此只需要借用上一题的代码就能够优雅地拿捏这道题。

为了方便处理首先定义一个头结点的前驱结点preHead,而每次传入 反转链表 II 参数是头结点,即传入preHead.next。定义一个计数器 idx 同时定义一个遍历链表的指针 tmp,tmp 开始时指向头结点 head ,例如下图所示。

  • 计数器 idx 记录遍历次数
  • 遍历指针 tmp 遍历链表
  • 记录反转前下一个结点 next

image-20220412203223704

当计数器 idx%k==0时,tmp指向需要一组的最后一个结点,此时进行 K 个一组的反转操作,反转 [ i d x − k + 1 , k ] [idx-k+1,k] [idxk+1,k] 链表结点。

image-20220412204404630

第一次转置会导致preHead.next指向出错,因此反转后将新的头结点赋值给 preHead.next。当 tmp 指向为空时循环结束,最终返回preHead.next即是结果。

image-20220412204303391

🧀 参考代码(Java语言)

class Solution {
    // 反转链表 代码
    public ListNode reverseList(ListNode head) {
        if (head == null) {
            return head;
        }
        ListNode pre = null;
        while (head != null) {
            ListNode next = head.next;
            head.next = pre;
            pre = head;
            head = next;
        }
        return pre;
    }
    // 反转链表II 代码
    public ListNode reverseBetween(ListNode head, int left, int right) {
        ListNode preHead = new ListNode();
        preHead.next = head;
        ListNode preH, h, t, tmp, afterT;
        t = afterT = preH = tmp = h = preHead;
        int idx = 0;
        while (tmp != null) {
            if (idx == left - 1) {
                h = tmp.next;
                preH = tmp;
            }
            if (idx == right) {
                t = tmp;
                afterT = tmp.next;
                tmp.next = null;
                break;
            }
            tmp = tmp.next;
            ++idx;
        }
        reverseList(h);
        preH.next = t;
        h.next = afterT;
        return preHead.next;
    }
    // 题目函数
    public ListNode reverseKGroup(ListNode head, int k) {
        // preHead 声明为头结点的前驱结点
        ListNode preHead = new ListNode();
        preHead.next = head;
        // tmp 遍历链表
        ListNode tmp = head;
        int idx = 0;
        while (tmp != null) {
            ++idx;
            ListNode next = tmp.next;
            if (idx % k == 0) {
                preHead.next = reverseBetween(preHead.next, idx - k + 1, idx);
            }
            tmp = next;
        }
        return preHead.next;
    }
}

🧡 写在最后

✨如果觉得本篇博客还写的不错的话可以三连支持一下博主✨

image-20200916114846002

举报

相关推荐

0 条评论