leetcode
leetcode 401 ~ 450
序列化和反序列化二叉搜索树

序列化和反序列化二叉搜索树

难度:

标签:

题目描述

序列化是将数据结构或对象转换为一系列位的过程,以便它可以存储在文件或内存缓冲区中,或通过网络连接链路传输,以便稍后在同一个或另一个计算机环境中重建。

设计一个算法来序列化和反序列化 二叉搜索树 。 对序列化/反序列化算法的工作方式没有限制。 您只需确保二叉搜索树可以序列化为字符串,并且可以将该字符串反序列化为最初的二叉搜索树。

编码的字符串应尽可能紧凑。

 

示例 1:

输入:root = [2,1,3]
输出:[2,1,3]

示例 2:

输入:root = []
输出:[]

 

提示:

  • 树中节点数范围是 [0, 104]
  • 0 <= Node.val <= 104
  • 题目数据 保证 输入的树是一棵二叉搜索树。

代码结果

运行时间: 80 ms, 内存: 19.7 MB


/*
 * 思路:
 * 1. 使用Stream API进行序列化和反序列化。
 * 2. 通过前序遍历的方式将树的节点值收集到Stream中。
 * 3. 反序列化时,将Stream转换为队列,按照前序顺序重建树。
 */
 
import java.util.*;
import java.util.stream.*;
 
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int x) { val = x; }
}
 
public class Codec {
    // 序列化:使用Stream将树转换为字符串
    public String serialize(TreeNode root) {
        return Stream.of(root)
            .flatMap(this::preorderTraversal)
            .map(String::valueOf)
            .collect(Collectors.joining(","));
    }
 
    private Stream<Integer> preorderTraversal(TreeNode node) {
        if (node == null) return Stream.of(-1); // 空节点用-1表示
        return Stream.concat(Stream.of(node.val),
            Stream.concat(preorderTraversal(node.left), preorderTraversal(node.right)));
    }
 
    // 反序列化:将字符串转换为树
    public TreeNode deserialize(String data) {
        Queue<Integer> nodes = Arrays.stream(data.split(","))
            .map(Integer::valueOf)
            .collect(Collectors.toCollection(LinkedList::new));
        return deserializeHelper(nodes);
    }
 
    private TreeNode deserializeHelper(Queue<Integer> nodes) {
        int val = nodes.poll();
        if (val == -1) return null; // 空节点
        TreeNode root = new TreeNode(val);
        root.left = deserializeHelper(nodes);
        root.right = deserializeHelper(nodes);
        return root;
    }
}

解释

方法:

该题解使用深度优先搜索进行序列化和反序列化。序列化时,按照先根节点、然后左子树、最后右子树的顺序递归遍历二叉搜索树,将节点值存储到一个列表中。最后将列表转换为以逗号分隔的字符串。反序列化时,先将字符串按逗号分隔得到节点值列表,然后使用递归辅助函数重建二叉搜索树。递归函数根据当前子数组的起始和结束索引,找到第一个大于根节点值的位置 k,然后递归构建左子树(范围是 i+1 到 k-1)和右子树(范围是 k 到 j)。

时间复杂度:

O(n^2)

空间复杂度:

O(n)

代码细节讲解

🦆
在序列化和反序列化二叉搜索树的过程中,为什么选择使用先序遍历而不是中序或后序遍历?
先序遍历(根左右)在序列化和反序列化二叉搜索树时非常有用,因为先序遍历首先访问根节点,这使得在反序列化过程中可以直接从序列的开始重新构建出原始树的结构。使用中序遍历(左根右)序列化虽然仍然可以保持二叉搜索树的顺序,但在反序列化时无法直接确定根节点,因此需要额外的信息才能重建树。后序遍历(左右根)虽然最后访问根节点,可以从后往前构建树,但相较于先序遍历的直观和便捷性较差。
🦆
给定先序遍历的序列化数据,反序列化函数在查找第一个大于根节点值的位置时,是否考虑了可能存在重复值的情况?
在该题解的实现中,并没有特别考虑重复值的情况,因为标准的二叉搜索树定义不包括重复元素。序列化和反序列化的过程假设了所有节点值是唯一的。如果实际应用中树可能包含重复值,则需要调整算法以适应重复值的处理,例如通过标记每个节点在树中的具体位置或者在序列化数据中添加额外的信息。
🦆
为什么在序列化时没有存储表示空子树的特殊标记,这对反序列化时的准确性有何影响?
在此题解中,由于使用先序遍历和二叉搜索树的特性,序列化时没有存储空子树的特殊标记也足以确保反序列化的准确性。因为每个节点值的范围都受到它在树结构中位置的限制,即使没有空子树的特殊标记,仍可以通过当前节点值的范围来确定子树的边界。不过,对于一般的二叉树,不存储空节点信息可能会导致无法正确重建所有结构,特别是那些不平衡的部分。
🦆
反序列化的helper函数在每次递归时都需要从头到尾扫描数组来定位右子树的起始位置,这是否最优,有没有更高效的方法?
当前实现中的方法确实需要在每次递归时扫描数组以找到右子树的起始位置,这在某些情况下可能效率较低(时间复杂度接近O(n^2))。更优的方法是使用二分搜索技术或者额外的数据结构(如栈)来辅助定位,这可以显著提高效率。例如,可以在遍历数组时使用栈来跟踪潜在的右子节点,或者在二叉搜索树的性质基础上使用二分查找来确定元素分界,从而将时间复杂度降低到O(n log n)。

相关问题

二叉树的序列化与反序列化

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

 

示例 1:

输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [1]
输出:[1]

示例 4:

输入:root = [1,2]
输出:[1,2]

 

提示:

  • 树中结点数在范围 [0, 104]
  • -1000 <= Node.val <= 1000

寻找重复的子树

给你一棵二叉树的根节点 root ,返回所有 重复的子树

对于同一类的重复子树,你只需要返回其中任意 一棵 的根结点即可。

如果两棵树具有 相同的结构相同的结点值 ,则认为二者是 重复 的。

 

示例 1:

输入:root = [1,2,3,4,null,2,4,null,null,4]
输出:[[2,4],[4]]

示例 2:

输入:root = [2,1,1]
输出:[[1]]

示例 3:

输入:root = [2,2,2,3,null,3,null]
输出:[[2,3],[3]]

 

提示:

  • 树中的结点数在 [1, 5000] 范围内。
  • -200 <= Node.val <= 200

序列化和反序列化 N 叉树

序列化和反序列化 N 叉树