33.【必备】链表高频题目和必备技巧

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解034【必备】链表高频题目和必备技巧_哔哩哔哩_bilibili

一.相交链表

题目:相交链表

算法原理

  • 整体思路
    • 这个算法的目的是找到两个无环链表相交的第一个节点。首先计算两个链表的长度差,然后根据长度差调整指针的起始位置,使得两个指针到相交节点的距离相同,最后同时移动两个指针,直到找到相交节点。
  • 具体原理
    • 链表长度计算与比较
      • 首先通过两个循环分别计算链表(h1)和(h2)的长度(不包含头节点)。在计算(h1)长度的同时,使用变量(diff)记录长度差值(初始为(h1)的长度),计算(h2)长度时则不断更新这个差值((diff)减去(h2)的长度)。
      • 如果两个链表的尾节点不相同(即(a!=b)),说明这两个链表不相交,直接返回(null)。
    • 指针调整
      • 根据计算出的长度差值(diff)来调整指针的起始位置。如果(diff\geq0),说明(h1)较长,将(a)指向(h1)的头节点,(b)指向(h2)的头节点;否则,将(a)指向(h2)的头节点,(b)指向(h1)的头节点,并取(diff)的绝对值。
      • 然后将较长链表的指针(a)向前移动(diff)步,这样就使得两个指针到相交节点的距离相同。
    • 相交节点查找
      • 最后,同时移动(a)和(b)指针,每次移动一步。当(a = b)时,就找到了相交节点,如果一直没有相等,则说明没有相交节点,最终返回(a)(如果没有相交则(a)为(null))。

代码实现

// 返回两个无环链表相交的第一个节点
// 测试链接 : https://leetcode.cn/problems/intersection-of-two-linked-lists/
public class Code01_IntersectionOfTwoLinkedLists {
    // 提交时不要提交这个类
    public static class ListNode {
        public int val;
        public ListNode next;
    }

    // 提交如下的方法
    public static ListNode getIntersectionNode(ListNode h1, ListNode h2) {
        // 如果其中一个链表为空,直接返回null,因为不存在相交节点
        if (h1 == null || h2 == null) {
            return null;
        }
        ListNode a = h1;
        ListNode b = h2;
        int diff = 0;
        // 计算链表h1的长度(不包含头节点),同时记录长度差值
        while (a.next!= null) {
            a = a.next;
            diff++;
        }
        // 计算链表h2的长度(不包含头节点),并更新长度差值
        while (b.next!= null) {
            b = b.next;
            diff--;
        }
        // 如果两个链表的尾节点不相同,说明两个链表不相交,直接返回null
        if (a!= b) {
            return null;
        }
        // 根据长度差值调整指针的起始位置
        // 如果diff >= 0,将a指向h1的头节点,b指向h2的头节点
        // 否则将a指向h2的头节点,b指向h1的头节点
        if (diff >= 0) {
            a = h1;
            b = h2;
        } else {
            a = h2;
            b = h1;
        }
        diff = Math.abs(diff);
        // 将较长链表的指针向前移动diff步,使得两个指针到相交节点的距离相同
        while (diff--!= 0) {
            a = a.next;
        }
        // 同时移动a和b指针,直到找到相交节点
        while (a!= b) {
            a = a.next;
            b = b.next;
        }
        // 返回相交节点,如果没有相交则返回null
        return a;
    }
}

二.K个一组翻转链表

题目:K 个一组翻转链表

算法原理

  • 整体思路
    • 这个算法的目标是每(k)个节点一组对链表进行翻转。首先确定第一组的起始和结束节点,对第一组进行翻转并处理特殊情况(涉及链表头节点的改变)。然后循环处理后续的组,确定每组的起始和结束节点,进行翻转操作,并连接好相邻的组,最后返回处理后的链表头节点。
  • 具体原理
    • 确定组的结束节点(teamEnd方法)
      • 从传入的节点(s)开始,每次循环减少(k)的值并将(s)移动到下一个节点,直到(k = 0)或者(s = null)。如果因为节点不足(k)个而提前退出循环,可能返回(null),否则返回找到的这一组的结束节点。
    • 翻转一组节点(reverse方法)
      • 首先将(e)指向它的下一个节点,因为翻转后当前组的最后一个节点要指向它后面的节点(下一组的开始节点)。
      • 使用(pre)、(cur)和(next)三个指针来辅助翻转操作。(pre)初始化为(null),指向当前节点的前一个节点;(cur)初始化为当前组的起始节点(s),是当前正在处理的节点;(next)用于保存当前节点(cur)的下一个节点。
      • 在循环中,先保存(cur)的下一个节点(next),然后将(cur)的(next)指针指向前一个节点(pre)实现翻转,接着更新(pre)为当前节点(cur),再更新(cur)为之前保存的下一个节点(next),直到(cur)到达原来的结束节点(e)(即翻转后的开始节点的下一个节点)。最后将原来的起始节点(s)的(next)指针指向(e),完成这一组节点的翻转。
    • 主函数reverseKGroup算法原理
      • 确定第一组的情况:
        • 首先确定第一组的起始节点(start)为链表头(head),通过teamEnd方法确定第一组的结束节点(end)。
        • 如果(end = null),说明剩余节点不足(k)个,直接返回原链表头节点(head)。
        • 如果第一组节点足够(k)个,将新的头节点设为这一组的结束节点(因为翻转后这一组的最后一个节点成为新的头),然后对第一组节点进行翻转操作。
      • 处理后续组:
        • 翻转之后(start)变成了上一组的结尾节点,用(lastTeamEnd)记录。
        • 只要(lastTeamEnd)的下一个节点不为空就继续循环处理后续组。在循环中,确定下一组的起始节点(start)和结束节点(end),如果(end = null),说明剩余节点不足(k)个,直接返回已经处理好的链表头。
        • 对这一组节点进行翻转操作,然后将上一组的结尾节点(lastTeamEnd)的(next)指针指向这一组的结束节点(翻转后这一组的最后一个节点),并更新(lastTeamEnd)为这一组原来的起始节点(现在的结尾节点)。
      • 最后返回处理后的链表头节点(head)。

代码实现

// 每k个节点一组翻转链表
// 测试链接:https://leetcode.cn/problems/reverse-nodes-in-k-group/
public class Code02_ReverseNodesInkGroup {
    // 不要提交这个类,这是内部辅助类,用于表示链表节点
    public static class ListNode {
        public int val;
        public ListNode next;
    }

    // 提交如下的方法,用于每k个节点一组翻转链表
    public static ListNode reverseKGroup(ListNode head, int k) {
        // start表示每组的起始节点,初始化为头节点
        ListNode start = head;
        // end表示每组的结束节点,通过teamEnd方法确定
        ListNode end = teamEnd(start, k);
        // 如果end为null,说明剩余节点不足k个,直接返回原链表头节点
        if (end == null) {
            return head;
        }
        // 第一组很特殊,因为这一组的翻转会涉及到链表头节点的改变
        // 将新的头节点设为这一组的结束节点(翻转后这一组的最后一个节点成为新的头)
        head = end;
        // 对第一组节点进行翻转操作
        reverse(start, end);
        // 翻转之后start变成了上一组的结尾节点
        ListNode lastTeamEnd = start;
        // 循环处理后续的组,只要上一组的结尾节点的下一个节点不为空就继续
        while (lastTeamEnd.next!= null) {
            // 确定下一组的起始节点
            start = lastTeamEnd.next;
            // 找到下一组的结束节点
            end = teamEnd(start, k);
            // 如果end为null,说明剩余节点不足k个,直接返回已经处理好的链表头
            if (end == null) {
                return head;
            }
            // 对这一组节点进行翻转操作
            reverse(start, end);
            // 将上一组的结尾节点的next指针指向这一组的结束节点(翻转后这一组的最后一个节点)
            lastTeamEnd.next = end;
            // 更新lastTeamEnd为这一组原来的起始节点(现在的结尾节点)
            lastTeamEnd = start;
        }
        // 返回处理后的链表头节点
        return head;
    }

    // 当前组的开始节点是s,往下数k个找到当前组的结束节点返回
    public static ListNode teamEnd(ListNode s, int k) {
        // 从传入的节点s开始,每次循环减少k的值,直到k为0或者s为null
        while (--k!= 0 && s!= null) {
            // 将s移动到下一个节点
            s = s.next;
        }
        // 返回找到的节点,如果因为节点不足k个而提前退出循环,可能返回null
        return s;
    }

    // s -> a -> b -> c -> e -> 下一组的开始节点
    // 上面的链表通过如下的reverse方法调整成 : e -> c -> b -> a -> s -> 下一组的开始节点
    public static void reverse(ListNode s, ListNode e) {
        // 先将e指向它的下一个节点,因为翻转后当前组的最后一个节点要指向它后面的节点(下一组的开始节点)
        e = e.next;
        // pre初始化为null,用于辅助翻转操作,指向当前节点的前一个节点
        ListNode pre = null;
        // cur初始化为当前组的起始节点s,是当前正在处理的节点
        ListNode cur = s;
        // next用于保存当前节点cur的下一个节点
        ListNode next = null;
        // 循环进行节点翻转操作,直到cur到达原来的结束节点e(即翻转后的开始节点的下一个节点)
        while (cur!= e) {
            // 保存当前节点cur的下一个节点
            next = cur.next;
            // 将当前节点cur的next指针指向前一个节点pre,实现翻转
            cur.next = pre;
            // 更新pre为当前节点cur,因为cur的下一个节点翻转后会成为新的cur
            pre = cur;
            // 更新cur为之前保存的下一个节点next
            cur = next;
        }
        // 将原来的起始节点s的next指针指向e,完成这一组节点的翻转
        s.next = e;
    }
}

三.随机链表的复制

题目:随机链表的复制

算法原理

  • 整体思路
    • 这个算法用于复制带随机指针的链表。分三步进行:首先在原链表的每个节点后面插入一个复制节点;然后利用原链表和新插入节点的结构关系,设置每个新节点的随机指针;最后将新老链表分离,得到原链表的完整副本。
  • 具体原理
    • 插入复制节点
      • 在第一个循环中,通过while (cur!= null)遍历原链表。对于每个节点cur
        • 先保存当前节点cur的下一个节点next = cur.next
        • 创建一个新节点,其值与当前节点cur相同(new Node(cur.val)),并将其插入到cur后面(cur.next = new Node(cur.val))。
        • 将新节点的下一个节点设置为之前保存的next节点(cur.next.next = next)。
        • 最后将cur移动到下一个原节点(即之前保存的next节点)。这样原链表的结构就变成了1 -> 1' -> 2 -> 2' -> 3 -> 3' ->...,其中1'2'3'等是对应的复制节点。
    • 设置复制节点的随机指针
      • 在第二个循环中,再次遍历链表。对于每个原节点cur
        • 先保存当前节点cur的下一个节点的下一个节点(即下一个原节点的下一个节点)next = cur.next.next
        • 获取当前原节点cur后面的复制节点copy = cur.next
        • 如果当前原节点cur的随机指针random不为null,则复制节点copy的随机指针应该指向原节点cur的随机指针指向的节点的下一个节点(即对应的复制节点)。这是因为原链表和新插入的复制节点是交替排列的,所以原节点的随机指针指向的节点的下一个节点就是复制节点的随机指针应该指向的位置。如果cur.randomnull,则copy.random也为null
        • 最后将cur移动到下一个原节点。
    • 新老链表分离
      • 在第三个循环中:
        • 首先保存当前节点cur的下一个节点的下一个节点(即下一个原节点的下一个节点)next = cur.next.next
        • 获取当前原节点cur后面的复制节点copy = cur.next
        • 将原链表中当前节点cur的下一个节点重新连接为下一个原节点(即恢复原链表结构)cur.next = next
        • 将复制链表中当前复制节点copy的下一个节点连接为下一个原节点的下一个节点(如果next不为nullcopy.next = next!= null? next.next : null
        • cur移动到下一个原节点。
      • 最后,新链表的头节点是原链表头节点的下一个节点(ans = head.next),返回这个节点就得到了原链表的完整副本,新链表包含了相同的值和正确的随机指针关系。

代码实现

// 复制带随机指针的链表
// 测试链接 : https://leetcode.cn/problems/copy-list-with-random-pointer/
public class Code03_CopyListWithRandomPointer {
    // 不要提交这个类,这是内部辅助类,用于表示链表节点
    public static class Node {
        public int val;
        public Node next;
        public Node random;

        // 构造函数,用于创建一个新节点并初始化其值
        public Node(int v) {
            val = v;
        }
    }

    // 提交如下的方法,用于复制带随机指针的链表
    public static Node copyRandomList(Node head) {
        // 如果原链表头节点为null,直接返回null,因为空链表的副本也是空链表
        if (head == null) {
            return null;
        }
        Node cur = head;
        Node next = null;
        // 1 -> 2 -> 3 ->...
        // 变成 : 1 -> 1' -> 2 -> 2' -> 3 -> 3' ->...
        // 这个循环的目的是在原链表的每个节点后面插入一个复制节点
        while (cur!= null) {
            // 保存当前节点cur的下一个节点
            next = cur.next;
            // 创建一个新节点,其值与当前节点cur相同,并将其插入到cur后面
            cur.next = new Node(cur.val);
            // 将新节点的下一个节点设置为之前保存的next节点
            cur.next.next = next;
            // 将cur移动到下一个原节点(即之前保存的next节点)
            cur = next;
        }
        cur = head;
        Node copy = null;
        // 利用上面新老节点的结构关系,设置每一个新节点的random指针
        while (cur!= null) {
            // 保存当前节点cur的下一个节点的下一个节点(即下一个原节点的下一个节点)
            next = cur.next.next;
            // 获取当前原节点cur后面的复制节点
            copy = cur.next;
            // 如果当前原节点cur的random指针不为null,
            // 则复制节点copy的random指针应该指向原节点cur的random指针指向的节点的下一个节点(即对应的复制节点)
            // 如果cur.random为null,则copy.random也为null
            copy.random = cur.random!= null? cur.random.next : null;
            // 将cur移动到下一个原节点
            cur = next;
        }
        Node ans = head.next;
        cur = head;
        // 新老链表分离 : 老链表重新连在一起,新链表重新连在一起
        while (cur!= null) {
            // 保存当前节点cur的下一个节点的下一个节点(即下一个原节点的下一个节点)
            next = cur.next.next;
            // 获取当前原节点cur后面的复制节点
            copy = cur.next;
            // 将原链表中当前节点cur的下一个节点重新连接为下一个原节点(即恢复原链表结构)
            cur.next = next;
            // 将复制链表中当前复制节点copy的下一个节点连接为下一个原节点的下一个节点(如果next不为null)
            copy.next = next!= null? next.next : null;
            // 将cur移动到下一个原节点
            cur = next;
        }
        // 返回新链表的头节点,新链表是原链表的副本,包含了相同的值和正确的random指针关系
        return ans;
    }
}

 四.回文链表

题目:回文链表

算法原理

  • 整体思路
    • 这个算法用于判断链表是否为回文结构。首先找到链表的中点,然后将中点之后的链表部分逆序,接着从链表的头和逆序后的尾开始向中间比较节点的值,如果都相等则为回文链表,最后将链表恢复到原来的结构并返回判断结果。
  • 具体原理
    • 寻找链表中点(快慢指针法)
      • 使用快慢指针slowfast,其中slow每次移动一步,fast每次移动两步。当fast.next!= nullfast.next.next!= null时,不断移动slowfast。当循环结束时,slow指针就指向了链表的中点(如果链表节点个数为奇数个,slow指向正中间的节点;如果是偶数个,slow指向中间偏左的节点)。
    • 链表后半部分逆序
      • 以找到的中点slow为起点,将中点之后的链表部分逆序。通过precurnext三个指针来实现逆序操作。pre初始为slowcurpre.nextnextcur.next。先将pre.next设为null,然后在循环中,不断将cur的下一个节点指向pre,并依次更新precurnext,直到curnull,此时链表后半部分已经逆序。
    • 判断是否为回文链表
      • 经过前面的操作,链表变成了从左右两侧往中间指的结构(head ->... -> slow <-... <- pre)。使用left指针从链表头开始,right指针从逆序后的尾(pre)开始,同时向中间移动并比较每个节点的值。如果在比较过程中发现left.val不等于right.val,则将结果ans设为false并跳出循环。
    • 恢复链表结构
      • 本着不改变原始链表结构的原则,在得到判断结果后,将逆序后的链表部分再逆序回来,恢复到原始的链表结构。通过与之前逆序操作类似的方法,使用curprenext指针来实现。
      • 最后返回判断结果ans,如果anstrue,则链表是回文结构;否则不是。

代码实现

// 判断链表是否是回文结构
// 测试链接 : https://leetcode.cn/problems/palindrome-linked-list/
public class Code04_PalindromeLinkedList {

    // 不要提交这个类
    public static class ListNode {
        public int val;
        public ListNode next;
    }

    // 提交如下的方法
    public static boolean isPalindrome(ListNode head) {
        if (head == null || head.next == null) {
            return true;
        }
        ListNode slow = head, fast = head;
        // 找中点
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        // 现在中点就是slow,从中点开始往后的节点逆序
        ListNode pre = slow;
        ListNode cur = pre.next;
        ListNode next = null;
        pre.next = null;
        while (cur != null) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        // 上面的过程已经把链表调整成从左右两侧往中间指
        // head -> ... -> slow <- ... <- pre
        boolean ans = true;
        ListNode left = head;
        ListNode right = pre;
        // left往右、right往左,每一步比对值是否一样,如果某一步不一样答案就是false
        while (left != null && right != null) {
            if (left.val != right.val) {
                ans = false;
                break;
            }
            left = left.next;
            right = right.next;
        }
        // 本着不坑的原则,把链表调整回原来的样子再返回判断结果
        cur = pre.next;
        pre.next = null;
        next = null;
        while (cur != null) {
            next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return ans;
    }

}

五.链表类题目注意点

1,如果笔试中空间要求不严格,直接使用容器来解决链表问题

2,如果笔试中空间要求严格、或者在面试中面试官强调空间的优化,需要使用额外空间复杂度O(1)的方法

3,最常用的技巧-快慢指针

4,链表类题目往往都是很简单的算法问题,核心考察点也并不是算法设计,是coding能力

5,这一类问题除了多写多练没有别的应对方法

个人建议:链表类问题既然练的就是coding,那么不要采取空间上讨巧的方式来练习

注意:链表相关的比较难的问题是约瑟夫环问题,会在【扩展】板块讲解,变形很多

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值