这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」
331. 验证二叉树的前序序列化
今天我们还来刷二叉树的题,验证二叉树的前序序列化,这道题如果没有思路还是比较困难做出来的。
LeetCode传送门331. 验证二叉树的前序序列化
在做这道题之前我们先明确一些概念
二叉树中的相关术语
- 结点:包含一个数据元素及若干指向子树分支的信息
- 结点的度:一个结点拥有子树的数目称为结点的度。
- 入度:除根节点外,所有结点的入度都为1. 我们一般说的度,指的是出度。
- 叶子结点:也称为终端结点,没有子树的节点或者度为0的结点
- 分支结点:也称为非终端结点,度不为0的结点。
- 树的度:树种所有结点的度的最大值。
二叉树的基本性质
度为0的节点比度为2的节点多一个(叶子结点数 = 度数为2的节点数 + 1)
上面的基本性质对我们很重要,请牢记。
下面我们来看看题目
题目
序列化二叉树的一种方法是使用前序遍历。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #。
One way to serialize a binary tree is to use preorder traversal. When we encounter a non-null node, we record the node's value. If it is a null node, we record using a sentinel value such as '#'.
_9_
/ \
3 2
/ \ / \
4 1 # 6
/ \ / \ / \
# # # # # #
例如,上面的二叉树可以被序列化为字符串 "9,3,4,#,#,1,#,#,2,#,6,#,#",其中 # 代表一个空节点。
给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。
每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 '#' 。
你可以认为输入格式总是有效的,例如它永远不会包含两个连续的逗号,比如 "1,,3" 。
For example, the above binary tree can be serialized to the string "9,3,4,#,#,1,#,#,2,#,6,#,#", where '#' represents a null node.
Given a string of comma-separated values preorder, return true if it is a correct preorder traversal serialization of a binary tree.
It is guaranteed that each comma-separated value in the string must be either an integer or a character '#' representing null pointer.
You may assume that the input format is always valid.
For example, it could never contain two consecutive commas, such as "1,,3". Note: You are not allowed to reconstruct the tree.
Example:
Input: preorder = "9,3,4,#,#,1,#,#,2,#,6,#,#"
Output: true
Input: preorder = "1,#"
Output: false
Input: preorder = "9,#,#,1"
Output: false
思考线
解题思路
我们根据上面的题目可以知道结点为#的都是叶子结点,结点为数字的都是度数为2的节点。那么我们就可以根据二叉树的基本性质来做这道题目。
/**
* @param {string} preorder
* @return {boolean}
*/
var isValidSerialization = function(preorder) {
const preorderArr = preorder.split(',');
const len= preorderArr.length;
if(preorderArr[0] === '#') return len === 1; // 如果第一个节点为空
let ans = 1; // 接下来要遍历第一个节点不为空的全部节点,查看是否满足二叉树的基本性质
for(let i = 1; i < len; i++) {
if(preorderArr[i] === '#') {
ans--;
} else {
ans ++;
}
if(ans < 0 && i !== len -1) return false; // 如果是最后一个节点 应该为 -1
}
return ans === -1;
}
思路2
我们同样也可以利用出度和入度的概念来解决这道题。
我们只要把字符串遍历一次,每个节点都累加 diff = 出度 - 入度 。在遍历到任何一个节点的时候,要求diff >= 0,原因是还没遍历到该节点的子节点,所以此时的出度应该大于等于入度。当所有节点遍历完成之后,整棵树的 diff == 0 。 这里解释一下为什么下面的代码中 diff 的初始化为 1。因为,我们加入一个非空节点时,都会对 diff 先减去 1(入度),再加上 2(出度)。但是由于根节点没有父节点,所以其入度为 0,出度为 2。因此 diff 初始化为 1,是为了在加入根节点的时候,diff 先减去 1(入度),再加上 2(出度),此时 diff 正好应该是2.
/**
* @param {string} preorder
* @return {boolean}
*/
var isValidSerialization = function(preorder) {
const preorderArr = preorder.split(',');
const len = preorderArr.length;
let diff = 1;
for(let i = 0; i < len; i ++ ) {
diff -= 1;
if(diff < 0) {
return false;
} else if( preorderArr[i] !== '#') {
diff +=2;
}
}
return diff === 0;
}
思路3
这个思路是使用了栈的特性,我已开始也想到了要用栈来解决,但是没有做出来,接下来看看如何用栈自底向上来解决这个问题。
我们知道「前序遍历」是按照「根节点-左子树-右子树」的顺序遍历的,只有当根节点的所有左子树遍历完成之后,才会遍历右子树。对于本题的输入,我们可以先判断「左子树」是否有效的,然后再判断「右子树」是否有效的,最后判断「根节点-左子树-右子树」是否为有效的。这个思路类似于递归,而把递归改写成循环时,就会使用「栈」,这就是本题使用「栈」的原因。
下面的重点是如何判断一棵子树是否有效?首先考虑最简单情况:怎么判断一个节点是叶子节点?很明显,当一个节点的两个孩子都是 "#"(空)的时候,该节点就是叶子节点。
把有效的叶子节点使用 # 代替。 比如把 4## 替换成 # 。此时,叶子节点会变成空节点!
如输入: "9,3,4,#,#,1,#,#,2,#,6,#,#" ,当遇到 x,#,# 的时候,就把它变为 #。
模拟一遍过程:
[9,3,4,#,#] => [9,3,#],继续 [9,3,#,1,#,#] => [9,3,#,#] => [9,#] ,继续 [9,#2,#,6,#,#] => [9,#,2,#,#] => [9,#,#] => [#],结束
/**
* @param {string} preorder
* @return {boolean}
*/
var isValidSerialization = function(preorder) {
const preorderArr = preorder.split(',');
const len = preorderArr.length;
const stack = [];
for (let i = 0; i < len; i++) {
stack.push(preorderArr[i]);
while (stack.length >= 3 && stack[stack.length - 1] === '#' && stack[stack.length - 2] === '#' && stack[stack.length - 3] !== '#') {
console.log('innnn')
stack.pop(), stack.pop(), stack.pop();
stack.push('#');
}
}
console.log(stack)
return stack.length === 1 && stack.pop() === '#';
}
以上就是本题的三种思路。第一第二种都是利用了二叉树的性质来解决。第三个我们利用了栈的特性同时又很灵动的把叶子结点一一划掉来得到结果,每一种都值得细细品味。