根据描述创建二叉树 图解题解
这道题到底在问什么
- 输入
- descriptions=[[20,15,1],[20,17,0],[50,20,1],[50,80,0],[80,19,1]]
- 输出
- 根=50 · 层序 [50,20,80,15,17,19]
- 输入
- descriptions=[[1,2,1],[2,3,0],[3,4,1]]
- 输出
- 根=1 · [1,2,null,null,3,4]
最优解:为什么这么做
一句话答案:LeetCode 2196 根据描述创建二叉树:哈希表按值建节点、取节点、接边,同一个值全程只留一个实例;根是唯一没当过孩子的值,时间 O(n)、空间 O(n)。
一堆父子描述,怎么拼回原来那棵树
给一个二维数组 descriptions,每条是 [parent, child, isLeft]:isLeft 是 1 表示 child 是 parent 的左孩子,0 是右孩子。各节点值互不相同,让你拼出二叉树、返回根节点。题面例子 descriptions=[[20,15,1],[20,17,0],[50,20,1],[50,80,0],[80,19,1]],根是 50,层序遍历是 50,20,80,15,17,19。
读一条就新建一个节点,两处会崩
描述是乱序的,没法从根往下顺着建。第一反应是读一条就把两个值各造节点、按 isLeft 接边,但两处会崩:同一个值会反复出现,20 既在 [20,15,1] 当父亲、又在 [50,20,1] 当孩子,重复造新节点会让父子指向不同实例,那半棵就断开;根也不能拿第一条的 parent 当,它其实可能是别人的孩子。
同值只留一个节点,根是没人认领的那个
先解决同值:用哈希表按值存节点,也就是给每个值配一张能直接查到它节点的表。每读到一个值先查表,有就取旧节点复用,没有才新建,同一个值全程只有一个实例。
再解决找根:另开一个 children 集合,凡当过 child 的值都塞进去。所有值里唯一没进这个集合的就是根,它从没被任何描述当成孩子。
一趟扫描接边,末尾用差集挑根
代码就一趟遍历:每条 [parent, child, isLeft],parent、child 缺谁补建谁,child 记进 children 集合,按 isLeft 让 parent 左或右指针指向 child 节点。扫完拿所有值的集合减去 children,剩下唯一那个值对应的节点就是根。
五条描述走一遍,50 怎么浮出来
拿题面五条逐条过。[20,15,1]:20、15 都新建,20 左指向 15。[20,17,0]:20 复用,17 新建,20 右指向 17。[50,20,1]:50 新建,20 复用,50 左指向 20,20 连着 15、17 挂到 50 底下。[50,80,0]:50 复用,80 新建,50 右指向 80。[80,19,1]:80 复用,19 新建,80 左指向 19。
五条读完,当过孩子的是 {15,17,20,80,19},出现过的值 {15,17,20,50,80,19} 减掉它只剩 50。50 就是根,从它层序遍历,输出就是 50,20,80,15,17,19。
复用一松手,半棵树就断开
复杂度:设描述 n 条,每条只做常数次哈希操作,建取节点、接边、加值;最后找根遍历所有值。时间 O(n),哈希表和 children 集合都不超过 2n 量级,空间 O(n)。
几个地方一松手就错。同值第二次出现必须取旧节点,重建一个会让父子指向不同实例,那半棵树当场断开。判根别认第一条的 parent,乱序里它常是别人的孩子。一个值还能既当父又当子:20 是 15、17 的父亲、又是 50 的孩子,照样进集合、照样不是根。单条描述时 parent 就是根。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记牢这两句:哈希表保证同名同节点、乱序也能接对边;孩子集合专门用来挑根。下面从第一条描述开始,一条一条走。
- 4哈希表 = 空舞台先用淡色把这六个值将来所在的位置摆出来,当作最终形态的参照。此刻哈希表还是空的,一个节点都没真正建。约定颜色:淡色表示还没建,蓝色表示已经进了哈希表,绿色表示这个值当过别人的孩子。下面每读一条描述,该新建的值就会亮起来。
- 5new/取 20 与 15读第一条描述 [20,15,1]。parent 20 和 child 15 都第一次出现,各新建一个节点。现在紫色是父亲 20,橙色是孩子 15。
- 620.left = 15isLeft 是 1,把 15 接到 20 的左孩子指针上。看 20 到 15 这条边,归属就定下来了。
- 7孩子集合 += 1515 现在是别人的孩子了,把它记进孩子集合,同时它变成绿色。凡是进了这个集合的值,都不可能是根。目前孩子集合里有 15。
- 8new/取 20 与 17读第二条描述 [20,17,0]。parent 20 已在哈希表里,取出复用;child 17 是新的,新建。现在紫色是父亲 20,橙色是孩子 17。
- 920.right = 17isLeft 是 0,把 17 接到 20 的右孩子指针上。看 20 到 17 这条边,归属就定下来了。
- 10孩子集合 += 1717 现在是别人的孩子了,把它记进孩子集合,同时它变成绿色。凡是进了这个集合的值,都不可能是根。目前孩子集合里有 15, 17。
- 11new/取 50 与 20读第三条描述 [50,20,1]。parent 50 是新的,新建;child 20 已在哈希表里,取出复用。现在紫色是父亲 50,橙色是孩子 20。
- 1250.left = 20isLeft 是 1,把 20 接到 50 的左孩子指针上。看 50 到 20 这条边,归属就定下来了。这一步很关键:20 顶着 15、17 那半棵小树,整个被挂到了主干 50 底下。
- 13孩子集合 += 2020 现在是别人的孩子了,把它记进孩子集合,同时它变成绿色。凡是进了这个集合的值,都不可能是根。目前孩子集合里有 20, 15, 17。
- 14new/取 50 与 80读第四条描述 [50,80,0]。parent 50 已在哈希表里,取出复用;child 80 是新的,新建。现在紫色是父亲 50,橙色是孩子 80。
- 1550.right = 80isLeft 是 0,把 80 接到 50 的右孩子指针上。看 50 到 80 这条边,归属就定下来了。
- 16孩子集合 += 8080 现在是别人的孩子了,把它记进孩子集合,同时它变成绿色。凡是进了这个集合的值,都不可能是根。目前孩子集合里有 20, 80, 15, 17。
- 17new/取 80 与 19读最后一条描述 [80,19,1]。parent 80 已在哈希表里,取出复用;child 19 是新的,新建。注意 80 之前作为别人的孩子已经登记过绿色了,这次它换个身份当父亲。现在紫色是父亲 80,橙色是孩子 19。
- 1880.left = 19isLeft 是 1,把 19 接到 80 的左孩子指针上。看 80 到 19 这条边,归属就定下来了。到这里,整棵树的边就全接完了。
- 19孩子集合 += 1919 现在是别人的孩子了,把它记进孩子集合,同时它变成绿色。凡是进了这个集合的值,都不可能是根。目前孩子集合里有 20, 80, 15, 17, 19。
- 20待找: 不在孩子集合的值五条描述读完,边全接好了。可根是谁?规律只有一条:根是唯一一个从来没当过别人孩子的值。现在孩子集合里是 20, 80, 15, 17, 19,绿色的这五个都当过孩子。只剩蓝色的 50 还没进过集合,拿几个值一个个去对照确认一下。
- 2120 ∈ 孩子集合20 在孩子集合里,它当过别人的孩子,不是根,跳过看下一个。
- 2215 ∈ 孩子集合15 在孩子集合里,它当过别人的孩子,不是根,跳过看下一个。
- 2317 ∈ 孩子集合17 在孩子集合里,它当过别人的孩子,不是根,跳过看下一个。
- 2450 ∉ 孩子集合轮到 50,它不在孩子集合里。也就是说,没有任何一条描述把 50 当成过孩子,它就是这棵树的根节点。
- 25根 = 50 · 层序 50,20,80,15,17,19找到根 50,从它出发,这棵树就是答案,层序遍历正好是 50,20,80,15,17,19。回头看整道题其实就两步:用哈希表按值建节点、取节点、接边,保证乱序也接得对;再用孩子集合挑出那个没当过孩子的根。
⚠️ 容易写错的地方
✗ 错:每读到一个值就 new 一个新节点
✓ 对:先查哈希表,已存在就取出复用
同一个值必须是同一个节点。重复 new 会让父子两头指向不同实例,树就断裂了
✗ 错:默认第一条描述的 parent 就是根
✓ 对:根要靠没当过孩子来判定
描述是乱序的,第一条的 parent 完全可能是别人的孩子,比如示例里第一条的 20 其实是 50 的孩子
✗ 错:以为进了孩子集合的值不会再当父亲
✓ 对:一个值可以既当父亲又当孩子
像 20 既是 15、17 的父亲又是 50 的孩子,它照样进集合、照样不是根,只有全程没当过孩子的才是根
完整代码(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 createBinaryTree(self, descriptions: List[List[int]]) -> Optional[TreeNode]:
nodes = defaultdict(TreeNode)
children = set()
for parent, child, isLeft in descriptions:
if parent not in nodes:
nodes[parent] = TreeNode(parent)
if child not in nodes:
nodes[child] = TreeNode(child)
children.add(child)
if isLeft:
nodes[parent].left = nodes[child]
else:
nodes[parent].right = nodes[child]
root = (set(nodes.keys()) - children).pop()
return nodes[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:
TreeNode* createBinaryTree(vector<vector<int>>& descriptions) {
unordered_map<int, TreeNode*> nodes;
unordered_set<int> children;
for (const auto& d : descriptions) {
int parent = d[0], child = d[1], isLeft = d[2];
if (nodes.find(parent) == nodes.end()) {
nodes[parent] = new TreeNode(parent);
}
if (nodes.find(child) == nodes.end()) {
nodes[child] = new TreeNode(child);
}
if (isLeft) {
nodes[parent]->left = nodes[child];
} else {
nodes[parent]->right = nodes[child];
}
children.insert(child);
}
for (const auto& [k, v] : nodes) {
if (children.find(k) == children.end()) {
return v;
}
}
return nullptr;
}
};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 TreeNode createBinaryTree(int[][] descriptions) {
Map<Integer, TreeNode> nodes = new HashMap<>();
Set<Integer> children = new HashSet<>();
for (int[] d : descriptions) {
int parent = d[0], child = d[1], isLeft = d[2];
if (!nodes.containsKey(parent)) {
nodes.put(parent, new TreeNode(parent));
}
if (!nodes.containsKey(child)) {
nodes.put(child, new TreeNode(child));
}
if (isLeft == 1) {
nodes.get(parent).left = nodes.get(child);
} else {
nodes.get(parent).right = nodes.get(child);
}
children.add(child);
}
for (Map.Entry<Integer, TreeNode> e : nodes.entrySet()) {
if (!children.contains(e.getKey())) {
return e.getValue();
}
}
return null;
}
}复杂度
时间
O(n)
n 是描述条数。每条描述做常数次哈希操作:建或取两个节点、接一条边、往集合加一个值;最后遍历所有值找根,值的个数不超过 2n,整体随 n 线性增长
空间
O(n)
按峰值算。哈希表最多存约 2n 个节点,因为每条描述最多引入两个新值;children 集合最多 n 个值。二者都是 O(n),不随 n 平方增长
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 根据描述创建二叉树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么按值用哈希表存节点,不能按顺序一路建下去?+
因为描述是乱序的,child 可能比它的 parent 先出现,一个值也可能先当孩子、后当父亲,按顺序建根本不知道从哪个节点起头。改用值到节点的哈希表,不管什么顺序读到一个值,都能在常数时间取到或新建它对应的节点,保证同一个值全程只有一个实例,接边永远接在同一个节点上,乱序也不会把树接断。
根到底怎么找,复杂度是多少?+
维护一个 children 集合,凡当过 child 的值都放进去,所有出现过的值里唯一不在集合里的就是根,因为它从没被任何描述当成过孩子。设描述有 n 条,建表、接边、找根都是遍历级别,时间 O(n);哈希表和集合都不超过 2n 量级,空间 O(n)。
Python、Java、C++ 找根的写法差在哪?+
思路完全一样,只是收尾取那个唯一值的手法不同。Python 有现成的集合差集,把所有键组成的集合减去 children 集合,剩下唯一元素 pop 出来。Java 和 C++ 没有直接的差集运算,就遍历哈希表的所有键,返回第一个不在 children 里的值。两种写法挑出来的,都是那个没当过孩子的根。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 根据描述创建二叉树 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。