如何挖掘二叉树特性的「规律」解法 (含证明)| Java 刷题打卡

简介: 如何挖掘二叉树特性的「规律」解法 (含证明)| Java 刷题打卡

网络异常,图片无法展示
|


题目描述



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


Tag : 「二叉树」


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


例如:


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


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


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


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


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


示例 1:


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


示例 2:


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


示例 3:


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


二叉树规律解法



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


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


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


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


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


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


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


证明 1(利用不等式)



我们令非空节点数量为 m,空节点数量为 n,入度和出度仍然使用 in 和 out 代表。


找一下 in 和 out 与 n 和 m 之间的关系。


一颗合格二叉树 m 和 n 的最小的比例关系是 1 : 2,也就是对应了这么一个形状:


4 
/ \
# #
复制代码


而遍历过程中 m 和 n 的最小的比例关系则是 1 : 0,这其实对应了二叉树空节点总是跟在非空节点的后面这一性质。


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


非空节点数量 >= 空节点数量 在遍历没结束前恒成立:m>=n


然后再结合「每一个非空节点都对应了 2 个出度,空节点都对应了 0 个出度;除了根节点,每个节点都有一个入度」特性。


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


  1. m >= nm>=n
  2. in <= m + n - 1in<=m+n1
  3. out <= 2 * mout<=2m


简单的变形可得:


  • 由 2 变形可得:m >= in + 1 - nm>=in+1n
  • 由 3 变形可得:m >= out / 2m>=out/2


即有:


  1. m >= nm>=n
  2. m >= in + 1 - nm>=in+1n
  3. m >= out / 2m>=out/2


再将 1 和 2 相加,抵消 n2m >= in + 12m>=in+1


  1. 2m >= in + 12m>=in+1 => in <= 2m - 1in<=2m1
  2. m >= out / 2m>=out/2 => out <= 2mout<=2m


因此,在遍历尚未完成时,inout 始终满足上述关系(与空节点数量 n 无关)。


如果不从合格二叉树的前提(m>=nm>=n)出发,我们是无法得到上述关系式的。


因此,我们可以一边遍历一边统计「严格出度」和「严格入度」,然后写一个 check 函数去判定 inoutm 三者关系是否符合要求,如果不符合则说明二叉树不合法。


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(利用技巧转换为等式)



我们令非空节点数量为 m,空节点数量为 n,入度和出度仍然使用 inout 代表。

找一下 inoutnm 之间的关系。


一颗合格二叉树 mn 的最小的比例关系是 1 : 2,也就是对应了这么一个形状:


4 
/ \
# #
复制代码


而遍历过程中 mn 的最小的比例关系则是 1 : 0,这其实对应了二叉树空节点总是跟在非空节点的后面这一性质。


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


非空节点数量 >= 空节点数量 在遍历没结束前恒成立:m>=nm>=n


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


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


  1. m >= nm>=n
  2. in = m + n - 1in=m+n1
  3. out = 2 * mout=2m


从第 2 个等式出发,结合第 1 个等式:


in = m + n - 1 <= m + m - 1 = 2m - 1 = out - 1in=m+n1<=m+m1=2m1=out1

即可得 in + 1 <= outin+1<=out ,也就是 in < out 恒成立。


代码:


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;
    }
}
复制代码


  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)


最后



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


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


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


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

相关文章
|
1月前
|
Java
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
26 1
|
1月前
|
算法 Java C语言
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
23 1
|
18天前
|
算法 Java
JAVA 二叉树面试题
JAVA 二叉树面试题
14 0
|
3月前
|
存储 算法 Java
LeetCode经典算法题:二叉树遍历(递归遍历+迭代遍历+层序遍历)以及线索二叉树java详解
LeetCode经典算法题:二叉树遍历(递归遍历+迭代遍历+层序遍历)以及线索二叉树java详解
79 0
|
5月前
|
Java
二叉树简单遍历、查找、删除(java)
二叉树简单遍历、查找、删除(java)
|
6天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
15天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
3天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
16 9
|
6天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
2天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin