验证二叉树的前序序列化 图解题解
这道题到底在问什么
- 输入
- "9,3,4,#,#,1,#,#,2,#,6,#,#"
- 输出
- true
- 输入
- "1,#"
- 输出
- false
- 输入
- "9,#,#,1"
- 输出
- false
先想最直接的笨办法
先把序列按逗号切成 13 个记号,挨个读。我们不重建树,只维护一个栈:每读一个记号压栈,栈顶一旦凑出「真值, #, #」就把这三个压成一个 #。读完后栈里恰好只剩一个 #,就合法。(动画第 4 步)
最优解:一步一步想明白
- 3记住这一句:真节点配齐两个 # 就坍缩成一个 #。下面每一帧都在套它。
- 4先把序列按逗号切成 13 个记号,挨个读。我们不重建树,只维护一个栈:每读一个记号压栈,栈顶一旦凑出「真值, #, #」就把这三个压成一个 #。读完后栈里恰好只剩一个 #,就合法。
- 5读入第 0 个 “9”,它是真实节点,直接压入栈顶。栈顶还没凑出「真值, #, #」的形状,不坍缩,继续往后读。
- 6读入第 1 个 “3”,真实节点压入栈顶。栈顶暂时还不构成可坍缩的形状,接着读下一个。
- 7读入第 2 个 “4”,真实节点压入栈顶。栈顶暂时还不构成可坍缩的形状,接着读下一个。
- 8读入第 3 个 “#”(空节点)压栈。栈顶只有一个 #,还差一个,先不坍缩,继续读。
- 9读入第 4 个 “#”(空节点)压栈,这下栈顶正好是「真值, #, #」,可以坍缩了。
- 10“4” 的左右孩子都到齐了,以它为根的小子树整体退化成一个空位,把栈顶三个 4, #, # 压成一个 #。坍缩后栈顶不再是可坍缩形状,暂停,接着读下一个记号。
- 11读入第 5 个 “1”,真实节点压入栈顶。栈顶暂时还不构成可坍缩的形状,接着读下一个。
- 12读入第 6 个 “#”(空节点)压栈。栈顶只有一个 #,还差一个,先不坍缩,继续读。
- 13读入第 7 个 “#”(空节点)压栈,这下栈顶正好是「真值, #, #」,可以坍缩了。
- 14“1” 的左右孩子都到齐了,以它为根的小子树整体退化成一个空位,把栈顶三个 1, #, # 压成一个 #。坍缩后栈顶又凑出「真值, #, #」,继续往下坍。
- 15“3” 的左右孩子都到齐了,以它为根的小子树整体退化成一个空位,把栈顶三个 3, #, # 压成一个 #。坍缩后栈顶不再是可坍缩形状,暂停,接着读下一个记号。
- 16读入第 8 个 “2”,真实节点压入栈顶。栈顶暂时还不构成可坍缩的形状,接着读下一个。
- 17读入第 9 个 “#”(空节点)压栈。栈顶只有一个 #,还差一个,先不坍缩,继续读。
- 18读入第 10 个 “6”,真实节点压入栈顶。栈顶暂时还不构成可坍缩的形状,接着读下一个。
- 19读入第 11 个 “#”(空节点)压栈。栈顶只有一个 #,还差一个,先不坍缩,继续读。
- 20读入第 12 个 “#”(空节点)压栈,这下栈顶正好是「真值, #, #」,可以坍缩了。
- 21“6” 的左右孩子都到齐了,以它为根的小子树整体退化成一个空位,把栈顶三个 6, #, # 压成一个 #。坍缩后栈顶又凑出「真值, #, #」,继续往下坍。
- 22“2” 的左右孩子都到齐了,以它为根的小子树整体退化成一个空位,把栈顶三个 2, #, # 压成一个 #。坍缩后栈顶又凑出「真值, #, #」,继续往下坍。
- 23“9” 的左右孩子都到齐了,以它为根的小子树整体退化成一个空位,把栈顶三个 9, #, # 压成一个 #。坍缩后栈只剩一个 #,进入终局判定。
- 2413 个记号全部读完,栈里恰好只剩一个 “#”。这说明整串正好拼成一棵完整二叉树的前序序列化,合法,返回 true,正是示例 1 的答案。
⚠️ 容易写错的地方
✗ 错:只数 # 的个数、不管顺序
✓ 对:必须用栈判断「真值, #, #」的局部形状
顺序非法的串(如 #,1)即便 # 数量看着对也不合法
✗ 错:树已完成后多余记号没判出
✓ 对:坍缩到底若栈提前只剩一个 #,后面还来记号就非法
9,#,#,1 里树已完成又冒出 1
✗ 错:坍缩条件漏判第三个不是 #
✓ 对:必须确认被消的父节点 stk[-3] 是真值(不是 #)才坍缩
只看栈顶两个 # 就坍缩会错折叠 #,#,# 这种空孩子,真节点配齐两个 # 子树才算完整
完整代码(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 Solution:
def isValidSerialization(self, preorder: str) -> bool:
stk = []
for c in preorder.split(","):
stk.append(c)
while len(stk) > 2 and stk[-1] == stk[-2] == "#" and stk[-3] != "#":
stk = stk[:-3]
stk.append("#")
return len(stk) == 1 and stk[0] == "#"C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#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;
class Solution {
public:
bool isValidSerialization(string preorder) {
vector<string> stk;
stringstream ss(preorder);
string s;
while (getline(ss, s, ',')) {
stk.push_back(s);
while (stk.size() >= 3 && stk[stk.size() - 1] == "#" && stk[stk.size() - 2] == "#" && stk[stk.size() - 3] != "#") {
stk.pop_back();
stk.pop_back();
stk.pop_back();
stk.push_back("#");
}
}
return stk.size() == 1 && stk[0] == "#";
}
};Java
import java.util.*;
class Solution {
public boolean isValidSerialization(String preorder) {
List<String> stk = new ArrayList<>();
for (String s : preorder.split(",")) {
stk.add(s);
while (stk.size() >= 3 && stk.get(stk.size() - 1).equals("#")
&& stk.get(stk.size() - 2).equals("#") && !stk.get(stk.size() - 3).equals("#")) {
stk.remove(stk.size() - 1);
stk.remove(stk.size() - 1);
stk.remove(stk.size() - 1);
stk.add("#");
}
}
return stk.size() == 1 && stk.get(0).equals("#");
}
}复杂度
时间·整体思路
O(n)
每个记号压栈一次,坍缩总次数也不超过记号数,本身是线性的
时间·分语言
C++/Java O(n)
C++/Java 原地弹栈是 O(1),整体 O(n);Python 这版用切片 stk[-3:] 重建前缀,最坏退化到 n 的平方,想保证 O(n) 可改成 del stk[-3:] 或连续 pop 三次再 append
空间
O(n)
最坏情况栈里同时存近 n 个记号
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 验证二叉树的前序序列化 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
除了栈,还有更省空间的做法吗?+
有,用「槽位(出入度)」法:把每个位置看成一个待填的槽,根先有 1 个槽;遇到真节点占 1 个槽同时新增 2 个槽,遇到 # 占 1 个槽不新增。过程中槽数不能提前归零,结束时槽数恰好为 0 即合法。它只用一个计数器,空间 O(1),和栈法等价。
为什么不用真的重建树?+
题目明确不允许重建,且重建要额外空间和递归。栈式消除只关心「真节点配齐两个孩子就退化成空」这一局部规则,反复把已完成的子树折叠成 #,不必知道整棵树的样子就能判断合法性。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 验证二叉树的前序序列化 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。