「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」
题目要求
序列化二叉树的一种方法是使用前序遍历。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #。
例如,上面的二叉树可以被序列化为字符串 "9,3,4,#,#,1,#,#,2,#,6,#,#",其中 # 代表一个空节点。
给定一串以逗号分隔的序列,验证它是否是正确的二叉树的前序序列化。编写一个在不重构树的条件下的可行算法。
每个以逗号分隔的字符或为一个整数或为一个表示 null 指针的 '#' 。
你可以认为输入格式总是有效的,例如它永远不会包含两个连续的逗号,比如 "1,,3" 。
示例 1:
输入: "9,3,4,#,#,1,#,#,2,#,6,#,#"
输出: true
示例 2:
输入: "1,#"
输出: false
示例 3:
输入: "9,#,#,1"
输出: false
栈方法(假定槽位)
我们可以把每一个节点当做一个槽位,等待被填充中。如果该节点为空节点,则消耗掉了一个槽位。
如果该节点为一个非空节点,则消耗掉一个槽位的同时,又新增了两个槽位。根节点需特殊处理。
我们使用栈维护槽位的变化。栈中的每一个元素,都代表了剩余槽位的数量,栈顶元素代表下一步可用的槽位的数量。遇到空节点时,栈顶元素-1,遇到非空节点时,栈顶元素-1,+2。只要栈顶元素为0,即可出栈。
遍历结束后,若栈为空,代表没有需要填充的槽位。则是合法序列。否则不合法,在遍历过程中,如若槽位不足,同样不合法。
var isValidSerialization = function (preorder) {
let arr = preorder.split(',');
let stack = [1];
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if (!stack.length) return false;
const len = stack.length;
if (item == "#") {
stack[len - 1]--;
if (stack[len - 1] == 0) stack.pop()
} else {
stack[len - 1]--;
if (stack[len - 1] == 0) stack.pop()
stack.push(2)
}
}
return stack.length == 0
};
槽位法升级
上面方法中,把每一个节点的槽位,看成是独立的,如果我们把所有槽位看成是一个整体。用一个元素维护所有的槽位数量的变化。
var isValidSerialization = function (preorder) {
const arr = preorder.split(",");
const len = arr.length;
// 默认初始化有一个槽位,如果是一个空链表["#"],也需要消耗一个槽位。
let slots = 1;
for (let i = 0; i < len; i++) {
if (slots == 0) return false;
if (arr[i] == "#") {
slots--
} else {
slots++
}
}
return slots === 0
};
二叉树特性法
根据前序遍历的特性,根→左→右的顺序遍历,只有当根节点的所有左子树全部遍历完成之后,才会遍历右子树。所以我们可以先判断左子树是否有效,再判断右子树是否有效。最后再判断根节点。用递归的思路处理。下面就是如何判断一棵子树是否有效呢,我们先需要找到最深的结点,也就是叶子结点,即左右子树均为”#”的结点。所以如果遍历到了叶子结点,则证明遍历到了最后一层,如果该节点成立,我们可以把这个节点置空,来判断其根节点是否也成立。即利用“#”将叶子结点替代。如题中把 4## 替换成 # 。
"9,3,4,#,#,1,#,#,2,#,6,#,#""9,3,#,1,#,#,2,#,6,#,#""9,3,#,#,2,#,6,#,#""9,#,2,#,6,#,#""9,#,2,#,#""9,#,#""#"
如上
var isValidSerialization = function (preorder) {
const arr = preorder.split(",");
const len = arr.length;
let stack = [];
for (let i = 0; i < len; i++) {
stack.push(arr[i])
while (stack.length >= 3 && stack[stack.length - 1] == "#" &&
stack[stack.length - 2] == "#" && stack[stack.length - 3] != "#") {
stack.pop(), stack.pop(), stack.pop()
stack.push('#')
}
}
return stack.length == 1 && stack[0] == "#"
};
递归方法
我们依旧根据二叉树的前序遍历的特性,我们进行一次前序遍历,来判断二叉树的节点数。
我们先将给定的字符串分割成数组。利用前序遍历,遍历数组,计算得到左右子节点的数量再加上根节点个数1,判断是否等于数组的长度。
var isValidSerialization = function (preorder) {
const arr = preorder.split(",");
let isValid = true;
let len = dfs(0, arr);
return isValid && len == arr.length
function dfs(index, arr) {
if (index >= arr.length) {
isValid = false;
return 0
}
if (arr[index] == "#") return 1;
let leftLen = dfs(index + 1, arr);
let rightLen = dfs(index + 1 + leftLen, arr)
return 1 + leftLen + rightLen
}
};
二叉树的规律,入度和出度
由于一个二叉树的每一个非空节点都有两个出度,而空节点没有出度,所有结点(根结点除外)都有一个入度。
我们可以使用 in 和 out 来分别记录「入度」和「出度」的数量;m 和 n分别代表「非空节点数量」和「空节点数量」。
同时,一颗合格的二叉树最终结果必然满足 in == out。
但我们又不能只利用最终 in == out 来判断是否合法,这很容易可以举出反例:考虑将一个合法序列的空节点全部提前,这样最终结果仍然满足 in == out,但这样的二叉树是不存在的。
我们还需要一些额外的特性,支持我们在遍历过程中提前知道一颗二叉树不合法。
例如,我们可以从合格二叉树的前提出发,挖掘遍历过程中 in 和 out 与 n 和 m 的关系。
一颗合格二叉树 m 和 n 的最小的比例关系是 1 : 2,也就是对应了这么一个形状:
4
/ \\
# #
而遍历过程中 m 和 n ``的最小的比例关系则是 1 : 0,这其实对应了二叉树空节点总是跟在非空节点的后面这一性质。
换句话说,在没到最后一个节点之前,我们是不会遇到 空节点数量 > 非空节点数量 的情况的。
非空节点数量 >= 空节点数量 在遍历没结束前恒成立:m>=n
之后我们再采用一个技巧,就是遍历过程中每遇到一个「非空节点」就增加两个「出度」和一个「入度」,每遇到一个「空节点」只增加一个「入度」。而不管每个「非空节点」是否真实对应两个子节点。
var isValidSerialization = function (preorder) {
const arr = preorder.split(",");
const len = arr.length;
let enter = 0,
out = 0;
for (let i = 0; i < len; i++) {
if (arr[i] != "#") out += 2;
if (i != 0) enter++;
if (i != (len - 1) && out <= enter) return false
}
return enter == out
};
由于in在JavaScript中为特殊字符,所以我们用enter代替。