【月度刷题计划同款】验证二叉树的前序序列化

题目描述

这是 LeetCode 上的 331. 验证二叉树的前序序列化 ,难度为 中等

Tag : 「二叉树」

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

bash 复制代码
     _9_
    /   \
   3     2
  / \   / \
 4   1  #  6
/ \ / \   / \
# # # #   # #

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

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

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

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

示例 1:

arduino 复制代码
输入: "9,3,4,#,#,1,#,#,2,#,6,#,#"
输出: true

示例 2:

arduino 复制代码
输入: "1,#"
输出: false

示例 3:

arduino 复制代码
输入: "9,#,#,1"
输出: false

二叉树规律解法

事实上,我们能利用「二叉树」的特性来做。

由于每一个非空节点都对应了 2 个出度,空节点都对应了 0 个出度;除了根节点,每个节点都有一个入度。

我们可以使用 inout 来分别记录「入度」和「出度」的数量;mn 分别代表「非空节点数量」和「空节点数量」。

同时,一颗合格的二叉树最终结果必然满足 in == out

但我们又不能只利用最终 in == out 来判断是否合法,这很容易可以举出反例:考虑将一个合法序列的空节点全部提前,这样最终结果仍然满足 in == out,这样的二叉树是不存在的。

我们还需要一些额外的特性,支持我们在遍历过程中提前知道一颗二叉树不合法。

例如,我们可以从合格二叉树的前提出发,挖掘遍历过程中 inoutnm 的关系。

证明 1(利用不等式)

我们令非空节点数量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m,空节点数量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n,入度和出度仍然使用 <math xmlns="http://www.w3.org/1998/Math/MathML"> i n in </math>in 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> o u t out </math>out 代表。

找一下 <math xmlns="http://www.w3.org/1998/Math/MathML"> i n in </math>in 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> o u t out </math>out 与 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m 之间的关系。

一颗合格二叉树 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 的最小的比例关系是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 : 2 1 : 2 </math>1:2,也就是对应了这么一个形状:

shell 复制代码
 4 
/ \
# #

而遍历过程中 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 的最小的比例关系则是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 : 0 1 : 0 </math>1:0,这其实对应了二叉树空节点总是跟在非空节点的后面这一性质

换句话说,在没到最后一个节点之前,我们是不会遇到 空节点数量 > 非空节点数量 的情况的。

非空节点数量 >= 空节点数量 在遍历没结束前恒成立: <math xmlns="http://www.w3.org/1998/Math/MathML"> m > = n m>=n </math>m>=n

然后再结合「每一个非空节点都对应了 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2 个出度,空节点都对应了 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 个出度;除了根节点,每个节点都有一个入度」特性。

在遍历尚未结束前,我们有以下关系:

  1. <math xmlns="http://www.w3.org/1998/Math/MathML"> m > = n m >= n </math>m>=n
  2. <math xmlns="http://www.w3.org/1998/Math/MathML"> i n < = m + n − 1 in <= m + n - 1 </math>in<=m+n−1
  3. <math xmlns="http://www.w3.org/1998/Math/MathML"> o u t < = 2 ∗ m out <= 2 * m </math>out<=2∗m

简单的变形可得:

  • 由 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2 变形可得: <math xmlns="http://www.w3.org/1998/Math/MathML"> m > = i n + 1 − n m >= in + 1 - n </math>m>=in+1−n
  • 由 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>3 变形可得: <math xmlns="http://www.w3.org/1998/Math/MathML"> m > = o u t / 2 m >= out / 2 </math>m>=out/2

即有:

  1. <math xmlns="http://www.w3.org/1998/Math/MathML"> m > = n m >= n </math>m>=n
  2. <math xmlns="http://www.w3.org/1998/Math/MathML"> m > = i n + 1 − n m >= in + 1 - n </math>m>=in+1−n
  3. <math xmlns="http://www.w3.org/1998/Math/MathML"> m > = o u t / 2 m >= out / 2 </math>m>=out/2

再将 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2 相加,抵消 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n: <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 m > = i n + 1 2m >= in + 1 </math>2m>=in+1

  1. <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 m > = i n + 1 2m >= in + 1 </math>2m>=in+1 => <math xmlns="http://www.w3.org/1998/Math/MathML"> i n < = 2 m − 1 in <= 2m - 1 </math>in<=2m−1
  2. <math xmlns="http://www.w3.org/1998/Math/MathML"> m > = o u t / 2 m >= out / 2 </math>m>=out/2 => <math xmlns="http://www.w3.org/1998/Math/MathML"> o u t < = 2 m out <= 2m </math>out<=2m

因此,在遍历尚未完成时, <math xmlns="http://www.w3.org/1998/Math/MathML"> i n in </math>in 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> o u t out </math>out 始终满足上述关系(与空节点数量 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 无关)。

如果不从合格二叉树的前提( <math xmlns="http://www.w3.org/1998/Math/MathML"> m > = n m>=n </math>m>=n)出发,我们是无法得到上述关系式的。

因此,我们可以一边遍历一边统计「严格出度」和「严格入度」,然后写一个 check 函数去判定 <math xmlns="http://www.w3.org/1998/Math/MathML"> i n in </math>in、 <math xmlns="http://www.w3.org/1998/Math/MathML"> o u t out </math>out 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m 三者关系是否符合要求,如果不符合则说明二叉树不合法。

代码:

Java 复制代码
class Solution {
  public boolean isValidSerialization(String s) {
    String[] ss = s.split(",");
    int n = ss.length;
    int in = 0, out = 0;
    for (int i = 0, m = 0; i < n; i++) {
      // 统计「严格出度」和「严格入度」...
      if (i != n - 1 && !check(m, in, out)) return false;
    } 
    return in == out;
  }
  boolean check(int m, int in, int out) {
    boolean a = (in <= 2 * m - 1), b = (out <= 2 * m);
    return a && b; 
  }
}

注意:因为我们这里的证明使用到的是不等式。因此统计的必须是「严格出度」&「严格入度」,不能假定一个「非空节点(非根)」必然对应两个「出度」和一个「入度」

要想统计出「严格出度」&「严格入度」在编码上还是有一定难度的。那么是否可以推导出更加简单性质来使用呢?

请看「证明 2」。

证明 2(利用技巧转换为等式)

我们令非空节点数量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m,空节点数量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n,入度和出度仍然使用 <math xmlns="http://www.w3.org/1998/Math/MathML"> i n in </math>in 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> o u t out </math>out 代表。

找一下 <math xmlns="http://www.w3.org/1998/Math/MathML"> i n in </math>in 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> o u t out </math>out 与 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m 之间的关系。

一颗合格二叉树 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 的最小的比例关系是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 : 2 1 : 2 </math>1:2,也就是对应了这么一个形状:

shell 复制代码
 4 
/ \
# #

而遍历过程中 <math xmlns="http://www.w3.org/1998/Math/MathML"> m m </math>m 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> n n </math>n 的最小的比例关系则是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 : 0 1 : 0 </math>1:0,这其实对应了二叉树空节点总是跟在非空节点的后面这一性质。

换句话说,在没到最后一个节点之前,我们是不会遇到 空节点数量 > 非空节点数量 的情况的。

非空节点数量 >= 空节点数量 在遍历没结束前恒成立: <math xmlns="http://www.w3.org/1998/Math/MathML"> m > = n m>=n </math>m>=n

之后我们再采用一个技巧,就是遍历过程中每遇到一个「非空节点」就增加两个「出度」和一个「入度」,每遇到一个「空节点」只增加一个「入度」。而不管每个「非空节点」是否真实对应两个子节点。

那么我们的起始条件变成:

  1. <math xmlns="http://www.w3.org/1998/Math/MathML"> m > = n m >= n </math>m>=n
  2. <math xmlns="http://www.w3.org/1998/Math/MathML"> i n = m + n − 1 in = m + n - 1 </math>in=m+n−1
  3. <math xmlns="http://www.w3.org/1998/Math/MathML"> o u t = 2 ∗ m out = 2 * m </math>out=2∗m

从第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2 个等式出发,结合第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 个等式:

<math xmlns="http://www.w3.org/1998/Math/MathML"> i n = m + n − 1 < = m + m − 1 = 2 m − 1 = o u t − 1 in = m + n - 1 <= m + m - 1 = 2m - 1 = out - 1 </math>in=m+n−1<=m+m−1=2m−1=out−1

即可得 <math xmlns="http://www.w3.org/1998/Math/MathML"> i n + 1 < = o u t in + 1 <= out </math>in+1<=out ,也就是 <math xmlns="http://www.w3.org/1998/Math/MathML"> i n < o u t in < out </math>in<out 恒成立。

代码:

Java 复制代码
class Solution {
    public boolean isValidSerialization(String s) {
        String[] ss = s.split(",");
        int n = ss.length;
        int in = 0, out = 0;
        for (int i = 0; i < n; i++) {
            if (!ss[i].equals("#")) out += 2;
            if (i != 0) in++;
            if (i != n - 1 && out <= in) return false;
        } 
        return in == out;
    }
}
  • 时间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.331 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour...

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

相关推荐
拾贰_C几秒前
【SpringBoot】MyBatisPlus(MP | 分页查询操作
java·spring boot·后端·spring·maven·apache·intellij-idea
远瞻。6 分钟前
【论文阅读】人脸修复(face restoration ) 不同先验代表算法整理2
论文阅读·算法
先做个垃圾出来………3 小时前
哈夫曼树(Huffman Tree)
数据结构·算法
phoenix@Capricornus5 小时前
反向传播算法——矩阵形式递推公式——ReLU传递函数
算法·机器学习·矩阵
Inverse1625 小时前
C语言_动态内存管理
c语言·数据结构·算法
数据与人工智能律师5 小时前
虚拟主播肖像权保护,数字时代的法律博弈
大数据·网络·人工智能·算法·区块链
就叫飞六吧5 小时前
Spring Security 集成指南:避免 CORS 跨域问题
java·后端·spring
wuqingshun3141595 小时前
蓝桥杯 16. 外卖店优先级
c++·算法·职场和发展·蓝桥杯·深度优先
YouQian7726 小时前
2025春训第十九场
算法
CodeJourney.6 小时前
基于MATLAB的生物量数据拟合模型研究
人工智能·爬虫·算法·matlab·信息可视化