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

侠客行・iOS 26 Liquid Glass TabBar 破阵记

侠客行・iOS 26 Liquid Glass TabBar 破阵记

引子 话说侠客岛旁的 “码农山庄” 里,有位青年开发者石破天,一手 SwiftUI 功夫练得炉火纯青,身旁常伴着心思缜密的产品女侠阿绣。 这日,山庄接到一桩棘手活计 —— 玄铁老怪掌管的 “APP 审核阁” 放出话来,凡要上 iOS 26 的 APP,必过Liquid Glass设计关,尤其Tab Bar这块,稍有差池便打回重练。 在本篇侠客行中,您将学到如下内容: * 引子 * 1. 📱 初探 iOS 26 的 Tab Bar:旧功新用,基础先扎牢 * 2. 🔍 拆解 Tab Bar 的模糊特效:藏在 “滚动容器” 里的玄机 * 3. 📜 给 TabView 加 “缩骨功”

By Ne0inhk
无人机与机器人群控通信技术的现状与未来展望

无人机与机器人群控通信技术的现状与未来展望

随着人工智能和自动化技术的迅速发展,无人机群控和机器人群控在多个领域的应用不断扩展。从智能农业到灾难救援、从物流运输到城市巡检,群控技术已经成为实现大规模协同作业的核心动力。然而,这些技术的应用离不开强大的通信基础设施支持。那么,现有的通信技术如何满足这些需求?未来市场又需要怎样的通信技术和指标? 一、无人机与机器人群控通信技术的现状 目前,无人机和机器人群控的通信技术主要有以下几种: 1. Wi-Fi (包括 Wi-Fi 6/6E/7) * 优点:高带宽、低延迟,能够支持高清视频传输和实时控制。 * 缺点:在大规模群控中,Wi-Fi 网络会受到距离、干扰和拥堵问题的影响,尤其是在复杂环境或信号密集的区域。 2. 5G NR (新无线) * 优点:高带宽、低延迟,特别适合需要大数据量传输和实时控制的应用,如无人机群控。 * 缺点:5G的基础设施建设仍然在发展中,部署成本较高,且对设备的能耗有一定要求,这可能限制了它在小型无人机和低功耗设备上的广泛应用。 3. LoRa (长距离低功耗无线) * 优点:长距离、

By Ne0inhk
论文阅读 SAM 3: Segment Anything with Concepts

论文阅读 SAM 3: Segment Anything with Concepts

创新点 * 首次定义 Promptable Concept Segmentation (PCS)可提示概念分割任务,支持通过名词短语、图像样本或两者结合,检测、分割并跟踪图像 / 视频中所有匹配概念的实例,同时保留视频帧间目标身份。 * 引入 “存在头(Presence Token)” 解耦识别与定位任务;采用共享骨干网络的检测器 + 视频跟踪器架构,避免任务冲突。 * 构建四阶段数据引擎,通过媒体筛选、标签生成(含难负样本)、AI 验证器实现标注吞吐量翻倍,生成高质量的合成训练数据。 * 创建包含 20.7 万个独特概念的 SA-CO (大规模概念分割数据集与基准体系),涵盖 12 万张图像和 1.7 千个视频,概念数量是现有基准的 50 倍以上,支持 PCS 任务全面评估 问题 SAM系列(Kirillov等人,2023年;

By Ne0inhk
RS485收发器在FPGA中的应用及注意事项

RS485收发器在FPGA中的应用及注意事项

1 前言 明确设计思路,精准定位问题,对于我们后期理解迭代工程有很大的帮助。 这就是我们常说的40%设计,20%编写和剩下的40%时间进行调试优化。 今天为大家带来的是如何解决RS485收发器使能转变引起的毛刺。 2 问题 Q1:什么时候需要用到RS485收发器? Q2:为何RS485收发器使能转变会引起毛刺? Q3:如何处理毛刺规避FPGA时序判断? 3 RS485收发器 3.1 硬件基础 3.1.1 标准收发器 RS485收发器是一类集成电路芯片,它的核心作用是在微控制器(如FPGA、MCU)的逻辑电平(如TTL电平,通常是0V/3.3V或0V/5V)与RS485差分信号之间进行双向转换。大多数RS485收发器还具备使能控制引脚(DE或RE),允许主控芯片灵活地切换其工作模式——发送或接收,从而支持半双工通信架构。 在实际应用中,微控制器输出的信号属于低电压、低电流的逻辑电平,适合短距离、高精度的内部电路通信,但无法直接用于长距离传输,

By Ne0inhk