1. 题目到底在考什么
刷LeetCode热门100题,刷到这道“随机链表的复制”时,如果你是第一次见,大概率会愣一下。题目本身没什么弯弯绕:给你一个长度为n的链表,每个节点除了next指针,还多了一个random指针,random可以指向链表中任意一个节点,也可以指向null。要求你返回一个深拷贝,也就是new出来的全新链表,和原链表结构完全一样,但每一个节点都是独立的新对象。
为什么单独拿出来说?因为这道题入围了力扣的hot 100,绝不是一道简单题的水平。它考察的不只是“会不会遍历链表”,而是两个关键能力:第一,能不能处理“复制过程中引用关系依赖顺序”的问题;第二,有没有空间换时间、或者通过巧妙的结构设计把辅助空间省下来的意识。前者对应回溯+哈希,后者对应迭代+节点拆分。两种解法在LeetCode官方题解里分别是“回溯+哈希表”和“迭代+节点拆分”,这也是标题里那串括号的来历。
适合谁来读?我建议这几类人都细看一遍:准备面试、正在刷hot 100的朋友,尤其要理解两种解法的取舍;学过链表但总被指针绕晕的新手,这道题能把“新旧节点映射”这个概念彻底讲透;还有一类是做代码评审或想提升代码设计感的同学,节点拆分那种“原地织毛衣”的思路,对锻炼结构化思维非常有帮助。
1.1 一个看起来“很简单”的复制题
先把直觉版的思路摆出来:普通链表的复制很简单,遍历一次,每遇到一个原节点,就new一个值相同的新节点,然后让上一个新节点的next指向当前新节点,完事。
但随机链表多了一个random指针,麻烦就来了。你没办法在第一遍遍历的时候就准确填好random指向,因为random指向的那个节点,很可能还没有被创建。举个例子:原链表的第1个节点random指向第5个节点,你从头开始复制,当你复制第1个节点时,第5个节点还没new出来,你手里根本没有一个“新第5节点”可以指。
有人会说:那我先遍历两遍,第一遍把所有节点全部new出来存好,第二遍再逐一设置next和random,不就行了?思路方向是对的,但需要一个Map来记录“原节点 -> 新节点”的对应关系,不然你在第二遍设置random时,拿到一个原节点引用,还是不知道对应的新节点是哪个。所以最朴素的正确解法,就是哈希表映射。
1.2 真正的难点:random指针打破顺序假设
这道题最核心的难点,在于random让“复制顺序”这件事变得不可预测。链表天然是一个线性结构,next指针决定了你只需要按顺序处理。但random像是一个隐藏的“跳线”,它随时可能指向一个你还没访问到的未来节点,也可能指回一个已经处理过的历史节点,甚至指向自己。这个特性让简单的迭代复制失效。
更深一层,它考察的是“深拷贝”的概念。所谓深拷贝,不只是值相同,而是新链表里每一个节点的地址都和原链表不同,并且所有指针关系都在新链表的节点之间建立。很多人在LeetCode上跑测试,发现返回结果打印出来“好像一样”,但用node1 == node2一比较,或者修改新链表的值导致原链表也跟着变,就立刻露馅了。
1.3 解法全景图:哈希映射与节点拆分两条路线
针对这道题,主流的两个方向必须都在脑子里有清晰画像:
- 方案一:回溯+哈希表。用递归去模拟“需要哪个节点就现场创建哪个节点”,哈希表记录原节点和新节点的对应关系,避免重复创建。这是最容易想到、也最好解释清楚的方案。
- 方案二:迭代+节点拆分。把新节点先一个个插到原节点后面,构造
原1 -> 新1 -> 原2 -> 新2 -> ...的交替链条,然后在原链表上原地设置新节点的random,最后再把整个链表拆成两条。空间复杂度从O(n)降到O(1),思路极其精巧。
两条路各有优劣,后面我分别拆开讲。而且这道题非常适合当作面试题去练“题后复盘”——把两种解法都写一遍,你对“引用关系”“指针操作”“递归记忆化”的理解会上一个台阶。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 回溯 + 哈希:最符合直觉的思路
先说我个人刷题时的习惯:遇到这种存在“循环依赖”或者“未知引用”的场景,第一步想到的往往是递归加记忆化。这道题就是教科书级别的回溯应用。
你回头看看标题里的“回溯+哈希”,其实可以拆成两部分理解:回溯解决的是“顺序不确定”的问题,哈希解决的是“重复创建”的问题。两个合在一起,就是一套非常成熟的解法模板。这个模板不止能用在链表上,克隆图(LeetCode 133)、克隆树、带random指针的数据结构复制,全都是同一个套路。
2.1 为什么回溯法和哈希表能组队
我们把问题换一种问法:如果你要复制一个节点A,你需要知道什么?你需要知道A的val,需要知道A的next指向哪个“新节点”,还需要知道A的random指向哪个“新节点”。而A的next和random指向的那个节点,可能已经复制过了,也可能还没复制。如果已经复制过,直接复用即可;如果没复制过,就先递归去复制它——这就是回溯过程。
但这里有一个致命问题:如果链表中存在环形的引用链,比如A的random指向B,B的next又指回A,递归就会无限套娃下去。解决这个问题的办法叫“记笔记”:
- 每次创建一个新节点,立刻把
原节点 -> 新节点的对应关系存进哈希表; - 下一次不管是通过next还是random访问到同一个原节点,先查哈希表,查到了就直接返回已经复制过的新节点,不再递归。
哈希表在这里的角色,既是映射表,又是“备忘录”,同时避免了重复创建和死循环。用大白话说:递归负责“按需生产”,哈希负责“防止生产重复”。这跟动态规划里的记忆化搜索逻辑如出一辙。
2.2 Python递归实现:代码逐段拆解
直
