根据前序和后序遍历构造二叉树 图解题解
这道题到底在问什么
- 输入
- preorder=[50,30,20,40,80,60,90], postorder=[20,40,30,60,90,80,50]
- 输出
- 层序 [50,30,80,20,40,60,90]
最优解:为什么这么做
一句话答案:LeetCode 889 根据前序和后序遍历构造二叉树:前序开头必是根,前序第二个是左子树的根,去后序里定出左子树多大,把两个数组各切两半递归,哈希表定位做到 O(n)。
两个数组要还原成同一棵树
给你同一棵二叉树的前序遍历数组 preorder 和后序遍历数组 postorder,值互不相同,请把这棵树搭出来,前序开头就是整棵树的根。题面给的 preorder=[50,30,20,40,80,60,90]、postorder=[20,40,30,60,90,80,50],还原后按层序,也就是一层层横着读,是 50、30、80、20、40、60、90。一份输入可能对应好几棵树,返回任意一棵都算对。
光有前序,左右子树切在哪儿
假设手里只有前序。根是开头那个数,可根后面一长串里,前一段属于左子树、后一段属于右子树,中间那条分界线前序自己并不标出来。想还原只能挨个位置去假设「左子树占 k 个」,切开往下建,建不成再退回来换个 k,一棵 n 个节点的树试下来是指数级的功夫。若给的是前序加中序,中序能把根左右两边天然隔开,可这题给的是后序,得换个地方找分界线。
后序结尾框定左子树的大小
转机在后序。前序里根紧后面那个数 preorder[a+1],就是左子树自己的根(前提是它有左子树)。后序会把一整棵左子树排在连续的一段里,左子树的根恰好落在这段的最末。拿 preorder[a+1] 去后序里查它排第几位,从段首数到它就是左子树的节点数,右边剩下的全归右子树。每次现扫后序是 O(n)、整体滑到 O(n²);先把后序每个值映射到下标存进哈希表 pos,查位置只要 O(1)。
每一段递归各自建好一棵子树
写成一个递归 dfs,它盯着前序的一个区间和后序的一个区间,这两段是同一棵子树的两种排法。区间空了返回空;只剩一个数就是叶子,直接建好返回、不再往下取。否则拿前序开头 preorder[a] 建根,用 preorder[a+1] 在后序里的位置算出左子树大小 m。左子树递归前序接下来的 m 个配后序对应段,右子树递归前序剩下的配后序去掉末尾根的一段。孩子建好挂到根下,返回这个根。
拿题面这棵 7 节点树切一遍
回到题面这棵树。前序开头 50 是根,后面的 30 是左子树的根;30 在后序里排第 3 个(前面是 20、40),左子树占 3 个节点,前序后序据此各切成左右两半递归。左段根 30,20 在后序里排第 1 个、左子树只 1 个,于是 20 当左孩子、40 当右孩子,都成叶子返回。右段同理,根 80、左孩子 60、右孩子 90。全挂回去,读出的层序 50、30、80、20、40、60、90 正是题面要的那棵树。
少加一个 1,整段就会错位
最容易栽的是把左子树大小算成后序位置减区间起点、忘了加 1——位置差要加 1 才是元素个数,少算一个左右边界就整体挪位、往下全建歪。区间只剩一个数时别急着取 preorder[a+1]:那时它本就是叶子,硬取要么下标越界,要么把叶子误当成还带着左子树。前序加后序还定不出唯一的树——某节点只有一个孩子时挂左挂右两份序列一模一样、分不出来,题目才允许返回任意一种。时间上每个节点只建一次、查位置靠哈希表 O(1),合计 O(n);空间是哈希表加递归栈 O(n)。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这句口诀:前序第一个是根,前序第二个是左根,去后序定左子树大小,再切两半递归。下面每一帧都在套它。
- 4前序定根,后序定大小开局树是空的。手里只有两个数组:前序 pre 和后序 post。前序的第一个 50 必是整棵树的根,咱们就从它开始切。
- 5pre[0..6] / post[0..6]现在处理被方括号框住的这一段,前序和后序各有 7 个数,它们正好是同一棵子树。先看前序开头。
- 6前序第一个 = 根 = 50前序的第一个 50(紫色),就是这棵子树的根,先把它建出来放好。接着要分清谁是它的左子树、谁是右子树。
- 7左根 30 → 左子树 3 个50 后面紧挨的 30 就是它左子树的根。去后序里找 30,发现它排在第 3 个,说明左子树一共 3 个节点。据此把前序、后序都切成左、右两段,左边那段递归建左子树,右边那段建右子树。
- 8pre[1..3] / post[0..2]现在处理被方括号框住的这一段,前序和后序各有 3 个数,它们正好是同一棵子树。先看前序开头。
- 9前序第一个 = 根 = 30前序的第一个 30(紫色),就是这棵子树的根,先把它建出来放好。接着要分清谁是它的左子树、谁是右子树。
- 10左根 20 → 左子树 1 个30 后面紧挨的 20 就是它左子树的根。去后序里找 20,发现它排在第 1 个,说明左子树一共 1 个节点。据此把前序、后序都切成左、右两段,左边那段递归建左子树,右边那段建右子树。
- 11pre[2..2] / post[0..0]现在处理被方括号框住的这一段,前序和后序各有 1 个数,它们正好是同一棵子树。先看前序开头。
- 12区间只剩 1 个 → 叶子这一段只剩 20 一个数,没有左右可分,它就是一片叶子。直接建好挂上去,这一支到底了,返回。
- 13pre[3..3] / post[1..1]现在处理被方括号框住的这一段,前序和后序各有 1 个数,它们正好是同一棵子树。先看前序开头。
- 14区间只剩 1 个 → 叶子这一段只剩 40 一个数,没有左右可分,它就是一片叶子。直接建好挂上去,这一支到底了,返回。
- 1530 左右接好30 的左、右子树都递归建好并挂回到它下面,这棵子树完工,返回上一层继续。
- 16pre[4..6] / post[3..5]现在处理被方括号框住的这一段,前序和后序各有 3 个数,它们正好是同一棵子树。先看前序开头。
- 17前序第一个 = 根 = 80前序的第一个 80(紫色),就是这棵子树的根,先把它建出来放好。接着要分清谁是它的左子树、谁是右子树。
- 18左根 60 → 左子树 1 个80 后面紧挨的 60 就是它左子树的根。去后序里找 60,发现它排在第 1 个,说明左子树一共 1 个节点。据此把前序、后序都切成左、右两段,左边那段递归建左子树,右边那段建右子树。
- 19pre[5..5] / post[3..3]现在处理被方括号框住的这一段,前序和后序各有 1 个数,它们正好是同一棵子树。先看前序开头。
- 20区间只剩 1 个 → 叶子这一段只剩 60 一个数,没有左右可分,它就是一片叶子。直接建好挂上去,这一支到底了,返回。
- 21pre[6..6] / post[4..4]现在处理被方括号框住的这一段,前序和后序各有 1 个数,它们正好是同一棵子树。先看前序开头。
- 22区间只剩 1 个 → 叶子这一段只剩 90 一个数,没有左右可分,它就是一片叶子。直接建好挂上去,这一支到底了,返回。
- 2380 左右接好80 的左、右子树都递归建好并挂回到它下面,这棵子树完工,返回上一层继续。
- 24层序 = 50,30,80,20,40,60,9050 的左右子树都递归建好并接上了,整棵树就此还原完成,层序读出来正是 50、30、80、20、40、60、90。一次自顶向下的递归,每段都靠「前序定根、后序定大小」切两半。
- 25层序 [50,30,80,20,40,60,90]回头看整个过程:每进入一段,前序第一个当根,前序第二个去后序定出左子树大小,切两半递归。最终还原出的树层序是 50、30、80、20、40、60、90,和目标完全一致。
⚠️ 容易写错的地方
✗ 错:忘了 a 等于 b 的单节点情况就去取 preorder[a+1]
✓ 对:区间只剩一个先当叶子返回
否则 a+1 越界、或把叶子误当成有左子树
✗ 错:左子树大小算成 i 减 c(少加 1)
✓ 对:m = i 减 c 再加 1
位置差要加 1 才是元素个数,算少一个会整段错位
✗ 错:以为前序加后序能唯一确定一棵树
✓ 对:只有一个孩子时无法区分左右,本题允许返回任意一种
约定把 preorder[a+1] 当左孩子即可
完整代码(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 constructFromPrePost(
self, preorder: List[int], postorder: List[int]
) -> Optional[TreeNode]:
def dfs(a: int, b: int, c: int, d: int) -> Optional[TreeNode]:
if a > b:
return None
root = TreeNode(preorder[a])
if a == b:
return root
i = pos[preorder[a + 1]]
m = i - c + 1
root.left = dfs(a + 1, a + m, c, i)
root.right = dfs(a + m + 1, b, i + 1, d - 1)
return root
pos = {x: i for i, x in enumerate(postorder)}
return dfs(0, len(preorder) - 1, 0, len(postorder) - 1)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* constructFromPrePost(vector<int>& preorder, vector<int>& postorder) {
unordered_map<int, int> pos;
int n = postorder.size();
for (int i = 0; i < n; ++i) {
pos[postorder[i]] = i;
}
function<TreeNode*(int, int, int, int)> dfs = [&](int a, int b, int c, int d) -> TreeNode* {
if (a > b) {
return nullptr;
}
TreeNode* root = new TreeNode(preorder[a]);
if (a == b) {
return root;
}
int i = pos[preorder[a + 1]];
int m = i - c + 1;
root->left = dfs(a + 1, a + m, c, i);
root->right = dfs(a + m + 1, b, i + 1, d - 1);
return root;
};
return dfs(0, n - 1, 0, n - 1);
}
};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 {
private Map<Integer, Integer> pos = new HashMap<>();
private int[] preorder;
public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
this.preorder = preorder;
for (int i = 0; i < postorder.length; ++i) {
pos.put(postorder[i], i);
}
return dfs(0, preorder.length - 1, 0, postorder.length - 1);
}
private TreeNode dfs(int a, int b, int c, int d) {
if (a > b) {
return null;
}
TreeNode root = new TreeNode(preorder[a]);
if (a == b) {
return root;
}
int i = pos.get(preorder[a + 1]);
int m = i - c + 1;
root.left = dfs(a + 1, a + m, c, i);
root.right = dfs(a + m + 1, b, i + 1, d - 1);
return root;
}
}复杂度
时间
O(n)
先用哈希表存后序每个值的位置;之后每个节点只被建一次、每次查位置 O(1),合计线性
空间
O(n)
哈希表 O(n) + 递归栈深 O(h),最坏(退化成链)递归深 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 根据前序和后序遍历构造二叉树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么前序加后序定不出唯一的树,前序加中序却可以?+
中序能把根的左右两边干净地分开——根左边全是左子树、右边全是右子树,所以前序配中序能唯一还原。前序配后序在「某个节点只有一个孩子」时会丢信息:这个孩子是左是右,两份序列长得一模一样,没法区分。所以本题只要求返回任意一种合法的树。
为什么非要用哈希表存后序里每个值的位置?+
每切一段都得知道左子树的根在后序里的下标。要是每次现扫一遍后序,一次是 O(n),整棵树切下来会滑到 O(n²)。开工前先花一遍把后序每个值映射到下标存好,之后查位置只要 O(1),整体就压回 O(n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 根据前序和后序遍历构造二叉树 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。