找出克隆二叉树中的相同节点 图解题解
这道题到底在问什么
- 输入
- original=[7], target 指向那个 7
- 输出
- 克隆树的根 7(整棵树只有一个节点)
- 输入
- original=[1,2,3], target 指向值 3 的节点
- 输出
- 克隆树里根的右孩子 3(位置对应)
先想最直接的笨办法
两个指针退回到了根 50。回顾一下,原树根 50 的整个左子树: 30、20、40、45,挨个看过,没有一个是目标 70。按先左后右的顺序,左边找完了,现在轮到根的右子树。两指针一起转向右孩子。(动画第 17 步)
最优解:为什么这么做
一句话答案:LeetCode 1379 找出克隆二叉树中的相同节点:两棵树形状全同,同步递归原树和克隆树,原树指针按引用踩中 target 时返回克隆树同位置的节点,时间 O(n)。
两棵一样的树,要在克隆树里返回哪个节点
给三样东西:原始树 original、它的副本克隆树 cloned,还有一个落在 original 里的目标节点 target。两棵树形状和值都一样,但节点是两套互相独立的对象,谁也不许改。要返回的,是克隆树里和 target 位置相同的那个节点——是位置对应,不是随便找个值相等的。题面例子:original=[1,2,3]、target 是值 3 那节点,返回克隆树根的右孩子 3。
只在克隆树里按值搜,遇到重复值会抓错人
克隆树里照着 target 的值去搜一个相同的数,题面这种值都不同的树似乎行——可进阶偏偏问:树里几个节点值都相等呢?按值搜会撞上第一个同值节点,它和 target 未必在同一位置,答案就错了。根子在于值相等不等于位置相同,只盯着值就把『位置对应』这个真正的要求丢了。
两棵树结构全同,那就让两个指针一起走
original 和 cloned 形状完全一样,那就别单独在克隆树翻找,而是让两个指针齐步走:一个在原树、一个在克隆树,永远停在同一位置。原树指针往左,克隆指针也往左;往右也同步。只需盯着原树指针,判断它是不是就是 target——判的是同一个对象,不是值相等——命中时克隆指针停的位置正好镜像对应,它指的就是答案。
递归函数每一步在比什么、返回什么
写成递归:函数带着原树指针 original 和克隆树指针 cloned 一起下探。先看 original 是不是空,空了返回空;再判 original 是不是 target 本身,是就返回当前的 cloned,即取克隆树对应位置的节点;都不满足,就先递归左边 original.left 和 cloned.left,没找到再递归右边。两边用『或』连:左子树返回非空就用它,否则看右子树,找到就层层传回。
[1,2,3] 找值 3、[7] 各怎么走
先看 [1,2,3]:根 1、左孩子 2、右孩子 3,target 是那个 3。进函数,original 是根 1,不是 target,递归左边。左边 original 是 2,也不是 target,左右孩子都空,递归返回空,左支落空。回到根看右边:original 是 3 就是 target 本身,返回克隆树的这个 3,它是根的右孩子,正是答案。
再看 [7]:整棵树只有根 7,target 就是它。进函数,original 不空,7 就是 target,返回克隆树的根,一步命中。
为什么是 O(n),几处判定别写偏
复杂度:最坏把整棵原树扫一遍,每个节点只判一次,时间 O(n),n 是节点数;提前命中就立刻返回。没有额外容器,只有递归栈,峰值深度等于树高 H:平衡树约 log n,链状退化时 H 等于 n,空间最坏 O(n)。
几处容易写偏:判定得比 original 是不是 target 本身,写成 original.val 等于 target.val 在有重复值的树里会误命中别的同值节点;命中后返回的必须是克隆树的 cloned,返回原树 target 就答成另一棵树;还有 original 为空的出口不能漏,少了它走到叶子外面就空指针出错。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记牢一句: 两个指针齐步走,一个在原树、一个在克隆树,永远同位置。原树指针踩中目标的那一刻,克隆树指针停的位置就是答案。下面每一帧都在套这条规则。
- 4原树与克隆树同形同值,目标是原树里的 70先看清布局。屏幕主舞台画的是原始树: 根是 50,左孩子是 30,右孩子是 80;30 下面挂着 20 和 40,40 再往下挂着一个 45;80 下面挂着 70 和 90。克隆树形状和这棵完全一样、每个值也一样,只是另一套独立的节点。高亮的这个 70,就是原始树里的目标节点。我们的任务,是在克隆树里把和它位置对应的那个节点找出来。
- 5形状值全同,但原树克隆树是两套对象强调一个容易绕晕的点: 原始树和克隆树虽然长得一模一样、每个位置的值都相等,但它们是两套各自独立的节点。题目要的不是「值等于 70」这么简单,而是「克隆树里和原始树这个 70 处在同一位置」的那个节点。所以光在克隆树里搜一个值 70 不够稳,正经做法是带着两棵树一起走,用位置来对应。
- 6op 在原树, cp 在克隆树, 永远同位置具体怎么走。准备两个指针: op 在原始树上,cp 在克隆树上,一开始都指向各自的根。接下来每一步,两个指针都朝同一个方向同步移动,先试左孩子、再试右孩子。因为两棵树形状一样,它俩的脚步永远齐。我们只盯着原树这边的 op: 它什么时候踩中目标,克隆树那边的 cp 就同时停在了对应位置。现在两个指针都在根上,开始。
- 7op 指原树根 50, cp 指克隆树根两个指针都落在各自的根上: op 指着原始树的根 50,cp 指着克隆树的根。注意它俩是同一个位置。每到一个新位置,第一件事永远是问: 原树这边的 op,是不是就是目标那个节点 70?来看根这边。
- 850 不是目标 70, 两指针同步进左子树原树根是 50,目标是那个 70,显然这个根不是目标节点。既然不是,就让两个指针同步往下: 先走左孩子。op 从原树的 50 走到它的左孩子 30,cp 在克隆树里同步走到对应的左孩子。它俩还是停在同一位置。
- 9op 到原树的 30, cp 到克隆树同位置两个指针同步落到了下一层左边这个位置,原树这边 op 指着 30。根 50 现在标成路径色,表示它在当前这条往下的路径上、已经看过。老规矩,先问: 这个 30 是不是目标节点 70?
- 1030 不是 70, 继续同步进左孩子30 不是目标 70。那就继续往下,两个指针同步走 30 的左孩子。op 从原树的 30 走到它的左孩子 20,cp 在克隆树同步走到对应位置。先把这一支探到底。
- 11op 到原树的 20(叶子), cp 同位置两指针同步落到 20。30 也进入路径色。这个 20 是个叶子节点,下面没有孩子了。还是先判: 它是不是目标 70?
- 1220 不是 70 且无孩子, 退回 30 试右20 不是 70,而且它是叶子,左右都没有孩子可以再往下走,这一支到底了。于是两个指针一起回退,退回到 30,准备改试 30 还没走过的右孩子。被否定的 20 标成已访问色。
- 13op 到原树的 40, cp 同位置两个指针同步走到 30 的右孩子,原树这边 op 指着 40。判: 这个 40 是不是目标 70?
- 1440 不是 70, 同步进 40 的左孩子40 也不是 70。继续往下,两指针同步走 40 的左孩子。op 从原树的 40 走到它的左孩子 45,cp 在克隆树同步跟上。
- 15op 到原树的 45(叶子), cp 同位置两个指针落到 45,40 进入路径色。这个 45 又是一个叶子。判: 它是不是目标 70?
- 1645 不是, 40 右空, 30 两支走完45 不是 70,它也是叶子。回退到 40,40 的右孩子是空,没得走了。再回退到 30,30 的左孩子 20、右孩子 40 这两支都走完了,整个以 30 为根的左子树彻底没有找到目标。两个指针一起退回更上面。
- 17原树整个左子树走完没找到, 退回根两个指针退回到了根 50。回顾一下,原树根 50 的整个左子树: 30、20、40、45,挨个看过,没有一个是目标 70。按先左后右的顺序,左边找完了,现在轮到根的右子树。两指针一起转向右孩子。
- 18op 到原树的 80, cp 同位置两个指针同步走到根的右孩子,原树这边 op 指着 80。判: 这个 80 是不是目标 70?
- 1980 不是 70, 同步进 80 的左孩子80 不是 70。继续往下,两指针同步走 80 的左孩子。op 从原树的 80 走到它的左孩子,cp 在克隆树同步跟上。下一帧就是关键。
- 20op 到原树的 70, cp 到克隆树同位置两个指针同步落到了 80 的左孩子。原树这边 op 指着 70。这个位置看起来很关键,因为目标正是原始树里那个 70。来做最后一次判定: op 是不是就是目标节点?
- 21原树 op 正是目标节点 70op 现在指的,正是原始树里那个目标节点 70,它就是 target 本身。命中了!按规则,这一刻不用再往下走,直接看克隆树这边: 因为两个指针一路齐步走,cp 此刻停的位置,正好和 op 镜像对应。答案就在克隆树的这个位置上。
- 22cp 停在克隆树同一位置, 即答案把镜头切到克隆树这边。原树的 op 落在 80 的左孩子这个位置上命中目标,那么克隆树的 cp,此刻同样停在克隆树 80 的左孩子这个位置。这个位置上的节点,值也是 70,但它是克隆树里那个独立的节点。它就是我们要返回的答案。
- 23返回 cp 指向的克隆 70把答案节点标成绿色。注意,我们返回的是克隆树里这个节点的引用,不是原始树里的那个目标。两者值都是 70,可身份不同,题目要的是克隆树这一个。定位它靠的不是去克隆树里搜值 70,而是带着两棵树一路同步走、用位置把它锁定。
- 24一路同步走, 位置一对应即得答案回放整个过程。我们没有单独去克隆树里翻找,而是让两个指针在两棵树上齐步走: 原树这边沿着 50、右孩子 80、再到左孩子,踩中了目标 70;克隆树那边同步走到了一模一样的位置。位置一对应,克隆树那个位置上的节点就是答案。这就是用同步遍历把位置对上的妙处。
- 25返回克隆树 80 的左孩子那个 70收工。绿色这个节点,代表的是克隆树里和原始树目标同一位置的那个 70,它就是我们要返回的引用。整道题的核心,就是两棵同形的树一起递归: 原树指针找到目标的瞬间,克隆树指针自然停在了对应位置上。
⚠️ 容易写错的地方
✗ 错:只在克隆树里按值搜 70
✓ 对:带着两棵树一起同步递归, 用位置对应
题目进阶就问如果允许重复值怎么办: 单按值搜会找错节点,正解是两棵树同步走、用位置锁定,这样和值无关
✗ 错:找到后返回原始树的目标节点
✓ 对:返回克隆树同一位置的那个节点
原树的 target 和克隆树的对应节点值相同但身份不同,题目明确要返回克隆树里已有的节点,返回原树的会答错
✗ 错:比较时用值相等 original.val 等于 target.val
✓ 对:比较节点身份 original 等于 target 本身
稳的判断是身份相等(同一个对象);在有重复值的树里,值相等会提前误命中别的节点,身份相等才精确
完整代码(Python / C++ / Java)
Python
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def getTargetCopy(self, original: TreeNode, cloned: TreeNode, target: TreeNode) -> TreeNode:
if original is None:
return None
if original is target:
return cloned
return self.getTargetCopy(original.left, cloned.left, target) or self.getTargetCopy(original.right, cloned.right, target)C++
#include <iostream>
#include <optional>
#include <queue>
#include <vector>
using namespace std;
struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x): val(x), left(nullptr), right(nullptr) {} };
class Solution {
public:
TreeNode* getTargetCopy(TreeNode* original, TreeNode* cloned, TreeNode* target) {
if (!original) return nullptr;
if (original == target) return cloned;
auto left = getTargetCopy(original->left, cloned->left, target);
return left ? left : getTargetCopy(original->right, cloned->right, target);
}
};Java
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x){val=x;} }
class Solution {
public final TreeNode getTargetCopy(final TreeNode original, final TreeNode cloned, final TreeNode target) {
if (original == null) return null;
if (original == target) return cloned;
TreeNode left = getTargetCopy(original.left, cloned.left, target);
return left != null ? left : getTargetCopy(original.right, cloned.right, target);
}
}复杂度
时间
O(n)
n 是树的节点数。最坏情况要把整棵原始树遍历一遍才找到目标,每个节点只看一次,所以是线性的。一旦提前命中目标就立刻返回,常数更小
空间
O(H)
没有额外容器,只用递归栈。栈的峰值深度等于树高 H: 递归最深下到目标所在的那一层。平衡树时 H 约为 log n,链状退化树最坏 H 等于 n,所以空间最坏是 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 找出克隆二叉树中的相同节点 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
如果树里允许出现值相同的节点,这套解法还成立吗?+
成立,而且几乎不用改。从头到尾比的都是节点身份 original 是不是 target 本身,也就是判断是不是同一个对象,根本没依赖值唯一这个前提。再加上两棵树同步遍历、用位置对应,原树指针命中目标那一刻克隆树指针就在镜像位置,所以哪怕多个节点值相同也不会认错。反过来,谁要图省事改成在克隆树里按值搜,遇到重复值就会捞错节点,这恰好是进阶想考的点。
能不能不用递归,改成广度优先用队列来做?+
可以。备两个队列,分别装原树和克隆树的节点,一开始各放入自己的根。每轮同时从两个队列各弹出一个节点,它俩必然是对应位置;检查原树这个节点是不是 target,是就返回克隆树这个节点。不是就把两边的左孩子一起入队、右孩子一起入队,让两个队列步调始终一致。本质和递归一样是两棵树同步遍历,只是换成一层层横着走的层序,占的空间从递归栈变成队列。
为什么空间复杂度是 O(H) 而不是 O(1)?+
因为递归本身要占调用栈,栈的深度等于当前递归到的层数,最深会下到目标所在那一层,峰值就是树高 H。平衡树时 H 大约 log n,可一旦树退化成一条链,H 就等于 n,空间最坏是 O(n)。我们确实没开额外的数组或哈希表,但只要用了递归,这部分栈空间就省不掉,按峰值算就是 O(H)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 找出克隆二叉树中的相同节点 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。