Hot100(开刷) 之 环形链表(II)-- 随机链表的复制 -- 翻转二叉树

三十二、环形链表(II) _ 检测环的入口点

1.一句话描述思路

  • 用哈希集合记录所有访问过的节点,遍历链表时第一个重复遇到的节点即为环的入口;若遍历结束无重复,则无环。
java 复制代码
//java版本
public ListNode detectCycle(ListNode head) {
    // 创建一个指针 pos,初始指向链表头节点,用于遍历链表
    ListNode pos = head;
    // 创建一个 HashSet,用于存储已经访问过的节点对象(基于节点引用)
    Set<ListNode> visited = new HashSet<ListNode>();
    
    // 当 pos 不为 null 时,继续遍历链表
    while (pos != null) {
        // 检查当前节点是否已经被访问过(即是否在 set 中)
        if (visited.contains(pos)) {
            // 如果已经访问过,说明当前节点就是环的入口节点(因为第一次遇到重复节点)
            return pos;
        } else {
            // 否则,将当前节点加入 visited 集合,标记为已访问
            visited.add(pos);
        }
        // 将指针移动到下一个节点,继续遍历
        pos = pos.next;
    }
    // 如果遍历完整个链表(pos 变为 null)都没有遇到重复节点,说明链表无环,返回 null
    return null;

}
Kotlin 复制代码
// Kotlin版本
fun detectCycle(head: ListNode?): ListNode? {
    // 创建一个指针 pos,初始指向链表头节点,用于遍历链表
    var pos = head
    // 创建一个 HashSet,用于存储已经访问过的节点对象(基于节点引用)
    val visited = HashSet<ListNode>()
    
    // 当 pos 不为 null 时,继续遍历链表
    while (pos != null) {
        // 检查当前节点是否已经被访问过(即是否在 set 中)
        if (visited.contains(pos)) {
            // 如果已经访问过,说明当前节点就是环的入口节点(因为第一次遇到重复节点)
            return pos
        } else {
            // 否则,将当前节点加入 visited 集合,标记为已访问
            visited.add(pos)
        }
        // 将指针移动到下一个节点,继续遍历
        pos = pos.next
    }
    // 如果遍历完整个链表(pos 变为 null)都没有遇到重复节点,说明链表无环,返回 null
    return null
}
}

2.代码逻辑详解

  • 从链表头结点 head 开始,用指针 pos 指向当前节点。
  • 创建一个 HashSet<ListNode> 用于存储已经遍历过的节点引用。
  • 循环遍历链表,直到 posnull(表示到达链表尾部)。
  • 对于每个节点,检查它是否已经存在于集合中:
    • 如果存在,说明之前已经经过该节点,这正是环的起始节点(因为第一次重复遇到的节点就是环的入口),直接返回该节点。
    • 如果不存在,将该节点加入集合。
  • 移动指针到下一个节点,继续循环。
  • 如果循环正常结束(pos == null),说明链表中没有环,返回 null

3.关键点

  • 哈希集合的妙用 :利用集合的 O(1) 平均查找时间复杂度,快速检测重复节点。

  • 环的入口判定 :第一个重复出现的节点一定是环的起始节点,因为从 head 出发进入环后,再次访问到的第一个环上节点就是入口。

  • 终止条件 :既要处理无环(遇到 null),也要处理有环(遇到已访问节点)。

  • 节点比较 :这里比较的是 节点对象引用(内存地址),而不是节点值,因此不会因为值相同而误判。


4.复杂度分析

  • 时间复杂度:O(n)
    最坏情况下,每个节点被访问一次,集合的插入和查找操作平均为 O(1),因此总时间复杂度为 O(n),其中 n 为链表节点数。
  • 空间复杂度:O(n)
    需要使用一个哈希集合来存储最多 n 个节点的引用,因此额外空间与链表长度成正比。

5.优缺点

优点:

  • 实现简单直观,容易理解和编写。
  • 通用性强,不要求修改链表结构或节点值,适用于只读场景。
  • 能准确返回环的入口节点,而不仅仅是判断是否有环。

缺点:

  • 空间开销大,需要 O(n) 的额外存储空间,当链表很长时内存占用较高。
  • 无法处理无法使用哈希的场景(如内存极度受限的环境)。
  • 相比双指针(Floyd 判圈算法)效率较低,后者只需要 O(1) 额外空间。

三十三、随机链表的复制

1.一句话算法流程描述

分三步:①在原链表每个节点后插入其副本;②根据原节点的 random 指针设置副本的 random;③拆开混合链表,恢复原链表并提取新链表。

java 复制代码
//Java版本
public Node copyRandomList(Node head) {
            // 边界条件:如果原链表为空,直接返回 null
        if (head == null) {
            return null;
        }

        // ========== 第一步:在原链表的每个节点后面插入一个自己的副本 ==========
        // 例如:原链表 A -> B -> C
        // 插入后:A -> A' -> B -> B' -> C -> C'
        for (Node node = head; node != null; node = node.next.next) {
            // 创建当前节点的新副本,值与原节点相同
            Node nodeNew = new Node(node.val);
            // 新副本的 next 指向原节点的下一个节点
            nodeNew.next = node.next;
            // 原节点的 next 指向新副本,完成插入
            node.next = nodeNew;
        }

        // ========== 第二步:设置新节点的 random 指针 ==========
        // 此时链表结构为:原节点 -> 新节点 -> 原节点 -> 新节点 ...
        // 原节点 node 的 random 指向某个原节点,那么 node.next(新节点)的 random 应该指向 node.random.next(那个原节点对应的新节点)
        for (Node node = head; node != null; node = node.next.next) {
            // 获取当前原节点对应的新节点
            Node nodeNew = node.next;
            // 如果原节点的 random 不为空,则新节点的 random 指向原节点的 random 所指节点的下一个节点(即对应的新节点)
            // 否则新节点的 random 设为 null
            nodeNew.random = (node.random != null) ? node.random.next : null;
        }

        // ========== 第三步:将原链表和新链表拆分开 ==========
        // 新链表的头节点就是原链表头节点的下一个节点
        Node headNew = head.next;
        // 遍历整个混合链表,分离出原链表和新链表
        for (Node node = head; node != null; node = node.next) {
            // nodeNew 是当前原节点对应的新节点
            Node nodeNew = node.next;
            // 恢复原节点的 next 指针:跳过新节点,指向原来的下一个原节点(即 nodeNew.next)
            node.next = node.next.next;
            // 恢复新节点的 next 指针:如果 nodeNew 后面还有新节点,则指向那个新节点,否则为 null
            nodeNew.next = (nodeNew.next != null) ? nodeNew.next.next : null;
        }
        // 返回新链表的头节点
        return headNew;
    }
}
java 复制代码
 // Kotlin
fun copyRandomList(head: Node?): Node? {
        // 边界条件:空链表直接返回 null
        if (head == null) return null

        // ========== 第一步:插入副本节点 ==========
        // 原链表:A -> B -> C
        // 插入后:A -> A' -> B -> B' -> C -> C'
        var node: Node? = head
        while (node != null) {
            val newNode = Node(node.`val`)   // 创建副本
            newNode.next = node.next         // 副本的 next 指向原节点的下一个
            node.next = newNode              // 原节点的 next 指向副本
            node = newNode.next              // 跳两步,继续处理下一个原节点
        }

        // ========== 第二步:设置副本节点的 random ==========
        // 此时结构:原节点 -> 副本节点 -> 原节点 -> 副本节点 ...
        // 原节点 node 的 random 指向某原节点 target,
        // 则 node.next(副本节点)的 random 应指向 target?.next(对应的副本节点)
        node = head
        while (node != null) {
            val newNode = node.next           // 当前原节点对应的副本节点
            // 原节点的 random 可能为 null,需处理空安全
            newNode?.random = node.random?.next
            node = newNode?.next              // 跳两步到下一个原节点
        }

        // ========== 第三步:拆分原链表和新链表 ==========
        val newHead = head.next               // 新链表的头节点
        var cur: Node? = head
        while (cur != null) {
            val copy = cur.next               // 副本节点
            // 恢复原链表的 next:原节点指向原节点的下一个原节点(跳过副本)
            cur.next = copy?.next
            // 恢复副本链表的 next:副本节点指向下一个副本节点(如果存在)
            copy?.next = copy.next?.next
            cur = cur.next                    // 移动到下一个原节点
        }
        return newHead
    }

2.代码逻辑详解(分步说明)

第一步:插入副本节点

  • 遍历原链表,在每个节点 node 后面插入一个值相同的新节点 newNode

  • newNode.next = node.next 让副本指向原节点的下一个节点。

  • node.next = newNode 让原节点指向副本。

  • 然后将指针移动到 newNode.next(即原链表的下一个原节点),继续处理。

    效果:A → A' → B → B' → C → C' → null

第二步:设置副本的 random 指针

  • 再次遍历混合链表,每次取一对 (原节点, 副本节点)

  • 原节点 noderandom 可能指向某个原节点(或 null)。

  • 因为副本节点 node.next 正好紧跟在原节点后面,且原节点的 random 指向的目标原节点 target 的副本正好是 target.next

  • 所以设置:node.next.random = node.random?.next(若 node.randomnull,则副本的 random 也为 null)。

第三步:拆分两个链表

  • 新链表的头节点已确定为 head.next

  • 再次遍历混合链表,恢复原链表的 next 指针,让它跳过副本节点指向下一个原节点。

  • 同时恢复副本链表的 next 指针,让副本节点指向下一个副本节点(若存在)。

  • 遍历结束后,原链表恢复原状,新链表独立存在。


3.关键点

  • 三步法:插入 → 设置 random → 拆分,缺一不可。

  • 空间 O(1) 原地修改:不使用额外哈希表,只在原链表上操作。

  • random 指针的映射关系 :利用 原节点.random原节点.random.next 的对应关系,无需额外存储映射。

  • 指针移动步长:插入时每次跳两步,设置 random 时也跳两步,拆分时每次跳一步。

  • 空指针处理 :需注意 node.random 可能为 null,Kotlin 中用 ?. 安全调用。


4.复杂度分析

  • 时间复杂度:O(n)

    链表被完整遍历三次(插入、设 random、拆分),每次遍历都是线性时间。

  • 空间复杂度:O(1) (不计输出链表)

    除了返回的新链表节点外,只使用了常数个临时指针变量。新链表节点本身是题目要求的输出,不算额外空间。


5.优缺点

优点

  • 空间效率极高:O(1) 额外空间,优于哈希表法(O(n))。

  • 不依赖哈希函数,适用于所有环境。

  • 一次遍历即可建立 random 映射,逻辑清晰。

缺点

  • 实现稍复杂:需要谨慎处理指针移动和空值,容易出错。

  • 修改了原链表结构(虽然最后恢复了),在多线程或只读场景下不适用。

  • 需要三次遍历,常数时间略高于哈希表法(但渐进复杂度相同)。

三十四、翻转二叉树

1.一句话算法流程描述

使用队列按层遍历二叉树,对每个出队节点交换其左右子节点,并将非空子节点入队,直到队列为空。

java 复制代码
//Java版本
public TreeNode invertTree(TreeNode root) {
    // 基础情况:如果当前节点为空,直接返回 null
    if (root == null) {
        return null;
    }

    // 创建一个队列(使用 LinkedList 实现),用于存储待处理的节点
    // 队列遵循先进先出原则,保证按层处理节点(层序遍历)
    LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
    // 将根节点加入队列,开始遍历
    queue.add(root);

    // 循环处理队列中的所有节点,直到队列为空
    while (!queue.isEmpty()) {
        // 从队列头部取出一个节点(当前待处理的节点)
        TreeNode tmp = queue.poll();

        // 交换该节点的左右子节点
        TreeNode left = tmp.left;   // 暂存左子节点
        tmp.left = tmp.right;       // 将右子节点赋值给左子节点
        tmp.right = left;           // 将原左子节点赋值给右子节点

        // 如果当前节点的左子节点不为空,则将其加入队列,以便后续处理其子节点
        if (tmp.left != null) {
            queue.add(tmp.left);
        }

        // 如果当前节点的右子节点不为空,则将其加入队列,以便后续处理其子节点
        if (tmp.right != null) {
            queue.add(tmp.right);
        }
    }

    // 返回翻转后的根节点(原根节点位置不变,但其左右子树已交换)
    return root;
}
java 复制代码
//Kotlin版本
fun invertTree(root: TreeNode?): TreeNode? {
    // 边界条件:空树直接返回 null
    if (root == null) return null

    // 使用队列进行层序遍历(FIFO)
    val queue: ArrayDeque<TreeNode> = ArrayDeque()
    queue.addLast(root)   // 根节点入队

    while (queue.isNotEmpty()) {
        // 取出队首节点
        val current = queue.removeFirst()

        // 交换当前节点的左右子节点
        val temp = current.left
        current.left = current.right
        current.right = temp

        // 如果左子节点不为空,入队以便后续处理其子节点
        current.left?.let { queue.addLast(it) }
        // 如果右子节点不为空,入队
        current.right?.let { queue.addLast(it) }
    }

    // 返回原根节点(树结构已被原地翻转)
    return root
}
}

2.代码逻辑详解(分步说明)

  1. 处理空树

    如果 root == null,直接返回 null,无需任何操作。

  2. 初始化队列

    使用双端队列(ArrayDeque)作为队列,将根节点加入队列。

  3. 循环处理队列

    只要队列不为空:

    • 从队首取出一个节点 current

    • 交换 current 的左右子节点:借助临时变量 temp 存储 left,然后 left = rightright = temp

    • 检查交换后的左子节点(原来的右子节点)是否为 null,若不为空则入队。

    • 检查交换后的右子节点(原来的左子节点)是否为 null,若不为空则入队。

  4. 循环结束

    所有节点处理完毕后,返回根节点。原二叉树的每个节点都已交换左右子树,实现了整棵树的翻转。


3.关键点

  • 层序遍历(广度优先):利用队列按层处理节点,确保每个节点都被访问一次。

  • 原地翻转:直接修改原二叉树的节点引用,不创建新树,空间效率高。

  • 空节点处理 :在入队前检查子节点是否为空,避免将 null 加入队列。

  • 交换操作 :使用临时变量或 Kotlin 的 also/let 均可,本质是交换两个引用。

  • 适用性:此方法对任何二叉树都有效,包括完全二叉树、退化链表等。


4.复杂度分析

  • 时间复杂度:O(n)

    每个节点恰好入队一次、出队一次,交换操作是 O(1),总操作次数与节点数 n 成正比。

  • 空间复杂度:O(n)

    最坏情况下(满二叉树),队列中最多同时存储约 n/2 个节点(最后一层),因此额外空间为 O(n)。

    注:若考虑递归解法,递归栈深度 O(h) 更优,但此处为队列实现。


5.优缺点

优点

  • 直观易懂:层序遍历思想简单,交换左右子树的逻辑清晰。

  • 避免递归栈溢出:对于深度极大的树(如链状树),不会像递归解法那样可能导致栈溢出。

  • 原地修改:不占用额外树节点空间,仅使用队列存储指针。

缺点

  • 空间复杂度较高:最坏情况下需要 O(n) 的队列空间,而递归解法(DFS)只需要 O(h) 的栈空间(h 为树高)。对于满二叉树,两者都是 O(n),但常数因子队列稍大。

  • 代码略冗长:相比递归解法(几行代码),队列实现需要显式管理队列。

  • 需要额外数据结构:依赖队列容器,在某些受限环境中可能不可用。

相关推荐
indexsunny2 小时前
互联网大厂Java求职面试实战:Spring Boot与微服务架构解析
java·spring boot·redis·kafka·spring security·flyway·microservices
lulu12165440782 小时前
Claude Code Routines功能深度解析:24小时云端自动化开发指南
java·人工智能·python·ai编程
ch.ju2 小时前
Java程序设计(第3版)第二章——关系运算符
java
Tirzano2 小时前
springsession全能序列化方案
java·开发语言
神毓逍遥kang2 小时前
在nest.js中我想把Java的Sa-Token搬来
前端·后端
我登哥MVP2 小时前
【SpringMVC笔记】 - 2 - @RequestMapping
java·spring boot·spring·servlet·tomcat·intellij-idea·springmvc
神奇小汤圆2 小时前
MySQL CPU飙到680%:一次「僵尸查询」引发的雪崩
后端
殷紫川2 小时前
深度剖析:Java 并发三大量难题 —— 死锁、活锁、饥饿全解
java
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【14】ReactAgent 工具执行异常处理
java·人工智能·spring