Leecode热题100:随机链表的复制(链表)

Leecode热题100:随机链表的复制(链表)

目录

题目描述:

拆解本质:什么是“深拷贝”?

核心矛盾点

方案一:利用“映射”打破时空限制 (Hash Map)

思路过程:

方案二:利用“空间拓扑”消除映射 (Interweaving)

思路过程:

C++ 代码实现 (基于方案二)

面试回答

第一步:明确本质

第二步:提出矛盾

第三步:从直觉出发

第四步:极致优化(空间换位置)

第五步:总结复杂度


题目描述:

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为  null 。

你的代码 只 接受原链表的头节点 head 作为传入参数。

示例 1:

输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]

输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]

示例 2:

输入:head = [[1,1],[2,1]]

输出:[[1,1],[2,1]]

(来源:leecode)


要解决“带有随机指针的链表深拷贝”问题,我们不仅是在写代码,而是在处理信息映射内存拓扑的问题。

拆解本质:什么是“深拷贝”?

从物理层面看,链表是一组分散在内存中的节点。深拷贝意味着:

  1. 数量对等:原链表有 n 个节点,新链表也必须有 n 个。
  2. 数值一致:对应的节点 val 必须相同。
  3. 拓扑一致:最关键的一点,新节点的 nextrandom 必须指向新链表中的对应节点,而不是原链表。

核心矛盾点

当我们遍历原链表到达节点 A 时,我们需要设置 A'.random。

如果 A.random 指向节点 B,而 B 在链表的末尾,此时我们还没创建 B'。

矛盾点在于: 在单次线性遍历中,我们无法引用一个“尚未存在”的对象。

方案一:利用“映射”打破时空限制 (Hash Map)

如果我们想在一次或两次遍历中解决问题,我们需要一个“记忆体”来存储原节点与新节点之间的对应关系

  • 原子逻辑:给定原地址 Pold,我必须能立刻找到对应的新地址 Pnew。
  • 工具选择:std::unordered_map<Node*, Node*>

思路过程:

  1. 第一遍遍历:把所有新节点创建出来,并把 (原节点地址, 新节点地址) 存入哈希表。
  2. 第二遍遍历:根据哈希表,把新节点的 nextrandom 指针连接起来。

评价:这是最直观的逻辑,时间复杂度 O(n),空间复杂度 O(n)。


方案二:利用“空间拓扑”消除映射 (Interweaving)

如果我们追求极致,不使用额外的哈希表(即 O(1) 空间复杂度),该怎么办?

回到第一性原理:映射的本质是建立关联。 除了哈希表,还有什么方式能让原节点和新节点产生物理关联?

天才的想法:把新节点直接“嫁接”在原节点后面。

思路过程:

第一步:复制并嵌入 (Copy & Insert)

将每个新节点 A' 插入到原节点 A 和 A.next 之间。

  • 原结构:A -> B -> C
  • 新结构:A -> A' -> B -> B' -> C -> C'本质:现在 A' 的位置就在 A 的旁边。这意味着 A.next 就是 A'。

第二步:复制随机指针 (Copy Random)

因为 A' 紧跟在 A 后面,那么 A.random 的拷贝(即 A'.random)一定就在 A.random.next 的位置。

  • 逻辑:A->next->random = A->random->next (需注意判空)。

第三步:拆解链表 (Split)

将这个长链表拆分为原链表和新链表,恢复原貌并提取拷贝。


C++ 代码实现 (基于方案二)

这是最体现算法功底的解法。我们一步步用代码实现上述思维:

/* // 节点定义 class Node { public: int val; Node* next; Node* random; Node(int _val) { val = _val; next = NULL; random = NULL; } }; */ class Solution { public: Node* copyRandomList(Node* head) { if (!head) return nullptr; // --- 第一阶段:在每个原节点后面创建一个新节点 --- // 原:A -> B -> C // 现:A -> A' -> B -> B' -> C -> C' Node* curr = head; while (curr) { Node* newNode = new Node(curr->val); newNode->next = curr->next; curr->next = newNode; curr = newNode->next; } // --- 第二阶段:设置新节点的 random 指针 --- // 核心逻辑:A'.random = A.random->next curr = head; while (curr) { if (curr->random) { curr->next->random = curr->random->next; } curr = curr->next->next; // 移动到下一个原节点 } // --- 第三阶段:将交错的链表拆开 --- // 恢复原链表,提取新链表 Node* newHead = head->next; curr = head; Node* copyCurr = newHead; while (curr) { curr->next = curr->next->next; // 恢复原链表连接 if (copyCurr->next) { copyCurr->next = copyCurr->next->next; // 提取新链表连接 } curr = curr->next; copyCurr = copyCurr->next; } return newHead; } };

用第一性原理思考这道题的轨迹是:

  1. 明确原子目标:创建 n 个独立节点,并复制复杂的指向关系。
  2. 识别瓶颈random 指针指向的对象在创建当前节点时可能尚未就绪,或无法通过当前指针直接定位。
  3. 寻找关联媒介
    • 方案一:使用外部内存(哈希表)作为媒介。
    • 方案二:使用内部物理位置(交错排列)作为媒介。

如果理解了“如何建立 A 与 A' 的稳定关联”,你就真正掌握了这道题的精髓。


面试回答

第一步:明确本质

“首先,这道题的本质是处理带有复杂拓扑关系的内存拷贝。普通的单链表深拷贝只需要线性遍历,但这里的 random 指针打破了线性的顺序,它可能指向之后还没创建的节点,也可能指向之前的节点。”


第二步:提出矛盾

“这里的核心矛盾在于:

当我在克隆节点 A 时,我无法立刻确定它的 random 指针应该指向哪里,因为被指向的节点 B 可能还没被创建,或者我无法在 $O(1)$ 时间内找到 B 的克隆体。”

第三步:从直觉出发

“为了打破这个瓶颈,我首先想到的是建立映射关系

  • 思路:使用一个哈希表(Hash Map),以原节点地址为 Key,新节点地址为 Value。
  • 过程:第一遍遍历创建所有节点并存入 map,第二遍根据 map 连接 nextrandom
  • 评价:这个方案的时间复杂度是 O(n),非常高效,但它需要 O(n) 的额外空间来存储哈希表。”

第四步:极致优化(空间换位置)

“如果面试官要求空间复杂度优化到 O(1),我就需要利用原有的物理空间来存储映射关系,也就是所谓的‘原地交错法’:

  1. 节点嫁接:我遍历原链表,把每个克隆节点 A' 直接插入到原节点 A 的后面。这样,原节点和新节点就成对出现了。
  2. 关系同步:此时,映射关系隐含在位置中。如果 A.random 指向 B,那么 A' 的 random 就应该指向 B 的后继节点(即 B')。这一步我们只需要一次遍历就能完成。
  3. 链表拆分:最后,通过‘跳跃式’重新连接指针,将交错的链表还原为原链表和拷贝链表。

第五步:总结复杂度

“这种方案的精妙之处在于,它通过改变链表的物理结构,用 O(1) 的辅助空间巧妙地替代了哈希表的映射功能,同时时间复杂度依然保持在 O(n)。”

面试官可能会追问的细节:

  • Q: 拆分链表(第三步)时需要注意什么?
    • A: 需要注意空指针(nullptr)的处理,尤其是当 curr->next 是最后一个节点时,要确保不要访问 nullptr->next
  • Q: 哈希表法和交错法哪个更好?
    • A: 在工程实践中,哈希表法代码更简洁、易维护,不容易出错;在对内存极其敏感的嵌入式或底层开发中,交错法(O(1) 空间)更有优势。

Read more

基于Spring AI和Claude构建企业智能客服系统:从架构到实践的完整指南

基于Spring AI和Claude构建企业智能客服系统:从架构到实践的完整指南

个人名片 🎓作者简介:java领域优质创作者 🌐个人主页:码农阿豪 📞工作室:新空间代码工作室(提供各种软件服务) 💌个人邮箱:[[email protected]] 📱个人微信:15279484656 🌐个人导航网站:www.forff.top 💡座右铭:总有人要赢。为什么不能是我呢? * 专栏导航: 码农阿豪系列专栏导航 面试专栏:收集了java相关高频面试题,面试实战总结🍻🎉🖥️ Spring5系列专栏:整理了Spring5重要知识点与实战演练,有案例可直接使用🚀🔧💻 Redis专栏:Redis从零到一学习分享,经验总结,案例实战💐📝💡 全栈系列专栏:海纳百川有容乃大,可能你想要的东西里面都有🤸🌱🚀 目录 * 基于Spring AI和Claude构建企业智能客服系统:从架构到实践的完整指南 * 为什么选择Spring AI + Claude的技术组合? * Spring AI:企业级AI应用的理想选择 * Claude:强大的对话AI能力 * 系统架构设计 * 整体架构概览

By Ne0inhk
如何用MySQL Workbench导入sql文件(保姆级教程!!!)

如何用MySQL Workbench导入sql文件(保姆级教程!!!)

步骤 1:打开 MySQL Workbench 并连接到数据库 1. 启动 MySQL Workbench 应用程序。 在 MySQL Workbench 的主界面中,找到 “MySQL Connections” 区域。选择你要导入 SQL 文件的目标数据库连接,然后点击该连接进行连接。如果没有合适的连接,你可以点击 “+” 按钮创建一个新的连接,配置好连接信息(如主机名、端口、用户名、密码等)后进行连接。 连接我们的MySQL  步骤 2:打开 “Data Import/Restore” 工具 1. 连接成功后,在菜单栏中选择 “Server” -> “Data Import”。这将打开 “Data

By Ne0inhk
Flutter 组件 graphql 的适配 鸿蒙Harmony 实战 - 驾驭标准化分布式图形协议、实现鸿蒙端实时订阅与高性能交互网关方案

Flutter 组件 graphql 的适配 鸿蒙Harmony 实战 - 驾驭标准化分布式图形协议、实现鸿蒙端实时订阅与高性能交互网关方案

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 graphql 的适配 鸿蒙Harmony 实战 - 驾驭标准化分布式图形协议、实现鸿蒙端实时订阅与高性能交互网关方案 前言 在鸿蒙(OpenHarmony)生态的万物互联、极繁交互中台、以及对数据获取灵活性有极致要求的现代应用研发中,“高效的数据检索协议”是应用响应速度的灵魂。面对复杂的社交网络关系查询、实时的行情推送、或是海量状态信息的聚合。如果仅仅依靠传统的 RESTful 接口,那么不仅会导致因为 Over-fetching(获取多余数据)导致的带宽浪费,更会因为频繁的 API 版本演进引入严重的跨端兼容性碎片化问题。 我们需要一种“按需检索、逻辑解耦”的交互艺术。 graphql 是一套专为 Flutter 设计的标准 GraphQL 客户端套件。它通过构建规范的规范化缓存(Normalized Cache)与极其灵活的连接链路(Links)

By Ne0inhk