输出二叉树 图解题解
这道题到底在问什么
- 输入
- root=[1,2]
- 输出
- [["","1",""],["2","",""]] 高 1,矩阵 2 行 3 列
- 输入
- root=[1,2,3,null,4]
- 输出
- 4 落在 res[2][2] 高 2,矩阵 3 行 7 列
最优解:为什么这么做
一句话答案:LeetCode 655 输出二叉树:因为矩阵尺寸完全由树能长多深决定,先量树高定下行数和列数,再用 DFS 把每个节点放到它那行、那段区间的正中,空格填空串,时间 O(m·n)。
把一棵二叉树摆进一张 m×n 的字符串矩阵
给一棵二叉树的根 root,要把它画进一张 m 行 n 列的字符串矩阵。行数 m 是树高 height 加一,列数 n 是 2^(height+1) − 1,根写在顶行正中,每个节点的左右孩子落到下一行、往两侧偏开一段,空格填空串。题面 root=[1,2] 输出 [["","1",""],["2","",""]],高 1、2 行 3 列。
矩阵开多大、每个数落哪一格,都得先算准
难点不在遍历,在两处定位。矩阵多大,取决于树往下长多深:树高多一层,行数多一行、列数翻倍再减一。节点落哪一列,取决于它在第几层、从父节点哪侧分下来。任一处算偏,数就摆错格甚至越界。
尺寸由树高定,每往下一层偏移就对半缩
先求树高 h,也就是从根到最远叶子要走的边数。约定空节点高度是 −1,只有一个节点的树高度恰好是 0。有了 h,m = h+1 行、n = 2^(h+1) − 1 列。安放靠一次 DFS,顺着一条枝走到底再回头。DFS 到每个节点,带着行号 r、列号 c,把值写进 res[r][c],再把 c 减、加偏移量传给左右孩子。偏移量是 2^(h−r−1),根在第 0 层最大,越往下每层对半缩,因为越深的节点能占的列区间越窄。
DFS 在每格写一个值,再把区间对半分给左右孩子
从根开始 dfs(root, 0, (n−1)/2):根站顶行正中。到节点 res[r][c] 先填值,再算偏移 d = 2^(h−r−1),左孩子去 res[r+1][c−d]、右孩子去 res[r+1][c+d],各自站到下一行那半区间的正中。碰到空节点直接返回,那格保持空串。整条递归只在重复一个动作:站中点、把区间对半分往下带。
拿 root=[1,2,3,null,4] 亲手摆一遍
先量高。叶子 3、4 高度都是 0;节点 2 挂着 4,高度 1;根 1 取较大的加一得高度 2,h=2。尺寸定下:m = 2+1 = 3 行,n = 2^3 − 1 = 7 列。根列 = (7−1)/2 = 3,把 1 写进 res[0][3]。
根在第 0 层,偏移 d = 2^(2−0−1) = 2:左孩子 2 落到 res[1][3−2]=res[1][1],右孩子 3 落到 res[1][5]。节点 2 在第 1 层,偏移缩成 d = 2^(2−1−1) = 1,它没有左孩子,右孩子 4 落到 res[2][2]。4 正落在 res[2][2],与题面一致。
空节点高度、列数、偏移,三处约定别记混
空节点高度得记 −1 而不是 0——记成 0,树高虚高一层、行列一起多算。列数是 2^(h+1) − 1 而非 2^h,少写那一倍宽度,右边节点就越界或叠到一起。偏移量随层递减 2^(h−r−1),写成固定值,深层节点全站错列。空格填的是空串,既非 null 也非空格。
矩阵有 h+1 行、2^(h+1) − 1 列,光建表填空串就是 O(m·n);DFS 每个节点只走一次,递归栈最深到树高 O(h),被矩阵盖过,时间空间都是 O(m·n)。单节点是 1×1 矩阵,root=[1,2] 则是 2 行 3 列、根居中孩子落在半边角上。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住「占区间、站中点、左右对半分」这句口诀,下面每一帧都在套它。
- 45 个节点这就是要画的树:根是 50,左孩子 30、右孩子 80;30 底下还挂着 10,80 底下挂着 90。一共 5 个节点,我们要把它们摆进矩阵。
- 5h = 2先量树高。从根一路往下数最长的那条路,50 到 30 再到 10,走了两条边,所以树高 h 等于 2。约定空节点高度是 −1,这样叶子的高度刚好是 0。
- 6m = 3 行, n = 7 列有了 h,矩阵尺寸就定了。行数 m 等于 h 加 1,也就是 3 行;列数 n 等于 2 的 (h+1) 次方再减 1,等于 2 的 3 次方减 1,也就是 7 列。接下来就在这张 3 行 7 列的网格上摆数。
- 73 × 7 全空先把这张 3 行 7 列的矩阵全部填上空串,得到一张干净的网格。现在从根开始,做一次 DFS 把节点一个个放进去。
- 8区间 [0, 6] → 中点 3轮到节点 50。它分到的列区间是 [0, 6](蓝色高亮这一段),中点等于 (0+6) 除以 2,也就是第 3 列。50 就站在这一格的正中间。
- 9res[0][3] = 50把 50 写进 res[0][3],这一格点亮。它还有孩子,接着要把区间对半分给左右子树。
- 10左半 [0, 2] | 右半 [4, 6]50 把自己的区间从中点劈开:左半段 [0, 2] 留给左孩子,右半段 [4, 6] 留给右孩子。两个孩子都掉到下一行,各自再去站自己那半段的中点。
- 11区间 [0, 2] → 中点 1轮到节点 30。它分到的列区间是 [0, 2](蓝色高亮这一段),中点等于 (0+2) 除以 2,也就是第 1 列。30 就站在这一格的正中间。
- 12res[1][1] = 30把 30 写进 res[1][1],这一格点亮。它还有孩子,接着要把区间对半分给左右子树。
- 13左半 [0, 0] | 右半 [2, 2]30 把自己的区间从中点劈开:左半段 [0, 0] 留给左孩子,右半段 [2, 2] 留给右孩子。两个孩子都掉到下一行,各自再去站自己那半段的中点。
- 14区间 [0, 0] → 中点 0轮到节点 10。它分到的列区间是 [0, 0](蓝色高亮这一段),中点等于 (0+0) 除以 2,也就是第 0 列。10 就站在这一格的正中间。
- 15res[2][0] = 10把 10 写进 res[2][0],这一格点亮。它是叶子,没有孩子,这条分支到底了。
- 16右半 [2, 2] 留空30 没有右孩子,右半段 [2, 2] 也就空着。可以看到这些空格正好对应树上缺失的子树位置。
- 17区间 [4, 6] → 中点 5轮到节点 80。它分到的列区间是 [4, 6](蓝色高亮这一段),中点等于 (4+6) 除以 2,也就是第 5 列。80 就站在这一格的正中间。
- 18res[1][5] = 80把 80 写进 res[1][5],这一格点亮。它还有孩子,接着要把区间对半分给左右子树。
- 19左半 [4, 4] | 右半 [6, 6]80 把自己的区间从中点劈开:左半段 [4, 4] 留给左孩子,右半段 [6, 6] 留给右孩子。两个孩子都掉到下一行,各自再去站自己那半段的中点。
- 20左半 [4, 4] 留空80 没有左孩子,所以左半段 [4, 4] 不放任何值,整段一直是空串。空的地方就是树里缺的那条枝。
- 21区间 [6, 6] → 中点 6轮到节点 90。它分到的列区间是 [6, 6](蓝色高亮这一段),中点等于 (6+6) 除以 2,也就是第 6 列。90 就站在这一格的正中间。
- 22res[2][6] = 90把 90 写进 res[2][6],这一格点亮。它是叶子,没有孩子,这条分支到底了。
- 235 个节点全部就位五个节点全部放完。每个值都站在自己区间的正中央,空格保持空串。这张矩阵就是树的格式化布局,从上往下、左右对称地排开。
- 24与答案逐格一致对照一下:第 0 行正中间是 50,第 1 行 30 和 80 对称地分在左右,第 2 行最外侧是 10 和 90。每一格都和正确答案对得上,DFS 区间取中点的办法奏效。
⚠️ 容易写错的地方
✗ 错:空节点高度记成 0
✓ 对:空节点高度记 −1
记成 0 会让树高、进而行列数全部多算一层
✗ 错:列数写成 2^h
✓ 对:列数是 2^(h+1) − 1
少了一倍宽度,右侧节点会越界或叠到一起
✗ 错:偏移量用固定值
✓ 对:偏移量是 2^(h−r−1),随层递减
越往下区间越窄,偏移必须跟着对半缩小
✗ 错:空格填 null 或空格字符
✓ 对:空格填空串 ""
题目明确要求空单元格是空字符串
完整代码(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 printTree(self, root: Optional[TreeNode]) -> List[List[str]]:
def height(root):
if root is None:
return -1
return 1 + max(height(root.left), height(root.right))
def dfs(root, r, c):
if root is None:
return
ans[r][c] = str(root.val)
dfs(root.left, r + 1, c - 2 ** (h - r - 1))
dfs(root.right, r + 1, c + 2 ** (h - r - 1))
h = height(root)
m, n = h + 1, 2 ** (h + 1) - 1
ans = [[""] * n for _ in range(m)]
dfs(root, 0, (n - 1) // 2)
return ansC++
#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:
vector<vector<string>> printTree(TreeNode* root) {
int h = height(root);
int m = h + 1, n = (1 << (h + 1)) - 1;
vector<vector<string>> ans(m, vector<string>(n, ""));
dfs(root, ans, h, 0, (n - 1) / 2);
return ans;
}
void dfs(TreeNode* root, vector<vector<string>>& ans, int h, int r, int c) {
if (!root) return;
ans[r][c] = to_string(root->val);
dfs(root->left, ans, h, r + 1, c - pow(2, h - r - 1));
dfs(root->right, ans, h, r + 1, c + pow(2, h - r - 1));
}
int height(TreeNode* root) {
if (!root) return -1;
return 1 + max(height(root->left), height(root->right));
}
};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 List<List<String>> printTree(TreeNode root) {
int h = height(root);
int m = h + 1, n = (1 << (h + 1)) - 1;
String[][] res = new String[m][n];
for (int i = 0; i < m; ++i) {
Arrays.fill(res[i], "");
}
dfs(root, res, h, 0, (n - 1) / 2);
List<List<String>> ans = new ArrayList<>();
for (String[] t : res) {
ans.add(Arrays.asList(t));
}
return ans;
}
private void dfs(TreeNode root, String[][] res, int h, int r, int c) {
if (root == null) {
return;
}
res[r][c] = String.valueOf(root.val);
dfs(root.left, res, h, r + 1, c - (1 << (h - r - 1)));
dfs(root.right, res, h, r + 1, c + (1 << (h - r - 1)));
}
private int height(TreeNode root) {
if (root == null) {
return -1;
}
return 1 + Math.max(height(root.left), height(root.right));
}
}复杂度
时间
O(m × n)
矩阵有 m=(h+1) 行、n=2^(h+1)−1 列,建表与填空串就要 O(m·n);DFS 只访问每个节点一次,被填表开销盖过
空间
O(m × n)
结果矩阵本身占 O(m·n);递归栈深度是树高 O(h),相比矩阵更小
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 输出二叉树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
偏移量为什么是 2^(h−r−1),会随层减半?+
因为每往下一层,一个节点能占的列区间就对半缩小。根占满整行,偏移最大;越往下区间越窄,孩子要站到自己那半段的正中,偏移自然跟着减半。式子里 r 是当前行号,r 越大,h−r−1 越小,2 的这个幂就越小,正好对上区间一层层变窄。
用 DFS 还是 BFS 都行吗?+
都可以,关键是每放一个节点都要知道它的行 r 和列 c。DFS 递归时天然把 r、c 当参数一路传下去,最顺手;BFS 逐层安放也行,只是要在队列里额外给每个节点存上列号或所在区间,写起来稍微绕一点。这道题两种写法复杂度一样,都是 O(m·n)。
为什么空节点的高度要特意记成 −1?+
为了让叶子的高度正好是 0。高度是从当前点到最远叶子的边数,叶子往下没有边,它的两个空孩子若记高度 0、叶子取 max 再加一就成了 1,层数凭空多一层,矩阵行列全部多算。把空节点记 −1,叶子 max(−1,−1)+1=0,层数才对得上真实深度。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 输出二叉树 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。