合并二叉树 图解题解
两棵树叠在一起,每个位置只做一件事:相加或直接保留。
两张半透明的透写纸叠在一起:同一个格子两边都有字,就把数字相加写进去;只有一边有,就直接保留那边的内容连同它下面整张子纸一起拿来用。递归就是不断把这件事从根的那一格做到每一个格子,每次只判当前一个位置,剩下的交给下一层。
这道题到底在问什么
- 输入
- t1=[1,3,2,5,7,null,9,8], t2=[2,1,3,null,4,6,8,null,null,null,11]
- 输出
- [3,4,5,5,11,6,17,8,null,null,11]
最优解:为什么这么做
一句话答案:LeetCode 617 合并二叉树的标准解法是同步递归:两棵树从根开始一起往下走,对应位置都有节点就把值相加,一边为空就直接把另一边整棵子树接过来,然后对两边的左孩子、两边的右孩子递归做同样的事。只有两树重叠的部分才真正递归,时间 O(min(m,n))、空间 O(min(h1,h2)) 递归栈。
合并规则到底是什么
给两棵二叉树 t1、t2,把它们按位置叠加成一棵树:某个位置两棵树都有节点,新值等于两值之和;只有一棵有节点,就原样采用那个节点连同它下面的整棵子树。结果树的形状是两棵树形状的并集。可以想象两张画着树的透明胶片叠在一起:重叠处数值相加,不重叠处露出谁就是谁。
为什么这道题天然适合同步递归
合并的定义本身就是递归的:合并两棵树,等于合并两个根,再分别合并两边的左子树、两边的右子树——子问题和原问题一模一样,只是规模更小。于是让两个指针 a、b 同步下行,每一步只处理一对对应位置的节点。相比先把两棵树各自遍历成序列再对齐相加,同步递归根本不用处理两树形状不一致的对齐难题:位置的对应关系由递归结构自动维护,a 的左孩子永远只和 b 的左孩子配对。
一边为空为什么可以整支搬走
递归的终止条件是这题的精髓:a 为空就直接返回 b,b 为空就直接返回 a。理由是一旦一边为空,它下面的所有位置也全是空,按规则这一整片区域的结果都等于另一棵树的原样——那就不必再逐个节点走进去确认,直接把整棵子树接上。这一步不只是写法上的简洁,它直接决定了复杂度:递归只深入两树都有节点的重叠区域,非重叠的部分一步接走、零成本。
原地复用还是新建节点
参考代码选择原地复用:把 b 的值累加进 a(a.val += b.val),左右孩子分别接上递归结果,最后返回 a,全程不新建节点,空间开销最小。如果场景要求不修改输入,也可以每步新建节点、值取两者之和,逻辑完全不变,多花一份结果树的空间。
最容易犯的错是把终止条件写成「一边为空就返回空」——这会把另一边还完好的整棵子树直接丢掉,合并出的树缺一大块。正确语义是「一边为空,答案就是另一边」。
复杂度为什么是 O(min(m,n))
真正的递归只发生在两树重叠的位置:设两棵树分别有 m、n 个节点,重叠位置至多 min(m,n) 个,每个位置做一次相加和两次递归调用,时间 O(min(m,n))。递归栈深等于重叠部分的高度,即 O(min(h1,h2)),最坏两棵树都退化成重合的链时为 O(n)。另一个细节:合并是逐位置的,值相加之后必须继续递归左右孩子,只把两个根相加、忘了往下走,孩子层的叠加根本没发生。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3两棵树像两张透明胶片叠在一起:同步往下走。一边为空是天然的终止条件——直接把另一边整支搬过来,不必再钻进空子树。下面逐个位置看这个叠加过程。
- 4形状 = t1 ∪ t2 的位置并集先看结果树的「骨架」:它的形状是两棵树形状的并集——只要任意一棵在某位置有节点,结果树这里就有。每个位置的值待定(占位 ·)。接下来从根出发,按前序(根→左→右)逐个位置算出合并值。
- 5t1=1, t2=2 → 相加 3走到层序位置 0:t1 这里是 1、t2 这里是 2,两棵树同一个位置都有节点。这时就把两个值相加:1+2 = 3,作为合并后这个位置的新值。
- 6结果树[0] = 3把合并值 3 落进结果树的层序位置 0(绿)。它前面的位置都已确定(蓝)。接着按前序继续递归它的左、右孩子。 同步递归的节奏就是:处理当前位置 → 递归左 → 递归右。
- 7t1=3, t2=1 → 相加 4走到层序位置 1:t1 这里是 3、t2 这里是 1,两棵树同一个位置都有节点。这时就把两个值相加:3+1 = 4,作为合并后这个位置的新值。
- 8结果树[1] = 4把合并值 4 落进结果树的层序位置 1(绿)。它前面的位置都已确定(蓝)。接着按前序继续递归它的左、右孩子。 同步递归的节奏就是:处理当前位置 → 递归左 → 递归右。
- 9t1=5, t2=空 → 搬非空那支走到层序位置 3:t1=5、t2=空,只有一棵树有节点。一边为空时不用算,直接把非空那一支整个搬过来当结果——这正是递归的终止边界,省掉了对空子树的继续递归。
- 10结果树[3] = 5把合并值 5 落进结果树的层序位置 3(绿)。它前面的位置都已确定(蓝)。这一支已整体接上,无需再深入。 同步递归的节奏就是:处理当前位置 → 递归左 → 递归右。
- 11t1=8, t2=空 → 搬非空那支走到层序位置 7:t1=8、t2=空,只有一棵树有节点。一边为空时不用算,直接把非空那一支整个搬过来当结果——这正是递归的终止边界,省掉了对空子树的继续递归。
- 12结果树[7] = 8把合并值 8 落进结果树的层序位置 7(绿)。它前面的位置都已确定(蓝)。这一支已整体接上,无需再深入。 同步递归的节奏就是:处理当前位置 → 递归左 → 递归右。
- 13t1=7, t2=4 → 相加 11走到层序位置 4:t1 这里是 7、t2 这里是 4,两棵树同一个位置都有节点。这时就把两个值相加:7+4 = 11,作为合并后这个位置的新值。
- 14结果树[4] = 11把合并值 11 落进结果树的层序位置 4(绿)。它前面的位置都已确定(蓝)。接着按前序继续递归它的左、右孩子。 同步递归的节奏就是:处理当前位置 → 递归左 → 递归右。
- 15t1=空, t2=11 → 搬非空那支走到层序位置 10:t1=空、t2=11,只有一棵树有节点。一边为空时不用算,直接把非空那一支整个搬过来当结果——这正是递归的终止边界,省掉了对空子树的继续递归。
- 16结果树[10] = 11把合并值 11 落进结果树的层序位置 10(绿)。它前面的位置都已确定(蓝)。这一支已整体接上,无需再深入。 同步递归的节奏就是:处理当前位置 → 递归左 → 递归右。
- 17t1=2, t2=3 → 相加 5走到层序位置 2:t1 这里是 2、t2 这里是 3,两棵树同一个位置都有节点。这时就把两个值相加:2+3 = 5,作为合并后这个位置的新值。
- 18结果树[2] = 5把合并值 5 落进结果树的层序位置 2(绿)。它前面的位置都已确定(蓝)。接着按前序继续递归它的左、右孩子。 同步递归的节奏就是:处理当前位置 → 递归左 → 递归右。
- 19t1=空, t2=6 → 搬非空那支走到层序位置 5:t1=空、t2=6,只有一棵树有节点。一边为空时不用算,直接把非空那一支整个搬过来当结果——这正是递归的终止边界,省掉了对空子树的继续递归。
- 20结果树[5] = 6把合并值 6 落进结果树的层序位置 5(绿)。它前面的位置都已确定(蓝)。这一支已整体接上,无需再深入。 同步递归的节奏就是:处理当前位置 → 递归左 → 递归右。
- 21t1=9, t2=8 → 相加 17走到层序位置 6:t1 这里是 9、t2 这里是 8,两棵树同一个位置都有节点。这时就把两个值相加:9+8 = 17,作为合并后这个位置的新值。
- 22结果树[6] = 17把合并值 17 落进结果树的层序位置 6(绿)。它前面的位置都已确定(蓝)。接着按前序继续递归它的左、右孩子。 同步递归的节奏就是:处理当前位置 → 递归左 → 递归右。
- 23merge(t1, t2) 完成所有位置都合并完了:层序读出来正好是 [3,4,5,5,11,6,17,8,null,null,11]。整道题原地复用 t1 的节点(值就地累加),空间不额外开新树。回头看每一格:都有就相加、只一边就搬过来——同步递归把两棵树干净地叠成了一棵。
⚠️ 容易写错的地方
✗ 错:一边空时返回 null
✓ 对:一边空要返回「另一边」
另一边的整棵子树必须保留,返回 null 会丢节点
✗ 错:只相加根、忘了递归左右
✓ 对:每个对应位置都要合并
合并是逐位置的,不是只动根
✗ 错:新建一整棵结果树
✓ 对:可原地复用 a,把 b 累加进去
省一倍空间,返回 a 即可
完整代码(Java / Python / C++)
Java
class Solution {
public TreeNode mergeTrees(TreeNode a, TreeNode b) {
if (a == null) return b; // 一边空 → 搬另一边整支
if (b == null) return a;
a.val += b.val; // 都非空 → 对应位置相加
a.left = mergeTrees(a.left, b.left); // 递归合并左
a.right = mergeTrees(a.right, b.right); // 递归合并右
return a;
}
}Python
class Solution:
def mergeTrees(self, a: TreeNode, b: TreeNode) -> TreeNode:
if a is None: return b # 一边空 → 搬另一边整支
if b is None: return a
a.val += b.val # 都非空 → 对应位置相加
a.left = self.mergeTrees(a.left, b.left) # 递归合并左
a.right = self.mergeTrees(a.right, b.right) # 递归合并右
return aC++
class Solution {
public:
TreeNode* mergeTrees(TreeNode* a, TreeNode* b) {
if (a == nullptr) return b; // 一边空 → 搬另一边整支
if (b == nullptr) return a;
a->val += b->val; // 都非空 → 对应位置相加
a->left = mergeTrees(a->left, b->left); // 递归合并左
a->right = mergeTrees(a->right, b->right); // 递归合并右
return a;
}
};复杂度
时间
O(min(m,n))
只在两树重叠的位置才递归下去;非重叠支一步搬走
空间
O(min(h1,h2))
递归栈深 = 重叠部分的高度;最坏 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 合并二叉树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这道题为什么用「递归」,换最直接的暴力解会差在哪?+
递归抓住了本题的结构特征,把暴力解里重复的工作省掉;暴力解通常要多嵌套一层枚举,数据一大就超时。具体对比见上文「暴力解及其卡点」与「最优解逐步推演」两节。
时间复杂度为什么是 undefined?怎么推出来的?+
按上文复杂度小节的推导,时间复杂度为 undefined。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 合并二叉树 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。