序列化和反序列化二叉搜索树 图解题解
这道题到底在问什么
- 输入
- BST 根 50,左子树 30(20,40)、右子树 80(70,90)
- 输出
- 前序串 50,30,20,40,80,70,90
- 输入
- 空树
- 输出
- 空串
最优解:一步一步想明白
- 3记住「序列化 = 前序、不存 null;反序列化 = 前序 + (lo, hi) 区间唯一定位」,下面分两段逐帧套它。
- 4前序遍历先做序列化。规则是前序遍历:每到一个节点,先把它的值写进序列,再去左子树、最后去右子树。注意整个过程不写任何 null,这是 BST 比普通二叉树省空间的地方。
- 5已写 1 个前序走到节点 50(紫色),把它的值写进序列,序列现在是 50。先记根,再去左子树。
- 6已写 2 个前序走到节点 30(紫色),把它的值写进序列,序列现在是 50,30。接着按根、左、右的顺序继续。
- 7已写 3 个前序走到节点 20(紫色),把它的值写进序列,序列现在是 50,30,20。接着按根、左、右的顺序继续。
- 8已写 4 个前序走到节点 40(紫色),把它的值写进序列,序列现在是 50,30,20,40。接着按根、左、右的顺序继续。
- 9已写 5 个前序走到节点 80(紫色),把它的值写进序列,序列现在是 50,30,20,40,80。接着按根、左、右的顺序继续。
- 10已写 6 个前序走到节点 70(紫色),把它的值写进序列,序列现在是 50,30,20,40,80,70。接着按根、左、右的顺序继续。
- 11已写 7 个前序走到节点 90(紫色),把它的值写进序列,序列现在是 50,30,20,40,80,70,90。这是最后一个节点。
- 12得到前序串序列化完成,得到前序串 50,30,20,40,80,70,90,一共 7 个值、没有一个 null。接下来反过来,只凭这串数把整棵 BST 还原出来。
- 13读前序串开始反序列化。手里只有前序串 50,30,20,40,80,70,90。我们从左往右取值,每个位置都带一个允许区间 (lo, hi):根节点的区间是 (-∞, +∞),最宽松。
- 14读到第 1 个根:取到值 50,它满足 -∞ < 50 < +∞,于是在这里建节点 50。接着它的左子树区间收紧为 (-∞, 50)、右子树区间为 (50, +∞),继续往下递归。
- 15读到第 2 个节点 50 的左孩子:取到值 30,它满足 -∞ < 30 < 50,于是在这里建节点 30。接着它的左子树区间收紧为 (-∞, 30)、右子树区间为 (30, 50),继续往下递归。
- 16读到第 3 个节点 30 的左孩子:取到值 20,它满足 -∞ < 20 < 30,于是在这里建节点 20。接着它的左子树区间收紧为 (-∞, 20)、右子树区间为 (20, 30),继续往下递归。
- 17区间不符轮到节点 20 的左孩子,它的允许区间是 (-∞, 20)。但下一个待读的值 40 不落在区间里,说明这里没有节点,直接判为空,既不取值也不前进,回到上一层。
- 18区间不符轮到节点 20 的右孩子,它的允许区间是 (20, 30)。但下一个待读的值 40 不落在区间里,说明这里没有节点,直接判为空,既不取值也不前进,回到上一层。
- 19读到第 4 个节点 30 的右孩子:取到值 40,它满足 30 < 40 < 50,于是在这里建节点 40。接着它的左子树区间收紧为 (30, 40)、右子树区间为 (40, 50),继续往下递归。
- 20读到第 5 个节点 50 的右孩子:取到值 80,它满足 50 < 80 < +∞,于是在这里建节点 80。接着它的左子树区间收紧为 (50, 80)、右子树区间为 (80, +∞),继续往下递归。
- 21读到第 6 个节点 80 的左孩子:取到值 70,它满足 50 < 70 < 80,于是在这里建节点 70。接着它的左子树区间收紧为 (50, 70)、右子树区间为 (70, 80),继续往下递归。
- 22读到第 7 个节点 80 的右孩子:取到值 90,它满足 80 < 90 < +∞,于是在这里建节点 90。接着它的左子树区间收紧为 (80, 90)、右子树区间为 (90, +∞),继续往下递归。
- 23原样还原整串读完,还原出的 BST 和原来完全一样。验证一下它的中序是 20,30,40,50,70,80,90,严格升序,确实是合法 BST。这正是 BST 的妙处:前序串配上 (lo,hi) 区间,每个值的位置都被唯一确定。
⚠️ 容易写错的地方
✗ 错:像普通二叉树那样把 null 也存进序列
✓ 对:只存节点值、完全不存 null
BST 的有序性已经能用区间定位每个值,存 null 是浪费,本题要的就是这份紧凑
✗ 错:反序列化区间写成闭区间 ≤/≥
✓ 对:用开区间 lo < v < hi
BST 节点值互不相等,等号会把边界值错放进相邻子树
✗ 错:忘了取值后让读取指针前进
✓ 对:建一个节点就把下标 i 加一
指针不前进会反复读同一个值,树永远建不完或建错
完整代码(Python / C++ / Java)
Python
from typing import Optional
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
class Codec:
def serialize(self, root: Optional[TreeNode]) -> str:
vals = []
def dfs(node):
if node:
vals.append(str(node.val))
dfs(node.left)
dfs(node.right)
dfs(root)
return ','.join(vals)
def deserialize(self, data: str) -> Optional[TreeNode]:
if not data:
return None
vals = list(map(int, data.split(',')))
i = 0
def build(lo, hi):
nonlocal i
if i == len(vals) or not (lo < vals[i] < hi):
return None
node = TreeNode(vals[i])
i += 1
node.left = build(lo, node.val)
node.right = build(node.val, hi)
return node
return build(float('-inf'), float('inf'))C++
#include <sstream>
#include <string>
#include <vector>
using namespace std;
struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x): val(x), left(nullptr), right(nullptr) {} };
class Codec {
public:
string serialize(TreeNode* root) {
vector<string> vals;
function<void(TreeNode*)> dfs = [&](TreeNode* n){ if(!n) return; vals.push_back(to_string(n->val)); dfs(n->left); dfs(n->right); };
dfs(root);
string out;
for (int i = 0; i < (int)vals.size(); ++i) { if (i) out += ","; out += vals[i]; }
return out;
}
TreeNode* deserialize(string data) {
if (data.empty()) return nullptr;
vector<int> vals; string token; stringstream ss(data);
while (getline(ss, token, ',')) vals.push_back(stoi(token));
int i = 0;
function<TreeNode*(long long,long long)> build = [&](long long lo, long long hi) {
if (i == (int)vals.size() || vals[i] <= lo || vals[i] >= hi) return (TreeNode*)nullptr;
TreeNode* n = new TreeNode(vals[i++]);
n->left = build(lo, n->val);
n->right = build(n->val, hi);
return n;
};
return build(LLONG_MIN, LLONG_MAX);
}
};Java
import java.util.*;
class TreeNode {
int val;
TreeNode left, right;
TreeNode(int x) { val = x; }
}
class Codec {
public String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
preorder(root, sb);
return sb.toString();
}
private void preorder(TreeNode node, StringBuilder sb) {
if (node == null) return;
if (sb.length() > 0) sb.append(',');
sb.append(node.val);
preorder(node.left, sb);
preorder(node.right, sb);
}
public TreeNode deserialize(String data) {
if (data.isEmpty()) return null;
String[] parts = data.split(",");
int[] vals = new int[parts.length];
for (int i = 0; i < parts.length; i++) vals[i] = Integer.parseInt(parts[i]);
int[] idx = {0};
return build(vals, idx, Long.MIN_VALUE, Long.MAX_VALUE);
}
private TreeNode build(int[] vals, int[] idx, long lo, long hi) {
if (idx[0] == vals.length || vals[idx[0]] <= lo || vals[idx[0]] >= hi) return null;
TreeNode node = new TreeNode(vals[idx[0]++]);
node.left = build(vals, idx, lo, node.val);
node.right = build(vals, idx, node.val, hi);
return node;
}
}复杂度
时间
O(n)
序列化每个节点访问一次;反序列化每个值取用一次、每次区间判断是常数,共 O(n)
空间
O(n)
输出序列本身 O(n);递归栈深度等于树高 h,最坏退化成链时 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 序列化和反序列化二叉搜索树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
和普通二叉树的序列化(lc297)有什么区别?+
lc297 是普通二叉树,前序序列必须用特殊符号存 null 占位,否则无法重建结构。lc449 是 BST,利用值的有序性,反序列化时靠 (lo,hi) 区间就能唯一定位每个值,完全不用存 null,序列更短。本质区别就在于 BST 的有序性提供了额外信息。
能不能用层序而不是前序来序列化 BST 并省 null?+
不方便。区间还原天然契合前序(根先出,正好先确定当前区间再分裂给左右);层序无法在取值时直接知道每个值的父子区间关系,还原要麻烦得多。所以这类「省 null」的 BST 编码标准做法是前序 + 区间。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 序列化和反序列化二叉搜索树 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。