力扣-树-验证二叉树的前序序列化
验证二叉树的前序序列化(LeetCode 331)
- 题目概述:序列化二叉树的一种方法是使用前序遍历。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #。
例如,上面的二叉树可以被序列化为字符串 “9,3,4,#,#,1,#,#,2,#,6,#,#”,其中 # 代表一个空节点。给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 ‘#’ 。你可以认为输入格式总是有效的,例如它永远不会包含两个连续的逗号,比如 “1,3” 。 - 题目案例:
- 解题思路:(这道题我刚开始看的时候其实是一点思路都没有的,想把输入序列变成树可是这样既不好做我也没想能不能做出来心里告诉自己肯定不是正确思路,于是乎去看了大佬的思路)
以下是官方+大佬的四种方法,在这里做一个汇总
1.主要围绕一个概念叫做槽位,一个槽位可以被看作「当前二叉树中正在等待被节点填充」的那些位置。
二叉树的建立也伴随着槽位数量的变化。每当遇到一个节点时:
(1)如果遇到了空节点,则要消耗一个槽位;
(2)如果遇到了非空节点,则除了消耗一个槽位外,还要再补充两个槽位。
此外,还需要将根节点作为特殊情况处理。
我们使用栈来维护槽位的变化。栈中的每个元素,代表了对应节点处剩余槽位的数量,而栈顶元素就对应着下一步可用的槽位数量。当遇到空节点时,仅将栈顶元素减 1;当遇到非空节点时,将栈顶元素减 1后,再向栈中压入一个 2。无论何时,如果栈顶元素变为 0,就立刻将栈顶弹出。
遍历结束后,若栈为空,说明没有待填充的槽位,因此是一个合法序列;否则若栈不为空,则序列不合法。此外,在遍历的过程中,若槽位数量不足,则序列不合法。
class Solution {
public boolean isValidSerialization(String preorder) {
int n = preorder.length();
int i = 0;
Deque<Integer> stack = new LinkedList<Integer>();
stack.push(1);
while (i < n) {
if (stack.isEmpty()) {
return false;
}
if (preorder.charAt(i) == ',') {
i++;
} else if (preorder.charAt(i) == '#'){
int top = stack.pop() - 1;
if (top > 0) {
stack.push(top);
}
i++;
} else {
// 读一个数字
//这个地方的意义在于二位数以上的数字,因为字符串不会考虑数字位数
while (i < n && preorder.charAt(i) != ',') {
i++;
}
int top = stack.pop() - 1;
if (top > 0) {
stack.push(top);
}
stack.push(2);
}
}
return stack.isEmpty();
}
}
时间复杂度:O(n),其中 n 为字符串的长度。我们每个字符只遍历一次,同时每个字符对应的操作都是常数时间的。
空间复杂度:O(n)。此为栈所需要使用的空间。
2.是对法1的升级,不使用栈,而是用计数器代替
class Solution1 {
public boolean isValidSerialization(String preorder) {
int n = preorder.length();
int i = 0;
int slots = 1;
while (i < n) {
if (slots == 0) {
return false;
}
if (preorder.charAt(i) == ',') {
i++;
} else if (preorder.charAt(i) == '#'){
slots--;
i++;
} else {
// 读一个数字
while (i < n && preorder.charAt(i) != ',') {
i++;
}
slots++; // slots = slots - 1 + 2
}
}
return slots == 0;
}
}
时间复杂度:O(n),其中 n为字符串的长度。我们每个字符只遍历一次,同时每个字符对应的操作都是常数时间的。
空间复杂度:O(1)。
3.我们知道「前序遍历」是按照「根节点-左子树-右子树」的顺序遍历的,只有当根节点的所有左子树遍历完成之后,才会遍历右子树。对于本题的输入,我们可以先判断「左子树」是否有效的,然后再判断「右子树」是否有效的,最后判断「根节点-左子树-右子树」是否为有效的。这个思路类似于递归,而把递归改写成循环时,就会使用「栈」,这就是本题使用「栈」的原因。
当一个节点不是叶子节点的时候,那么它必定至少有一个孩子非空!有两种情况:
两个孩子都非"#"(空);
一个孩子为"#"(空),另一个孩子非"#"(空);
下面的重点是如何判断一棵子树是否有效?首先考虑最简单情况:怎么判断一个节点是叶子节点?很明显,当一个节点的两个孩子都是 “#”(空)的时候,该节点就是叶子节点。
把有效的叶子节点使用 “#” 代替。 比如把 4## 替换成 # 。此时,叶子节点会变成空节点!
class Solution2 {
public boolean isValidSerialization(String preorder) {
LinkedList<String> stack=new LinkedList<>();
String[] arr=preorder.split(",");
for(String s:arr){
stack.push(s);
//这个地方要用while,不能用if,因为有可能出现更新完一次又恰好是##,比如案例:"9,3,4,#,#,1,#,#,2,#,6,#,#"
//一定要用equals,不能用"=="
while(stack.size()>=3&&stack.get(0).equals("#")&&stack.get(1).equals("#")&&!stack.get(2).equals("#")){
stack.pop();
stack.pop();
stack.pop();
stack.push("#");
}
}
return stack.size()==1&&stack.pop().equals("#");
}
}
时间复杂度:O(N)
空间复杂度:O(N)
4.出度与入度:所有节点的入度之和等于出度之和
在一棵二叉树中:
每个空节点( “#” )会提供 0 个出度和 1 个入度。
每个非空节点会提供 2 个出度和 1 个入度(根节点的入度是 0)
class Solution3 {
public boolean isValidSerialization(String preorder) {
int diff=1;
for(String s:preorder.split(",")){
diff-=1;
if(diff<0) return false;
if(!s.equals("#")) diff+=2;
}
return diff==0;
}
}
时间复杂度:O(N)
空间复杂度:O(1)
(作者:fuxuemingzhu)
- 总结:
这四个解法个人觉得最好想也最简单的就是第四种,如果没有太大欲望想看完四种解法,可以先从最后一个入手。
本题还有一个解法不一样的地方就是关于字符串中数字的提取,可以参考四种方法中,前两种用的是一种方法,有两种是另一种,个人就本题来说更喜欢后两种,charAt返回的不是String类型,需要注意一下,还有一些小细节已经注释在代码中了