题目链接:leetcode 331
题目描述
序列化二叉树的一种方法是使用前序遍历。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如
#
。
1 | _9_ |
例如,上面的二叉树可以被序列化为字符串 "9,3,4,#,#,1,#,#,2,#,6,#,#"
,其中 #
代表一个空节点。
给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。
每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 ‘#’ 。
你可以认为输入格式总是有效的,例如它永远不会包含两个连续的逗号,比如 “1,,3” 。
示例 1:
1 | 输入: "9,3,4,#,#,1,#,#,2,#,6,#,#" |
示例 2:
1 | 输入: "1,#" |
示例 3:
1 | 输入: "9,#,#,1" |
题解
法一:栈
我们知道「前序遍历」是按照「根节点-左子树-右子树」的顺序遍历的,只有当根节点的所有左子树遍历完成之后,才会遍历右子树。
对于本题的输入,我们可以先判断「左子树」是否有效的,然后再判断「右子树」是否有效的,最后判断「根节点-左子树-右子树」是否为有效的。这个思路类似于递归,而把递归改写成循环时,就会使用「栈」,这就是本题使用「栈」的原因。
那么,如何判断一棵子树是有效的?当且仅当它的左右两棵子树都是有效的,这样递归下去就变成了判断叶子节点是不是有效的:
- 若叶子节点为
#
,那么它不能有左右子树 - 若叶子节点不为空,那么它的左右两棵子树为空(只有一个孩子为空,另一个孩子不为空时需要对不为空的孩子继续遍历)
为了兼容这两个情况,我们可以采用一个技巧,把有效叶子节点使用 #
代替,即:把 4##
替换成 #
,此时叶子节点将变成空节点,这样自底向上逐层化简,如果最后能够把所有的节点都化简为空节点,那么此序列是二叉树的前序序列化。
具体操作流程示例如下:
如示例 1 中的输入: 9,3,4,#,#,1,#,#,2,#,6,#,#
,当遇到 x,#,#
的时候,就把它变为 #
。
模拟一遍过程:
[9,3,4,#,#]
=>[9,3,#]
,继续[9,3,#,1,#,#]
=>[9,3,#,#] => [9,#]
,继续[9,#2,#,6,#,#]
=>[9,#,2,#,#]
=>[9,#,#]
=>[#]
,结束
代码如下:
1 | class Solution { |
法二:栈
此方法和法一本质上是相同的,不过采用的是不同的思考方式。
我们思考一下按照前序遍历的序列,从树根来建立一棵二叉树的过程。首先我们放置树根,这时候多出了树根的左右两个孩子的空位可以继续来放节点,接下来我们读取到一个节点,放到左子树,这时我们消耗了一个空位,同时又创造出了两个空位。。。
所以我们可以定义一个概念,叫做槽位(与上面的“空位”含义相同)。一个槽位可以被看作「当前二叉树中正在等待被节点填充」的那些位置。
二叉树的建立也伴随着槽位数量的变化。每当遇到一个节点时:
- 如果遇到了空节点,则要消耗一个槽位;
- 如果遇到了非空节点,则除了消耗一个槽位外,还要再补充两个槽位。
此外,还需要将根节点作为特殊情况处理。
我们使用栈来维护槽位的变化。栈中的每个元素,代表了对应节点处剩余槽位的数量,而栈顶元素就对应着下一步可用的槽位数量。当遇到空节点时,仅将栈顶元素减 1;当遇到非空节点时,将栈顶元素减 1 后,再向栈中压入一个 2。无论何时,如果栈顶元素变为 0,就立刻将栈顶弹出。
遍历结束后,若栈为空,说明没有待填充的槽位,因此是一个合法序列;否则若栈不为空,则序列不合法。此外,在遍历的过程中,若槽位数量不足,则序列不合法。
代码:
1 | class Solution { |
复杂度分析
时间复杂度:$O(n)$,其中 $n$ 为字符串的长度。我们每个字符只遍历一次,同时每个字符对应的操作都是常数时间的。
空间复杂度:$O(n)$。此为栈所需要使用的空间。
简化
如果你看懂了上面的思路,你应该能看出来,这个栈其实是不需要的,我们只需要维护一个计数器代表栈中元素之和即可,这样空间复杂度可降为 $O(1)$。
代码如下:
1 | class Solution { |
怎么样,是不是简单许多?
法三:计算入度和出度
我们知道,在树中,所有节点的入度之和等于出度之和。可以根据这个特点判断输入序列是否是有效的。
在一棵二叉树中:
- 每个空节点(
"#"
)会提供 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 | class Solution { |
复杂度分析
时间复杂度:$O(n)$,其中 $n$ 为字符串的长度。我们每个字符只遍历一次,同时每个字符对应的操作都是常数时间的。
空间复杂度:$O(1)$。