根据二叉树创建字符串 图解题解
这道题到底在问什么
- 输入
- root=[1,2,3,4]
- 输出
- "1(2(4))(3)"
- 输入
- root=[1,2,3,null,4]
- 输出
- "1(2()(4))(3)"
最优解:为什么这么做
一句话答案:LeetCode 606 根据二叉树创建字符串:按前序遍历边走边拼串,叶子只写值、右孩子空就省掉那组括号、左空右非空必须留一对 () 占位保住一一映射,时间 O(n)。
要把二叉树变成什么样的字符串
给一棵二叉树的根 root,按前序遍历,也就是先写自己再写左右子树,压成一行字符串:节点值在前,左右子树各用一对括号包住。省掉某对空括号后字符串还能唯一还原回这棵树,就省掉它。题面 root=[1,2,3,4] 拼成 "1(2(4))(3)",另一个只挪一个孩子,差别全在左孩子在不在。
括号全写满,串里会多出哪些空壳
一个偷懒的拼法是每个节点都写成 值(左子树)(右子树)。这样叶子 4 会拼成 "4()()"、只有左孩子的节点右边也拖一对 (),满树都是空气括号。可题目要省掉所有不影响还原的空括号,写满的留不住。
右孩子空能省、左孩子空却不能,差在哪
省括号的底线是省完后字符串还能唯一读回原来那棵树。右孩子为空时那对括号能省:左孩子已写在前面、位置定死,后面不再冒括号就代表右边没东西,只有左孩子的节点写成 值(左) 就够。但左孩子空、右孩子还在时那对空 () 必须留:省成 值(右),读串时会把里头第一段当成左孩子,右孩子被顶到左孩子的位置、成了另一棵树;留一对空 () 占位,后面那对 (右) 才认成右孩子。
递归到一个节点,该返回哪一段串
把拼串写成递归函数,每到一个节点只返回它这一小段。空节点返回空串;叶子,也就是左右孩子都没有,直接返回值、不添括号;右孩子空、左孩子在的节点返回 值(左),右边不写;右孩子在时不管左孩子,都返回 值(左)(右)——左孩子若空,左段是空串,拼出来正好是 值()(右),那对必须留的空 () 就这么冒出来。判完这四种,根返回的那段就是答案。
两棵四节点的树,分别拼成什么
先看 root=[1,2,3,4]:1 左孩子 2、右孩子 3,2 左孩子 4。1 左右都在,拼 值(左)(右)。到 2,右孩子空、左孩子 4 在,返回 值(左),4 是叶子返回 "4",2 这段是 "2(4)";3 是叶子返回 "3"。1 拼成 "1(2(4))(3)",跟题面输出一致。
再看 root=[1,2,3,null,4],2 的左孩子挪空、右孩子换成 4。到 2 时左孩子空、右孩子 4 在,落进最后一种,返回 值(左)(右):左边空串、右边 4 返回 "4",拼成 "2()(4)",中间那对空 () 正占住空左孩子的位置。1 和 3 不变,最终 "1(2()(4))(3)",差别只在 2 这段那对空括号留没留。
拼串会不会拖慢,哪三种树最该拿去试
遍历每个节点一次是 O(n)。但参考解法递归返回一整段字符串再往上拼,像 Python 的 f-string、C++/Java 的字符串相加,每拼一次都要复制下层整段;链状的树上越靠上复制的串越长,累计最坏滑到 O(n²)。想稳在 O(n),改用 StringBuilder 往末尾追加、不反复复制即可。空间是递归栈深度 O(h),链状树最坏 O(n)。
拿三种树戳:单节点的树只该吐一个值、别拖括号;左孩子空、右孩子在的树,那对空 () 得原样还在,省了右子树就会被当成左孩子;只有左孩子的树,右边整组括号得省掉,多一对就不是最简。
▶ 动画逐步走查(共 27 步)——想跟着动画一帧帧对照就展开
- 3记住三句话:值在前、左子树必带括号、右子树有才带。空括号只在「左空右非空」时保留,下面每帧都在套它。
- 4答案串为空开局答案串为空。我们从根节点 50 开始,按前序遍历:先写当前节点,再写左子树、右子树。盯住右侧的「已生成串」,它会一个字符一个字符地长出来。
- 5写下 50走到节点 50(紫色),前序遍历第一步就是把它的值写进串,现在串是 50。
- 6分析 50 左右50 左右孩子都在,接下来要写「(左子树)(右子树)」两组括号。
- 7写下 (给 50 写下左括号,准备把它的左子树装进去。串变成 50(。
- 8写下 30走到节点 30(紫色),前序遍历第一步就是把它的值写进串,现在串是 50(30。
- 9分析 30 左右30 左孩子空、但右孩子 60 在,这就是关键边界:左边要保留一对空括号 "()" 占位。
- 10写下 (给 30 写下左括号,准备把它的左子树装进去。串变成 50(30(。
- 11保留 ()30 左孩子是空的,但因为右孩子还在,这对括号不能省。写成空的 "()" 占住左孩子的位置,否则右子树会被误读成左孩子。串变成 50(30()。
- 12写下 (给 30 写下右子树的开括号 (,进入它的右子树。串变成 50(30()(。
- 13写下 60走到节点 60(紫色),前序遍历第一步就是把它的值写进串,现在串是 50(30()(60。
- 1460 是叶子60 左右都没有孩子,是叶子。叶子后面什么括号都不写,直接它的值就完事,标绿表示已处理。
- 15写下 )30 的右子树写完,补上闭合的右括号。现在串是 50(30()(60)。
- 1630 片段完成30 的左右子树全部写完,它对应的片段就是 30()(60),标绿收工。
- 17写下 )50 的左子树写完了,补上闭合的右括号,左半部分收口。现在串是 50(30()(60))。
- 18写下 (给 50 写下右子树的开括号 (,进入它的右子树。串变成 50(30()(60))(。
- 19写下 80走到节点 80(紫色),前序遍历第一步就是把它的值写进串,现在串是 50(30()(60))(80。
- 20分析 80 左右80 只有左孩子、右孩子是空的,左括号要写,右边那组括号可以省略。
- 21写下 (给 80 写下左括号,准备把它的左子树装进去。串变成 50(30()(60))(80(。
- 22写下 70走到节点 70(紫色),前序遍历第一步就是把它的值写进串,现在串是 50(30()(60))(80(70。
- 2370 是叶子70 左右都没有孩子,是叶子。叶子后面什么括号都不写,直接它的值就完事,标绿表示已处理。
- 24写下 )80 的左子树写完了,补上闭合的右括号,左半部分收口。现在串是 50(30()(60))(80(70)。
- 25右子树括号整组省略80 右孩子是空的,而且省掉不会破坏一一映射,所以右边那对括号整组省略,一个字符都不写。
- 2680 片段完成80 的左右子树全部写完,它对应的片段就是 80(70),标绿收工。
- 27写下 )50 的右子树写完,补上闭合的右括号。现在串是 50(30()(60))(80(70))。
- 2850 片段完成50 的左右子树全部写完,它对应的片段就是 50(30()(60))(80(70)),标绿收工。
- 29答案 = 50(30()(60))(80(70))整棵树前序走完,最终答案是 50(30()(60))(80(70))。回看两处省略:30 的左孩子空但右孩子在,保留了 "()";80 只有左孩子,右子树那组括号被整组省掉。这正是题目省略规则的精髓。
⚠️ 容易写错的地方
✗ 错:左空右非空时也省掉空括号
✓ 对:必须保留 "()" 占位
省了会让右子树被读成左孩子,破坏一一映射
✗ 错:给每个节点都写满两对括号
✓ 对:右孩子空时整组省略
题目要省略所有不影响映射的空括号,留着就不是最简形式
✗ 错:叶子后面还写一对 "()"
✓ 对:叶子只写值
叶子没孩子,任何括号都多余
完整代码(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 tree2str(self, root: Optional[TreeNode]) -> str:
def dfs(root):
if root is None:
return ''
if root.left is None and root.right is None:
return str(root.val)
if root.right is None:
return f'{root.val}({dfs(root.left)})'
return f'{root.val}({dfs(root.left)})({dfs(root.right)})'
return dfs(root)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:
string tree2str(TreeNode* root) {
if (!root) return "";
if (!root->left && !root->right) return to_string(root->val);
if (!root->right) return to_string(root->val) + "(" + tree2str(root->left) + ")";
return to_string(root->val) + "(" + tree2str(root->left) + ")(" + tree2str(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 String tree2str(TreeNode root) {
if (root == null) {
return "";
}
if (root.left == null && root.right == null) {
return root.val + "";
}
if (root.right == null) {
return root.val + "(" + tree2str(root.left) + ")";
}
return root.val + "(" + tree2str(root.left) + ")(" + tree2str(root.right) + ")";
}
}复杂度
时间
O(n)~O(n²)
遍历每个节点是 O(n);但本题参考解是「递归返回整段字符串直接拼接」(Python f-string / C++ string + / Java +),链状树上最坏会退化到 O(n²);要稳定 O(n) 需用 StringBuilder / list buffer 等可变缓冲追加
空间
O(h)
递归栈深度等于树高 h,最坏(链状树)O(n);不计输出串
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 根据二叉树创建字符串 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么「左空右非空」那对空括号不能省?+
因为字符串要和树一一对应。如果省成 "值(右)",读串时会把括号里第一段当成左孩子,本来的右孩子就被顶到了左孩子的位置,还原出来是另一棵树。保留一对空 "()" 占住左孩子的槽位,后面那对 "(右)" 才会被正确读成右孩子。
为什么「右孩子空」反而能把那组括号省掉?+
左孩子已经写在前面、位置确定了,后面不再出现括号就意味着没有右孩子,不会有第二种读法。题目要求省略所有不影响一一对应的空括号,这组正是可省的,留着反而不是最简形式。注意这跟左空右非空正好相反:右空省、左空留,别记混。
为什么最坏会到 O(n²),不是遍历一遍的 O(n) 吗?+
遍历节点确实是 O(n),慢在拼接方式。参考解法每层递归都返回一整段字符串再让上层拼,f-string 或字符串相加每拼一次都要整段复制。树退化成一条链时,越往上复制的串越长,加起来就到 O(n²)。改用 StringBuilder 或 list 缓冲、边走边往末尾追加,就能压回稳定的 O(n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 根据二叉树创建字符串 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。