从根到叶的二进制数之和 图解题解
这道题到底在问什么
- 输入
- root=[1,0,1,0,1,0,1]
- 输出
- 22
最优解:为什么这么做
一句话答案:LeetCode 1022 从根到叶的二进制数之和:每条根到叶路径读成一个二进制数,DFS 带累积值 cur、每下一层 cur 乘 2 再并上当前位,到叶子结算,时间 O(n)、空间 O(h)。
根到叶的路径,怎么读成一个二进制数
给一棵二叉树 root,每个节点的值不是 0 就是 1。从根一路走到某片叶子,把沿途的位按先后连起来,就是一个二进制数,最上面的根是最高位、最下面的叶是最低位。要求把每片叶子对应的那个数全加起来。题面例子 root=[1,0,1,0,1,0,1],四条根到叶路径分别读出 100、101、110、111,也就是 4、5、6、7,加起来 22。
先把整条路径存下来再转十进制,差在哪
位数最多和树高一样,看着不长,于是容易想着先 DFS 把每条根到叶的路径收成一串 0 和 1,再逐串换算成十进制相加。这样得为每条路径单开一块存储,走完还要再遍历一遍做换算。其实需要留在手里的只有一个当前值,边走边算就够,没必要把整条路径攒着。
二进制从高位读,深入一层就左移一位
二进制数是从最高位往低位读的。已经拼到手的高位,每往下深入一层,相当于整体左移一位、空出最低位,再把这一层的 0 或 1 填进去。所以维护一个累积值 cur:进入一个节点时,cur 乘 2 就是左移一位,再加上这个节点的位。根对应最高位、叶对应最低位,一路拼到叶子,cur 正好是这条路径读出的数。
递归到每个节点做什么、又返回什么
写成递归 dfs(node, cur):进来先更新 cur,让它乘 2 再加上 node 的值。接着判断是不是叶子——代码用 node 的左孩子和右孩子引用相等这一句来判定,因为叶子左右都是空、两个空引用恰好相等;只要有一个孩子还在,就不相等。是叶子,这条路径读完了,直接返回当前的 cur。不是叶子,就返回左子树和右子树两次递归结果之和。总和靠层层返回值相加汇总上来,不用额外开全局变量去累计。
拿 [1,0,1,0,1,0,1] 把四条路径都拼一遍
根是 1,进来 cur 从 0 变成 0 乘 2 加 1,等于 1。往左走到 0:cur 变成 1 乘 2 加 0,等于 2。再往左到叶子 0:cur 变成 2 乘 2 加 0,等于 4——这条路径 100 读出 4,返回 4。退回上一个 0,那层 cur 还是 2,改走右孩子 1:cur 变成 2 乘 2 加 1,等于 5,是叶子返回 5。
根的右半边同理。根传下来的 cur 是 1,走到右孩子 1:cur 变成 1 乘 2 加 1,等于 3。往左到叶子 0:3 乘 2 加 0 等于 6,返回 6;退回后走右孩子 1:3 乘 2 加 1 等于 7,返回 7。四片叶子交回 4、5、6、7,一层层加上去,4 加 5 加 6 加 7 得 22。
只走一遍的开销,传值和认叶别搞混
每个节点只在递归里访问一次,进出各做几步常数运算,时间就是 O(n),n 是节点数。额外空间是递归栈,深度等于当前路径长度、也就是树高 h;匀称时约 log n 级,长成单链最坏到 O(n)。
cur 一定要当参数传,靠的正是参数各走各的:右孩子拿到的是岔路口那层的 cur,左边走多远都不沾它。改成全局变量、回到岔路口忘了还原,右边就会接着左边的残值往下拼,答案整个偏掉。识别叶子也别退回去挨个问孩子空不空,一句引用相等更省,前提是非叶节点至少挂着一个孩子、必然不等。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3记住这套「进节点就 cur 乘 2 加当前位、到叶子加进答案」,下面每一帧都在套它。
- 4cur=0, sum=0开局答案 sum 是 0,手里的二进制值 cur 也从 0 开始。DFS 这就从根往下探,每经过一个节点把 cur 乘 2 再加这个节点的位,到叶子时结算进 sum。
- 5读取 1走到这个节点,它的值是 1。进来时手里的 cur 还是 0,下一步要把这个 1 接到二进制数的末尾。
- 60×2+1=1把 cur 乘 2 再加上 1:0×2+1=1。这等于在二进制末尾追加一位 1,现在根到这里是 1,十进制就是 1。
- 7读取 0走到这个节点,它的值是 0。进来时手里的 cur 还是 1,下一步要把这个 0 接到二进制数的末尾。
- 81×2+0=2把 cur 乘 2 再加上 0:1×2+0=2。这等于在二进制末尾追加一位 0,现在根到这里是 10,十进制就是 2。
- 9读取 0走到这个节点,它的值是 0。进来时手里的 cur 还是 2,下一步要把这个 0 接到二进制数的末尾。
- 102×2+0=4把 cur 乘 2 再加上 0:2×2+0=4。这等于在二进制末尾追加一位 0,现在根到这里是 100,十进制就是 4。
- 11sum += 4 → 4这个节点没有孩子,是叶子,路径读完了。根到它的二进制是 100,十进制 4。把它加进答案:sum 从 0 变成 4。
- 12cur 恢复 2左子树都结算完了,回到这个值为 0 的节点。注意 cur 又变回 2:因为 cur 是顺着参数往下传的,每条路径各算各的,左边走过完全不影响右边。接着走右孩子。
- 13读取 1走到这个节点,它的值是 1。进来时手里的 cur 还是 2,下一步要把这个 1 接到二进制数的末尾。
- 142×2+1=5把 cur 乘 2 再加上 1:2×2+1=5。这等于在二进制末尾追加一位 1,现在根到这里是 101,十进制就是 5。
- 15sum += 5 → 9这个节点没有孩子,是叶子,路径读完了。根到它的二进制是 101,十进制 5。把它加进答案:sum 从 4 变成 9。
- 16cur 恢复 1左子树都结算完了,回到这个值为 1 的节点。注意 cur 又变回 1:因为 cur 是顺着参数往下传的,每条路径各算各的,左边走过完全不影响右边。接着走右孩子。
- 17读取 1走到这个节点,它的值是 1。进来时手里的 cur 还是 1,下一步要把这个 1 接到二进制数的末尾。
- 181×2+1=3把 cur 乘 2 再加上 1:1×2+1=3。这等于在二进制末尾追加一位 1,现在根到这里是 11,十进制就是 3。
- 19读取 0走到这个节点,它的值是 0。进来时手里的 cur 还是 3,下一步要把这个 0 接到二进制数的末尾。
- 203×2+0=6把 cur 乘 2 再加上 0:3×2+0=6。这等于在二进制末尾追加一位 0,现在根到这里是 110,十进制就是 6。
- 21sum += 6 → 15这个节点没有孩子,是叶子,路径读完了。根到它的二进制是 110,十进制 6。把它加进答案:sum 从 9 变成 15。
- 22cur 恢复 3左子树都结算完了,回到这个值为 1 的节点。注意 cur 又变回 3:因为 cur 是顺着参数往下传的,每条路径各算各的,左边走过完全不影响右边。接着走右孩子。
- 23读取 1走到这个节点,它的值是 1。进来时手里的 cur 还是 3,下一步要把这个 1 接到二进制数的末尾。
- 243×2+1=7把 cur 乘 2 再加上 1:3×2+1=7。这等于在二进制末尾追加一位 1,现在根到这里是 111,十进制就是 7。
- 25sum += 7 → 22这个节点没有孩子,是叶子,路径读完了。根到它的二进制是 111,十进制 7。把它加进答案:sum 从 15 变成 22。
- 26答案 sum = 22四片叶子分别给出二进制 100、101、110、111,也就是 4、5、6、7。把它们加起来 4+5+6+7 等于 22,这就是答案。整个过程只是一次 DFS,沿途累积 cur、到叶子结算。
⚠️ 容易写错的地方
✗ 错:先把所有路径存下来再统一转十进制
✓ 对:边走边维护 cur=cur×2+val
沿途累积省掉额外存储,DFS 天然把每条路径分开
✗ 错:把 cur 当全局变量,回溯后忘了还原
✓ 对:cur 作为参数随递归传入
参数传值每条路径独立,不需要手动回退;全局变量才要小心还原
✗ 错:挨个判断左右孩子是否都空来识别叶子
✓ 对:代码用 left==right 一句判叶
叶子左右孩子都是空、引用相等;非叶至少一个孩子非空,必不相等
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def sumRootToLeaf(self, root: Optional[TreeNode]) -> int:
def dfs(root: Optional[TreeNode], x: int) -> int:
if root is None:
return 0
x = x << 1 | root.val
if root.left == root.right:
return x
return dfs(root.left, x) + dfs(root.right, x)
return dfs(root, 0)C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int sumRootToLeaf(TreeNode* root) {
function<int(TreeNode*, int)> dfs = [&]( TreeNode* root, int x ) -> int {
if (!root) {
return 0;
}
x = x << 1 | root->val;
if (root->left == root->right) {
return x;
}
return dfs(root->left, x) + dfs(root->right, x);
};
return dfs(root, 0);
}
};Java
import java.util.*;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public int sumRootToLeaf(TreeNode root) {
return dfs(root, 0);
}
private int dfs(TreeNode root, int x) {
if (root == null) {
return 0;
}
x = x << 1 | root.val;
if (root.left == root.right) {
return x;
}
return dfs(root.left, x) + dfs(root.right, x);
}
}复杂度
时间
O(n)
每个节点恰好访问一次,进出各做常数次运算
空间
O(h)
递归栈深等于树高 h;平衡时约 O(log n),退化成链时最坏 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 从根到叶的二进制数之和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么 cur 乘 2 再加节点值,就能从最高位开始拼出这个二进制数?+
二进制是从最高位往低位读的。每深入一层,等于把已经拼好的高位整体左移一位、空出最低位,再把这一层的 0 或 1 填进去。根在最上面对应最高位,叶在最下面对应最低位,一路乘 2 加位拼到叶子,读出的顺序正好是题目要的「从最高有效位开始」。
节点数能到一千,会不会溢出、递归会不会把栈压爆?+
题目已经保证答案落在 32 位整数范围内,单条路径的位数就是树高,位数有限,用普通整数存 cur 足够。递归深度等于树高,如果树退化成一条链,栈深可达 O(n),担心压爆可以改成显式用一个栈的迭代写法,进节点更新 cur、到叶子结算,逻辑完全一样。
代码用左右孩子引用相等判叶子,可靠吗?+
可靠。叶子的左右孩子都是空,两个空引用相等,这一句就成立;只要节点还挂着任意一个孩子,那个孩子非空、和另一边不相等,判定就为假。比起分别去问左孩子空不空、右孩子空不空再取与,一句引用相等更短,含义也一样。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 从根到叶的二进制数之和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。