• 剑指Offer_#37 序列化二叉树


    剑指Offer_#37 序列化二叉树

    Contents

    题目

    请实现两个函数,分别用来序列化和反序列化二叉树。
    示例: 
    你可以将以下二叉树:

        1
       / 
      2   3
         / 
        4   5

    序列化为 "[1,2,3,null,null,4,5]"

    思路分析

    题意分析

    1. 用哪种遍历方式进行序列化?
      这一题给出的示例,序列化时采用层序遍历(BFS),与书上不一样,书中是用前序遍历。但是实际上根据代码模板里边的说明:

      Your Codec object will be instantiated and called as such:
      Codec codec = new Codec();
      codec.deserialize(codec.serialize(root));

      在Leetcode平台中,它测试的方法是:对一个二叉树先做序列化,再做反序列化,然后看结果是否跟输入的二叉树相同。
      而没有去分别测试序列化的结果,反序列化的结果。
      所以说,要通过这一题,无论用哪种遍历方式都是可以的,重点在于,serializedeserialize函数是互逆的。
      看到题目的例子,我下意识认为序列化必须是层序遍历,感觉具有一定的迷惑性...层序遍历也是写法之一,但是代码较为复杂,也可以选择使用代码较简单的前序遍历。

    2. 剑指 Offer 07. 重建二叉树的区别?

      • 7题是通过前序遍历和中序遍历两个序列建立二叉树,而本题只需要用一种序列就可以重建二叉树。
      • 原因是:7题中给出的序列不包含null节点,一个序列中没有足够的信息量来重建二叉树,必须依赖两个序列重建。

    思路

    解法1:层序遍历(BFS)

    1. 序列化
      参考剑指Offer_#32_从上到下打印二叉树的写法,改动的地方是

      • 函数返回值变为String,使用StringBuilder来迭代式的构造字符串。
      • while循环中,可以向队列中加入是null的子节点;同时也要针对节点为null的情况做特殊处理,用一个“null”字符串来表示。
    2. 反序列化
      依然要借助队列来辅助。整体思路如下:

      • 队列存储的是待构造的节点val已经设置,但左右指针还未设置好),每次取出头部节点,设置它的leftright指针。
      • 同时遍历序列化字符串二叉树节点队列
        • 每次从队列取出一个节点node,对应的从序列化字符串里取出它的左右子节点,连接到node上。
        • 然后将左右子节点加入队列(因为他们的左右指针还未设置好)
      • 一个关键点:对于“null”的处理
        • 序列化字符串中遇到null的时候,不需要做任何处理。
        • 因为:
          • null节点不需要被连接到node,因为初始化时,node的左右子节点默认就是null;
          • null节点不需要设置左右子节点,所以不需要加入队列。

    解法2:前序遍历

    1. 序列化
      树的前序遍历。唯一特殊的地方是,需要将结果转换成String再返回。
    2. 反序列化
      递归遍历vals和树。
      • 出口条件: 遍历到$,返回null
      • 递推工作
        • 构建一个节点node,移动指向vals元素的指针index
        • 构建当前节点的左右子节点
        • 返回node

    解答

    解答1:层序遍历

    public class Codec {
        // Encodes a tree to a single string.
        public String serialize(TreeNode root) {
            if(root == null) return "[]";
            StringBuilder res = new StringBuilder("[");
            Queue<TreeNode> q = new LinkedList<>();
            q.offer(root);
            while(!q.isEmpty()){
                TreeNode node = q.poll();
                if(node != null){
                    res.append(node.val + ",");
                    q.offer(node.left);
                    q.offer(node.right);
                }
                //如果当前访问的节点是null,那么就不需要添加其左右子节点到队列
                else
                    res.append("null,");
            }
            res.deleteCharAt(res.length() - 1);
            res.append("]");
            return res.toString();
        }
        // Decodes your encoded data to tree.
        public TreeNode deserialize(String data) {
            if(data.equals("[]")) return null;
            String[] vals = data.substring(1,data.length() - 1).split(",");
            TreeNode root = new TreeNode(Integer.parseInt(vals[0]));
            Queue<TreeNode> q = new LinkedList<>();
            q.offer(root);
            //由于第索引为0的是根节点,已经入队列,所以while循环当中是从索引1开始的
            int i = 1;
            while(!q.isEmpty()){
                TreeNode node = q.poll();
                //构建左子节点
                //如果左子节点对应的值是“null”,则不做任何处理,直接跳过
                if(!vals[i].equals("null")){
                    TreeNode leftNode = new TreeNode(Integer.parseInt(vals[i]));
                    node.left = leftNode;
                    q.offer(leftNode);
                }
                i++;
                //构建右子节点
                if(!vals[i].equals("null")){
                    TreeNode rightNode = new TreeNode(Integer.parseInt(vals[i]));
                    node.right = rightNode;
                    q.offer(rightNode);
                }
                i++;
            }
            return root;
        }
    }

    解答2:前序遍历

    public class Codec {
        String[] vals;
        int index = 0;//vals[]的索引
    
    
        // Encodes a tree to a single string.
        public String serialize(TreeNode root) {
            if(root == null) return "[]";
            StringBuilder res = new StringBuilder();
            res.append("[");
            serializeRecur(root,res);
            res.deleteCharAt(res.length() - 1);
            res.append("]");
            return res.toString();
        }
    
        void serializeRecur(TreeNode root,StringBuilder res){
            if(root == null){
                res.append("$,");
                return;
            }
            //形参res指向的内存空间不变,所以可以修改原本的res变量
            res.append(root.val + ",");
            serializeRecur(root.left,res);
            serializeRecur(root.right,res);
            return;
        }
    
        // Decodes your encoded data to tree.
       public TreeNode deserialize(String data) {
            if(data.equals("[]")) return null;
            String[] vals = data.substring(1,data.length() - 1).split(",");
            this.vals = vals;
            return deserializeRecur();
        }
    
       TreeNode deserializeRecur(){
            if(vals[index].equals("$")){
                index++;
                return null;
            }
            else{
                //构建完当前的节点,就增加index,指向下一个节点的值
                TreeNode node =new TreeNode(Integer.parseInt(vals[index++]));
                node.left = deserializeRecur();
                node.right = deserializeRecur();
                return node;
            }
        }
    }

    Java基础:值传递和引用传递

    上面的两个递归函数serializeRecurdeserializeRecur当中体现了Java参数传递的知识。

    deserializeRecur的一种错误写法

    // Decodes your encoded data to tree.
        public TreeNode deserialize(String data) {
            if(data.equals("[]")) return null;
            String[] vals = data.substring(1,data.length() - 1).split(",");
            this.vals = vals;
            TreeNode root = new TreeNode();
            deserializeRecur(root);
            return root;
        }
        
        void deserializeRecur(TreeNode node){
            if(vals[index].equals("$")){
                node = null;
                index++;
                return;
            }
            else{
                //错误就在这句,这里把node形参指向了一个新对象,那么之后的所有操作,都和调用处传入的root无关了,root不会被改变
                node = new TreeNode(Integer.parseInt(vals[index++]));
                deserializeRecur(node.left);
                deserializeRecur(node.right);
            }
            
        }

    这是我第一次照猫画虎照着书上的C++代码写下来的, 但是Java跟C++不同啊,C++代码是:

    void Deserialize(BinaryTreeNode** pRoot, istream& stream)
    {
        int number;
        if(ReadStream(stream, &number))
        {
            *pRoot = new BinaryTreeNode();
            (*pRoot)->m_nValue = number;
            (*pRoot)->m_pLeft = nullptr;
            (*pRoot)->m_pRight = nullptr;
    
            Deserialize(&((*pRoot)->m_pLeft), stream);
            Deserialize(&((*pRoot)->m_pRight), stream);
        }
    }

    C++当中,pRoot是一个指针,*pRootpRoot指向的内存空间,这里用赋值语句,修改的就是指针指向的内容;
    但是在java当中,node并不等同于指针,赋值语句不会修改node原本指向的内存空间,而是把node指向一个新创建的TreeNode对象的内存空间。
    关于这部分知识,可以参考

  • 相关阅读:
    Java之Chat历程
    Java之静态方法中的内部类
    Java异常捕获之finally
    C语言复杂声明的本质与局限
    使用beyond compare或kompare作为git的对比、合并工具
    [二分] [洛谷] P1258 小车问题
    [STL] [洛谷] P1165 日志分析
    [洛谷] P2802 回家
    卡特兰数的应用
    [洛谷] P1722 矩阵Ⅱ
  • 原文地址:https://www.cnblogs.com/Howfars/p/13283898.html
Copyright © 2020-2023  润新知