验证二叉树的前序序列化

题目链接:leetcode 331

题目描述

序列化二叉树的一种方法是使用前序遍历。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #

1
2
3
4
5
6
7
     _9_
/ \
3 2
/ \ / \
4 1 # 6
/ \ / \ / \
# # # # # #

例如,上面的二叉树可以被序列化为字符串 "9,3,4,#,#,1,#,#,2,#,6,#,#",其中 # 代表一个空节点。

给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。

每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 ‘#’ 。

你可以认为输入格式总是有效的,例如它永远不会包含两个连续的逗号,比如 “1,,3” 。

示例 1:

1
2
输入: "9,3,4,#,#,1,#,#,2,#,6,#,#"
输出: true

示例 2:

1
2
输入: "1,#"
输出: false

示例 3:

1
2
输入: "9,#,#,1"
输出: false

题解

法一:栈

我们知道「前序遍历」是按照「根节点-左子树-右子树」的顺序遍历的,只有当根节点的所有左子树遍历完成之后,才会遍历右子树。

对于本题的输入,我们可以先判断「左子树」是否有效的,然后再判断「右子树」是否有效的,最后判断「根节点-左子树-右子树」是否为有效的。这个思路类似于递归,而把递归改写成循环时,就会使用「栈」,这就是本题使用「栈」的原因。

那么,如何判断一棵子树是有效的?当且仅当它的左右两棵子树都是有效的,这样递归下去就变成了判断叶子节点是不是有效的:

  • 若叶子节点为 # ,那么它不能有左右子树
  • 若叶子节点不为空,那么它的左右两棵子树为空(只有一个孩子为空,另一个孩子不为空时需要对不为空的孩子继续遍历)

为了兼容这两个情况,我们可以采用一个技巧,把有效叶子节点使用 # 代替,即:把 4## 替换成 # ,此时叶子节点将变成空节点,这样自底向上逐层化简,如果最后能够把所有的节点都化简为空节点,那么此序列是二叉树的前序序列化。

具体操作流程示例如下:

如示例 1 中的输入: 9,3,4,#,#,1,#,#,2,#,6,#,#,当遇到 x,#,# 的时候,就把它变为 #

模拟一遍过程:

  1. [9,3,4,#,#] => [9,3,#],继续
  2. [9,3,#,1,#,#] => [9,3,#,#] => [9,#] ,继续
  3. [9,#2,#,6,#,#] => [9,#,2,#,#] => [9,#,#] => [#],结束

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public boolean isValidSerialization(String preorder) {
LinkedList<String> stack = new LinkedList<>();
for (String s : preorder.split(",")) {
stack.push(s);
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("#");
}
}

法二:栈

此方法和法一本质上是相同的,不过采用的是不同的思考方式。

我们思考一下按照前序遍历的序列,从树根来建立一棵二叉树的过程。首先我们放置树根,这时候多出了树根的左右两个孩子的空位可以继续来放节点,接下来我们读取到一个节点,放到左子树,这时我们消耗了一个空位,同时又创造出了两个空位。。。

所以我们可以定义一个概念,叫做槽位(与上面的“空位”含义相同)。一个槽位可以被看作「当前二叉树中正在等待被节点填充」的那些位置。

二叉树的建立也伴随着槽位数量的变化。每当遇到一个节点时:

  • 如果遇到了空节点,则要消耗一个槽位;
  • 如果遇到了非空节点,则除了消耗一个槽位外,还要再补充两个槽位。

此外,还需要将根节点作为特殊情况处理。

我们使用栈来维护槽位的变化。栈中的每个元素,代表了对应节点处剩余槽位的数量,而栈顶元素就对应着下一步可用的槽位数量。当遇到空节点时,仅将栈顶元素减 1;当遇到非空节点时,将栈顶元素减 1 后,再向栈中压入一个 2。无论何时,如果栈顶元素变为 0,就立刻将栈顶弹出。

遍历结束后,若栈为空,说明没有待填充的槽位,因此是一个合法序列;否则若栈不为空,则序列不合法。此外,在遍历的过程中,若槽位数量不足,则序列不合法。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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)$。此为栈所需要使用的空间。

简化

如果你看懂了上面的思路,你应该能看出来,这个栈其实是不需要的,我们只需要维护一个计数器代表栈中元素之和即可,这样空间复杂度可降为 $O(1)$。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public boolean isValidSerialization1(String preorder) {
int slots = 1;
for (String s : preorder.split(",")) {
if (slots == 0) {
return false;
}
if (s.equals("#")) {
slots--;
} else {
slots++; //减去 1,加上 2,总体是 +1
}
}
return slots == 0;
}
}

怎么样,是不是简单许多?

法三:计算入度和出度

我们知道,在树中,所有节点的入度之和等于出度之和。可以根据这个特点判断输入序列是否是有效的。

在一棵二叉树中:

  • 每个空节点( "#" )会提供 0 个出度和 1 个入度。
  • 每个非空节点会提供 2 个出度和 1 个入度(根节点的入度是 0)。

我们只要把字符串遍历一次,每个节点都累加 diff = 出度 - 入度 。在遍历到任何一个节点的时候,要求 diff >= 0,原因是还没遍历到该节点的子节点,所以此时的出度应该大于等于入度。当所有节点遍历完成之后,整棵树的 diff == 0 。

这里解释一下为什么下面的代码中 diff 的初始化为 1。因为,我们加入一个非空节点时,都会对 diff 先减去 1(入度),再加上 2(出度)。但是由于根节点没有父节点,所以其入度为 0,出度为 2。因此 diff 初始化为 1,是为了在加入根节点的时候,diff 先减去 1(入度),再加上 2(出度),此时 diff 正好应该是2.

本质上和上面法二的简化版本是差不多的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public boolean isValidSerialization(String preorder) {
int diff = 1;
for (String s : preorder.split(",")) {
diff--;
if (diff < 0)
return false;
if (!s.equals("#")) {
diff += 2;
}
}
return diff == 0;
}
}

复杂度分析

时间复杂度:$O(n)$,其中 $n$ 为字符串的长度。我们每个字符只遍历一次,同时每个字符对应的操作都是常数时间的。

空间复杂度:$O(1)$。

参考

LeetCode-Solution

fuxuemingzhu

坚持原创技术分享,感谢您的支持和鼓励!