LeetCode Hot 100 链表专题(四)· 3 题(Java)

本篇题目 :138. 随机链表的复制 | 148. 排序链表 | 25. K 个一组翻转链表

统一结构:① 题目分析与解题思路 / ② 常见解法(附代码)及讲解 / ③ 知识点总结


138. 随机链表的复制

① 题目分析 & 解题思路

题意

普通链表每个节点只有 next,这题多了一个 random 指针,随机指向链表内任意节点或者 null。要求深拷贝 :全部新建节点,新链表所有指针(next、random)都只能指向新链表里面的节点,不能指向原链表节点,并且保持和原链表完全一样的结构。

深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。

难点:遍历的时候,random 指向的节点可能还没创建出来 ,所以不能一边遍历一边直接给 random 赋值,否则拿到的是 null。

核心思路(两种主流思路)

思路 A:哈希表 (简单好写,空间 O(n)O(n)O(n))

  1. 第一次遍历原链表:创建所有新节点,用 HashMap<原节点, 新节点> 建立映射关系;
  2. 第二次遍历原链表:借助哈希表,给每个新节点设置 next 和 random;
    • newNode.next = map.get(oldNode.next)
    • newNode.random = map.get(oldNode.random)
  3. 返回新头结点 map.get(head)。

思路 B:原地拼接链表 (不用哈希表,空间 O(1)O(1)O(1),面试常考)

  1. 拼接:在原链表每一个旧节点后面插入它的拷贝节点;A → A' → B → B' → C → C'
  2. 设置 random:旧节点的 random 如果是 R,那么拷贝节点的 random 就是 R.next;
  3. 拆分链表:把 A, A', B, B' 拆开,原链表恢复原样,分离出纯拷贝链表。

一句话对比:哈希表把"旧→新"映射存进表;原地法把映射藏在结构里------旧节点的下一个位置就是它的副本。


② 常见解法(附代码)及讲解

节点定义(题目自带):

java 复制代码
class Node {
    int val;
    Node next;
    Node random;
    public Node(int val) {
        this.val = val;
        this.next = null;
        this.random = null;
    }
}

解法 1:HashMap 哈希映射(推荐新手优先掌握)

HashMap 的设计:Map<Node, Node> map

  • key = 原链表的旧节点
  • value = 我们新创建出来的拷贝节点
  • 作用:建立一对一映射,只要拿到旧节点,立刻找到它对应的新节点。
java 复制代码
class Solution {
    public Node copyRandomList(Node head) {
        // 边界:原链表是空,直接返回null
        if (head == null) {
            return null;
        }
        // 哈希表:旧节点 → 拷贝节点
        Map<Node, Node> map = new HashMap<>();
        Node cur = head;

        // == 第一轮遍历:只创建节点,不连指针 ==
        while (cur != null) {
            // 根据旧节点的值,新建一个拷贝节点
            Node copyNode = new Node(cur.val);
            // 存入映射表:旧节点对应它的拷贝
            map.put(cur, copyNode);
            // 指针往后走
            cur = cur.next;
        }

        // 指针重置回表头,准备第二轮遍历
        cur = head;

        // == 第二轮遍历:给拷贝节点挂上 next 和 random ==
        while (cur != null) {
            // 通过旧节点取出它对应的拷贝节点
            Node copyNode = map.get(cur);

            // 1. 设置 next
            // cur.next 是【原链表下一个旧节点】,map.get 拿到它对应的拷贝节点
            copyNode.next = map.get(cur.next);

            // 2. 设置 random
            // cur.random 是【原链表 random 指向的旧节点】,map.get 拿到它对应的拷贝节点
            copyNode.random = map.get(cur.random);

            cur = cur.next;
        }

        // 返回原 head 对应的拷贝节点,也就是新链表头
        return map.get(head);
    }
}
分步拆解:拿示例 2 跟着走

head = [[1,1],[2,1]],原链表:

java 复制代码
旧A(val=1)  next → 旧B(val=2)  next → null
旧A.random → 旧B
旧B.random → 旧B

第一轮循环:只造节点、填 map,不连任何指针 (cur 从旧 A 出发)

  1. cur = 旧A → new Node(1) 得到拷贝 A → map.put(旧A, 拷贝A) → cur 移到旧 B
  2. cur = 旧B → new Node(2) 得到拷贝 B → map.put(旧B, 拷贝B) → cur = null,第一轮结束

此时 map 里已有全部映射(旧A→拷贝A、旧B→拷贝B)。重点 :所有拷贝节点都已存在内存里,不管 random 指向哪个都能找到。

第二轮循环:连接 next 和 random (cur 重置回旧 A)

  1. cur = 旧A → copyNode = map.get(旧A) = 拷贝A
    • cur.next = 旧B → map.get(旧B) = 拷贝B → 拷贝A.next = 拷贝B
    • cur.random = 旧B → map.get(旧B) = 拷贝B → 拷贝A.random = 拷贝B
    • cur 移到旧 B
  2. cur = 旧B → copyNode = map.get(旧B) = 拷贝B
    • cur.next = null → map.get(null) 返回 null → 拷贝B.next = null
    • cur.random = 旧B → map.get(旧B) = 拷贝B → 拷贝B.random = 拷贝B
    • cur = null,第二轮结束

最后返回 map.get(head) = 拷贝A,即新链表头。新链表:拷贝A(1) → 拷贝B(2) → null,且 拷贝A.random → 拷贝B、拷贝B.random → 拷贝B,完全符合深拷贝要求------所有指针都指向新节点,没有碰原链表。

三个关键细节 & 疑问解答

Q1:map.get(null) 为什么不会报错? HashMap 允许 get(null),key 不存在时直接返回 null。所以 cur.next == null 或 cur.random == null 时,map.get(null) 得到 null 正好赋给拷贝节点的指针,不用额外写 if 判断。

Q2:什么是深拷贝?和浅拷贝的区别?

  • ❌ 浅拷贝:Node copy = cur,只是拿引用,新变量和原节点是同一个对象,改一个另一个跟着变,random 依旧指向原链表节点,题目不允许;
  • ✅ 深拷贝:new Node(cur.val),在堆里开辟全新内存,是独立节点,和原节点互不干扰。

Q3:边界测试 case

  1. head = null:直接 return null,防止空指针;
  2. 某个节点 random 是 null:map.get(null) 得到 null,自动处理;
  3. 节点 random 指向自己(自环):map 里能拿到自己对应的拷贝节点,正常处理。

复杂度:两轮遍历 O(n)O(n)O(n) 时间;哈希表存 n 组映射 O(n)O(n)O(n) 空间,这就是哈希表方案的代价,原地法可压到 O(1)O(1)O(1)。

解法 2:原地拼接 + 拆分(O(1)O(1)O(1) 额外空间,进阶)

java 复制代码
class Solution {
    public Node copyRandomList(Node head) {
        // 边界:原链表为空,直接返回 null
        if (head == null) {
            return null;
        }
        Node cur = head;

        // == 第一步:在原节点后面插入拷贝节点 ==
        // 链表变化前:A -> B -> C -> null
        // 插入后:    A -> A' -> B -> B' -> C -> C' -> null
        while (cur != null) {
            // 根据当前旧节点的值,创建拷贝节点
            Node copy = new Node(cur.val);
            // 拷贝节点的 next 指向旧节点原来的下一个节点
            copy.next = cur.next;
            // 把拷贝节点接在旧节点的后面
            cur.next = copy;
            // cur 跳到下一个旧节点(跳过刚插入的拷贝节点)
            cur = copy.next;
        }

        cur = head;
        // == 第二步:给拷贝节点设置 random 指针 ==
        // 规律:旧节点 cur 的 random 是 R,则拷贝节点 cur.next 的 random 就是 R.next(R 对应的拷贝)
        while (cur != null) {
            Node copy = cur.next; // copy 就是 cur 对应的拷贝节点
            // 必须判断 random 不为空,否则 cur.random.next 空指针异常
            if (cur.random != null) {
                copy.random = cur.random.next;
            }
            // 跳到下一个旧节点
            cur = copy.next;
        }

        // == 第三步:拆分链表,分离原链表和拷贝链表 ==
        cur = head;
        Node dummy = new Node(0); // 虚拟头节点,用来拼接拷贝后的新链表
        Node p = dummy; // p 用来遍历、构建新链表
        while (cur != null) {
            Node copy = cur.next; // 取出拷贝节点
            cur.next = copy.next; // 恢复原链表的 next 指针,把原链表拆开
            p.next = copy;        // 将拷贝节点接到新链表上
            p = p.next;           // 新链表指针后移
            cur = cur.next;       // 原链表指针后移
        }
        // dummy.next 就是拷贝链表的头节点
        return dummy.next;
    }
}

三步要点回顾

  1. 插入:每个旧节点后紧跟自己的副本,利用相邻关系建立"旧节点 → 拷贝节点"的隐式映射,不再需要 HashMap;
  2. random 赋值 :核心公式 copy.random = cur.random.next,前提是 cur.random != null;因为 cur.random 是原链表的节点,它的下一个就是它的拷贝;
  3. 拆分:同时维护两条链表,把新旧两条链表分开,新链表单独摘出来,原链表结构复原。
  • 复杂度:时间 O(n)O(n)O(n),额外空间 O(1)O(1)O(1)(不算答案需要的 n 个节点)。

面试小贴士 :优先写哈希表版本,写完可以主动提一句"还有原地 O(1)O(1)O(1) 空间的优化方案",通常这就是下一问。


③ 知识点总结

常见出错点

  1. ❌ 浅拷贝 :直接 newNode = oldNode,新节点引用还是指向原链表节点,不是新建对象,直接报错;
  2. ❌ 第一次遍历就想给 random 赋值:random 指向的节点还没创建,拿到 null;
  3. ❌ 哈希表 put 顺序错误(还没 put 就先去 get),或者漏判 head == null,导致空指针;
  4. ❌ 原地法拆分链表时指针搞乱,把原链表也改坏,或者链表断链;
  5. ❌ 边界:head 为 null 的时候,没有直接返回 null;
  6. ❌ 原地设置 random 时,没有判断 old.random != null,直接写 old.random.next,空指针。

解法优缺点

  • ✅ 优点(哈希表):逻辑线性、好理解、不容易写崩,面试首选写法,不容易踩空指针大坑;
  • ❌ 缺点:需要额外哈希表存映射,占用 O(n)O(n)O(n) 空间;面试官后续大概率会追问"能不能不用 HashMap 做?"(即原地拼接拆分版本)。

知识点小结

  • 映射思维 :同一份"旧→新"映射,可以显式存(HashMap),也可以隐式藏(旧.next = 拷贝),两种解法只差这一层;
  • 指针三段式 :插入 → 改 random → 拆分,是"原地改结构"类题目的通用骨架。

148. 排序链表

① 题目分析 & 解题思路

题目 :给定单链表头节点,将链表升序排序并返回。进阶要求:O(nlog⁡n)O(n\log n)O(nlogn) 时间、O(1)O(1)O(1) 常数空间。

链表不支持随机访问,数组常用的快排、堆排不好直接写;归并排序天然适合链表------只需要改指针,不需要移动元素。

核心思路(自底向上归并排序,满足进阶要求):

  1. 先统计链表总长度 len;
  2. 分段归并:初始段长 subLen = 1,每轮翻倍(1→2→4→8...),直到 subLen ≥ len;
  3. 每一轮:从头开始,每次切出两段长度为 subLen 的链表,合并两个有序链表,再拼接回原链表;
  4. 用一个哑节点(哨兵)方便管理头节点,避免处理大量边界。

补充:还有自顶向下递归归并 写法,代码好写,但递归栈是 O(log⁡n)O(\log n)O(logn),不满足常数空间进阶要求。


② 常见解法(附代码)及讲解

解法一:自顶向下归并排序(简单好写,面试先写这个,空间 O(log⁡n)O(\log n)O(logn))

原理:分治。

  • 分割:快慢指针找到链表中点,断开,分成左右两个子链表;
  • 递归排序左、右;
  • 合并两个有序链表。
java 复制代码
class Solution {
    public ListNode sortList(ListNode head) {
        // 递归终止:空或者只有一个节点,本身有序
        if (head == null || head.next == null) return head;

        // 快慢指针找中点
        ListNode slow = head, fast = head.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next; // fast 从 head.next 开始
        }
        // slow 要停在中点前一个位置,mid = slow.next,再切断 slow.next
        ListNode mid = slow.next; // mid 是 slow 下一个,是中点
        slow.next = null; // ✅ 关键!切断左右链表,完全分开

        // 递归
        ListNode left = sortList(head);
        ListNode right = sortList(mid);
        return merge(left, right);
    }

    // 合并两个有序链表(LeetCode 21 原题)
    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                tail.next = l1;
                l1 = l1.next;
            } else {
                tail.next = l2;
                l2 = l2.next;
            }
            tail = tail.next;
        }
        tail.next = (l1 != null) ? l1 : l2;
        return dummy.next;
    }
}

✅ 优点:代码短,逻辑直观 ❌ 缺点:递归调用栈深度 O(log⁡n)O(\log n)O(logn),不满足进阶的 O(1)O(1)O(1) 空间。

为什么是 fast = head.next 而不是 head?

fast = head.next 时,slow 最后落在中点前一个节点 ,于是 mid = slow.next、切断 slow.next,左右链表完全分离------这个设定是专门为"归并排序分割链表"服务的。

一句话口诀:找中点用 fast = head;归并分割链表用 fast = head.next。

解法二:自底向上迭代归并(满足进阶:O(nlog⁡n)O(n\log n)O(nlogn) 时间,O(1)O(1)O(1) 空间,面试加分)

不递归,用循环模拟分段合并:

java 复制代码
class Solution {
    public ListNode sortList(ListNode head) {
        // 边界:空链表 / 只有一个节点,本身有序直接返回
        if (head == null || head.next == null) {
            return head;
        }

        // 第一步:统计链表总节点数量,用来控制归并的分段长度
        int totalLen = 0;
        ListNode p = head;
        while (p != null) {
            totalLen++;
            p = p.next;
        }

        // 哑节点(哨兵):统一处理头节点变化,不用单独判断谁是新头
        ListNode dummy = new ListNode(0, head);

        // subLen:当前一轮归并,每个子链表的长度;每次循环翻倍 1,2,4,8...
        // 当 subLen >= totalLen,说明整个链表已经有序,停止循环
        for (int subLen = 1; subLen < totalLen; subLen <<= 1) {
            // tail:记录【已经合并完成部分的最后一个节点】,用来拼接下一轮合并结果
            ListNode tail = dummy;
            // cur:当前待切割、待归并的起始位置
            ListNode cur = dummy.next;

            // 只要还有没处理完的节点,就继续切割 + 合并
            while (cur != null) {
                // 1. 切出左半段,长度最多 subLen
                ListNode left = cur;
                // cut 函数:从 left 开始切掉 subLen 个节点,返回左段后面第一个节点(也就是右段起点)
                ListNode right = cut(left, subLen);
                // 2. 切出右半段,长度最多 subLen;cur 更新为【下一组待处理的起点】
                cur = cut(right, subLen);

                // 把合并好的有序链表,接到上一轮结果的尾部
                tail.next = merge(left, right);
                // tail 向后走到本轮合并结果的末尾,为下一次拼接做准备
                while (tail.next != null) {
                    tail = tail.next;
                }
            }
        }
        // dummy.next 就是排序完成后的链表头
        return dummy.next;
    }

    /**
     * 截断链表:从 head 开始,截取最多 k 个节点,切断链表
     * @param head 截断起始位置
     * @param k 需要截取的节点数量
     * @return 返回截断之后,后面剩下链表的起点;同时把前面这段的尾节点 next 置 null
     */
    private ListNode cut(ListNode head, int k) {
        // 走 k-1 步,到达这一段的最后一个节点
        while (--k > 0 && head != null) {
            head = head.next;
        }
        // 如果已经走到 null,说明后面没有节点了,直接返回 null
        if (head == null) {
            return null;
        }
        // 保存截断位置的下一个节点(后面链表的起点)
        ListNode nextStart = head.next;
        // 核心:切断链表,让当前截取的这一段独立,避免和后面连在一起
        head.next = null;
        return nextStart;
    }

    /**
     * 合并两个有序单链表(LeetCode 21 模板)
     * @param l1 有序链表1
     * @param l2 有序链表2
     * @return 合并后的有序链表头
     */
    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        ListNode tmp = dummy;

        // 双指针依次取更小的值
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                tmp.next = l1;
                l1 = l1.next;
            } else {
                tmp.next = l2;
                l2 = l2.next;
            }
            tmp = tmp.next;
        }
        // 把剩下没走完的链表直接接在尾部(剩下的本身有序)
        tmp.next = (l1 != null) ? l1 : l2;
        return dummy.next;
    }
}

重点函数与关键点

  1. cut(head, k):截断链表取出 k 个节点,返回后面链表起点并断开链接------它是这个写法的灵魂,强制分段,防止归并时链表串成环、无限循环;
  2. subLen <<= 1 等价于 subLen *= 2,位运算写法,每轮块大小翻倍;
  3. dummy 哨兵统一处理头节点变化,不用单独判断头是谁;
  4. 自底向上没有递归、没有递归栈 ,满足题目进阶要求:O(nlog⁡n)O(n\log n)O(nlogn) 时间、O(1)O(1)O(1) 额外空间(只开了有限几个指针变量);
  5. 内层 while (tail.next != null):必须移动 tail,不然每次合并的结果会覆盖在上一段末尾,链表错乱。

③ 知识点总结

常见出错点

  1. 快慢指针找中点的时候,忘记切断左右两半链表! 不切断:合并的时候链表会循环,死循环;
  2. 合并有序链表时指针混乱,没有保存每一段的后继节点:分段切完之后,要记住下一段的起点,不然找不到后面的链表;
  3. 哑节点 dummy 使用错误,更新 tail 指针的时候忘记往后走:tail 是当前合并结果的末尾,每次合并完一段,tail 要移到新末尾;
  4. 空链表 / 单节点链表边界漏判 :head == null 或者 head.next == null 直接返回;
  5. 自顶向下递归写法 :快慢指针起点写错(fast = head.next, slow = head,不是都从 head.next 开始),导致中点分割错误;
  6. 段长翻倍循环条件写错 ,subLen 溢出。

两种写法对比

写法 时间 空间 难度 面试场景
自顶向下递归归并 O(nlog⁡n)O(n\log n)O(nlogn) O(log⁡n)O(\log n)O(logn) ⭐⭐ 快速写出,先保证 AC
自底向上迭代归并 O(nlog⁡n)O(n\log n)O(nlogn) O(1)O(1)O(1) ⭐⭐⭐⭐ 面试官追问进阶要求时写这个

拓展 :这道题经常和 21. 合并两个有序链表、876. 链表的中间结点 一起考,merge 函数是链表归并的核心模板,一定要背熟。

知识点小结

  • 两个模板 :merge(哑节点 + 双指针 + 剩余段接尾)来自 21 题,快慢指针找中点来自 876 题;
  • 必须断链 :递归版靠 slow.next = null,迭代版靠 cut 内置 null,否则两段不独立,归并必成环。

25. K 个一组翻转链表

① 题目分析 & 解题思路

题意 :给单链表,每 k 个节点为一组进行翻转;不足 k 个的末尾分组保持原样。不能只改节点的值,必须调换节点指针 。进阶要求:O(1)O(1)O(1) 额外空间。

核心思路(迭代,满足 O(1)O(1)O(1) 空间):

  1. 用哑节点 dummy,统一处理头部翻转,避免单独处理头节点变化;
  2. 维护一个 pre 指针:代表当前待翻转组的前一个节点 ,翻转完这一组后,要把 pre.next 指向新的组头;
  3. 每次先从 pre 出发,往后走 k 步,检查剩下节点够不够 k 个:不够直接结束;
    • 够 k 个:标记 start(本组起点)、end(本组终点),再记录 nextGroup(下一组的起点);
    • 把 end.next = null,断开当前组,调用翻转单链表函数 翻转 start;
    • 翻转后:原来的 end 变成新组头,原来的 start 变成本组尾;
    • 拼接:pre.next 指向翻转后的新组头(就是原来的 end,代码里写作 pre.next = reverse(start)),start.next = nextGroup;
    • 更新 pre = start,进入下一轮循环;
  4. 循环直到剩余节点不足 k 个。

配套辅助函数:翻转一条独立的单链表(206 题基础模板),代码见解法一的 reverse。


② 常见解法(附代码)及讲解

解法一:迭代写法(推荐,满足进阶 O(1)O(1)O(1) 空间,面试首选)

java 复制代码
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        // 哑节点,放在链表最前面,统一所有分组的前驱(它的值无所谓)
        // 等价于 ListNode dummy = new ListNode(-1, head);
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        // pre:待翻转分组的前驱节点
        ListNode pre = dummy;

        while (true) {
            // 1. 找到当前分组的末尾 end,同时判断剩余节点够不够 k 个
            ListNode end = pre;
            for (int i = 0; i < k; i++) {
                end = end.next;
                // end 走到 null,说明剩下节点不足 k 个,直接结束
                if (end == null) {
                    return dummy.next;
                }
            }
            // pre:上一组的尾节点;end:这一组的尾节点
            // start:本组原始起点;nextGroup:下一组的起点,必须提前保存!
            ListNode start = pre.next;
            ListNode nextGroup = end.next;

            // 关键:2. 断开当前组和下一组的连接,单独翻转当前组
            end.next = null;
            // 翻转本组,翻转之后 end 变成新的组头,start 变成本组尾
            // pre.next 指向反转后的这一组的头结点(prev)
            pre.next = reverse(start);

            // 3. 拼接:翻转后的尾部(start)接上后面没处理的分组
            start.next = nextGroup;
            // pre 更新为当前组的尾部,作为下一组的前驱
            pre = start;
        }
    }

    /**
     * 翻转一条独立的单链表(206 原题模板)
     * @param head 待翻转链表头
     * @return 翻转后的新头
     */
    private ListNode reverse(ListNode head) {
        ListNode prev = null;
        ListNode cur = head;
        while (cur != null) {
            ListNode temp = cur.next; // 先保存原顺序的下一个结点
            cur.next = prev; // 反转链表顺序,当前结点 next 指向 prev 值
            prev = cur; // prev 指向 cur 值
            cur = temp; // cur 指向下一个节点的值
        }
        return prev; // 返回反转后的第一个结点
    }
}
流程示例(head = 1,2,3,4,5, k = 2)
  • dummy→1→2→3→4→5,pre = dummy;
  • end 向后走 2 步到 2;start = 1,nextGroup = 3;断开 2→null;翻转 [1,2] 得到 2→1;
  • pre.next = 2;1.next = 3;pre 更新到 1;
  • 下一轮:end 从 1 往后走 2 步到 4;start = 3,nextGroup = 5;断开 4→null;翻转 [3,4] 得到 4→3;
  • pre.next = 4;3.next = 5;pre 更新到 3;
  • 下一轮 end 走两步,第一步到 5,第二步 end = null,退出循环。

结果:dummy→2→1→4→3→5 ✅

解法二:递归写法(代码简短,空间 O(n/k)O(n/k)O(n/k),递归栈,不满足进阶)

思路:递归处理后面的分组,当前组翻转之后,把当前组的尾部接上递归返回的、后面已经处理好的链表。

java 复制代码
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        // 检查剩下节点够不够 k 个
        ListNode check = head;
        for(int i = 0; i < k; i++){
            if(check == null) return head;
            check = check.next;
        }
        // 够 k 个,翻转当前 k 个节点
        ListNode prev = null;
        ListNode cur = head;
        for(int i = 0; i < k; i++){
            ListNode temp = cur.next;
            cur.next = prev;
            prev = cur;
            cur = temp;
        }
        // head 现在是本组翻转后的尾,接上后面递归处理好的结果
        // cur 是下一组的头结点
        head.next = reverseKGroup(cur, k);
        // prev 是本组翻转后的新头
        return prev;
    }
}

✅ 优点:代码很短,逻辑好理解 ❌ 缺点:递归栈深度是 O(n/k)O(n/k)O(n/k),不是 O(1)O(1)O(1) 空间,面试官问进阶就不能用这个。


③ 知识点总结

常见出错点

  1. ❌ 忘记提前保存下一组起点 nextGroup,翻转之后找不到后面链表,直接丢失后半段;
  2. ❌ 翻转前没有断开本组和下一组的连接:翻转函数会把后面一组一起反转,链表错乱成环;
  3. ❌ 指针更新顺序搞错 :pre 必须更新为【翻转后的本组尾节点(原来的 start)】,很多人继续用 end 当 pre,分组拼接完全错位;
  4. ❌ 检查剩余节点数量逻辑写错 :走到中途为 null 还继续翻转,把不足 k 个的尾部也翻转了(题目要求尾部不足 k 保留原序);
  5. ❌ 边界:k = 1,不需要翻转,直接返回原链表;
  6. ❌ 不用哑节点,处理第一组翻转后的新头非常麻烦,容易丢头节点。

两种写法对比

写法 时间 空间 特点
迭代 O(n)O(n)O(n) O(1)O(1)O(1) 进阶要求,指针多,边界容易错,面试优先准备
递归 O(n)O(n)O(n) O(n/k)O(n/k)O(n/k) 代码简洁,适合快速 AC,不能满足进阶

知识点小结

  • 保存---断开---拼接 :先存 start/end/nextGroup,再 end.next = null,最后 pre.next、start.next 回接,顺序错了必丢链或成环;
  • 难题 = 基础模板组合 :reverse 是 206 题模板;不足 k 个靠"先走 k 步探测"统一处理,避免特判末尾。

这道题是链表高频 Hard,依赖【翻转单链表】基础模板,一定要吃透指针的保存、断开、拼接三步。

相关推荐
天河归来13 小时前
第三方接口不稳定,如何让核心任务继续完成
java·大数据
Knight_AL14 小时前
Java 文件下载时文件名中的 `#` 被转换成 `%23`?一次 URL 编码与解码问题排查
java·开发语言
ZhengEnCi14 小时前
GPT-6 Astra 爆改 Qwen 权重真有那么神吗:权重空间编程 0.016% 参数的真相
算法·架构
罗西的思考14 小时前
【轻量化复刻大模型 / KIMI K3】KDA(Kimi Delta Attention):并行
人工智能·算法·机器学习
砚底藏山河15 小时前
python量化自定义周期 K 线合成
java·开发语言·数据库·python·金融
猿究院_夏燊16 小时前
MySQL 的四种隔离级别
java·数据库·mysql
码流子16 小时前
移动收费怎么做?手持终端+App+收费中心的架构实践
java·开发语言·架构
霸道流氓气质16 小时前
AI 流式对话与实时聊天平台:SSE 解码竞态、有状态会话隔离与背压取消的 Vue3 + Spring AI 生产级实战
java·人工智能·spring
夜不会漫长17 小时前
C++:红黑树
数据结构·算法
云创智城-yuncitys17 小时前
YunParking 4.0:JDK 17 与 Spring Boot 3.2 的停车云升级
java·开发语言·spring boot