• 二叉树的序列化与反序列化问题


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

    二叉树题目其实就是那几种遍历方式:前序,后序,中序,层级遍历

    二叉树的序列化和反序列化为例:

    leet297: 「二叉树的序列化与反序列化」就是给你输入一棵二叉树的根节点root,要求你实现如下一个类:

    public class Codec {
    
        // 把一棵二叉树序列化成字符串
        public String serialize(TreeNode root) {}
    
        // 把字符串反序列化成二叉树
        public TreeNode deserialize(String data) {}
    }
    

    我们可以用 serialize 方法将二叉树序列化成字符串,用 deserialize 方法将序列化的字符串反序列化成二叉树,至于以什么格式序列化和反序列化,这个完全由你决定。
    image-20210205080303895

    serialize 方法也许会把它序列化成字符串 2,1,#,6,3,#,#,其中 # 表示null 指针,那么把这个字符串再输入 deserialize 方法,依然可以还原出这棵二叉树。也就是说,这两个方法会成对儿使用,你只要保证他俩能够自洽就行了。
    二叉树结该是一个二维平面内的结构,而序列化出来的字符串是一个线性的一维结构。所谓的序列化不过就是把结构化的数据「打平」,其实就是在考察二叉树的遍历方式
    二叉树的遍历方式有哪些?递归遍历方式有前序遍历,中序遍历,后序遍历;迭代方式一般是层级遍历。本文就把这些方式都尝试一遍,来实现 serialize 方法和deserialize 方法。

    1.前序遍历解法

    前序遍历框架:

    void traverse(TreeNode root) {
        if (root == null) return;
    
        // 前序遍历的代码
    
        traverse(root.left);
        traverse(root.right);
    }
    

    很简单,在递归遍历两棵子树之前写的代码就是前序遍历代码
    调用 traverse 函数之后,这个 res 列表中元素的顺序是:
    image-20210205080806309

    那么 res = [1,2,-1,4,-1,-1,3,-1,-1],这就是将二叉树「打平」到了一个列表中,其中 -1 代表 null。

    StringBuilder 可以用于高效拼接字符串,所以也可以认为是一个列表,用 ,作为分隔符,用 # 表示空指针 null,调用完 traverse 函数后,StringBuilder 中的字符串应该是 1,2,#,4,#,#,3,#,#,

    String SEP = ",";
    String NULL = "#";
    
    /* 主函数,将二叉树序列化为字符串 */
    String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        serialize(root, sb);
        return sb.toString();
    }
    
    /* 辅助函数,将二叉树存入 StringBuilder */
    void serialize(TreeNode root, StringBuilder sb) {
        if (root == null) {
            sb.append(NULL).append(SEP);
            return;
        }
    
        /****** 前序遍历位置 ******/
        sb.append(root.val).append(SEP);
        /***********************/
    
        serialize(root.left, sb);
        serialize(root.right, sb);
    }
    

    现在,思考一下如何写 deserialize 函数,将字符串反过来构造二叉树。

    首先我们可以把字符串转化成列表:

    String data = "1,2,#,4,#,#,3,#,#,";
    String[] nodes = data.split(",");
    

    nodes 列表就是二叉树的前序遍历结果,问题转化为:如何通过二叉树的前序遍历结果还原一棵二叉树?
    [一般语境下,单单前序遍历结果是不能还原二叉树结构的,因为缺少空指针的信息,至少要得到前、中、后序遍历中的两种才能还原二叉树。但是这里的 node列表包含空指针的信息,所以只使用 node 列表就可以还原二叉树。]

    反序列化过程也是一样,先确定根节点 root,然后遵循前序遍历的规则,递归生成左右子树即可

    /* 主函数,将字符串反序列化为二叉树结构 */
    TreeNode deserialize(String data) {
        // 将字符串转化成列表
        LinkedList<String> nodes = new LinkedList<>();
        for (String s : data.split(SEP)) {
            nodes.addLast(s);
        }
        return deserialize(nodes);
    }
    
    /* 辅助函数,通过 nodes 列表构造二叉树 */
    TreeNode deserialize(LinkedList<String> nodes) {
        if (nodes.isEmpty()) return null;
    
        /****** 前序遍历位置 ******/
        // 列表最左侧就是根节点
        String first = nodes.removeFirst();
        if (first.equals(NULL)) return null;
        TreeNode root = new TreeNode(Integer.parseInt(first));
        /***********************/
    
        root.left = deserialize(nodes);
        root.right = deserialize(nodes);
    
        return root;
    }
    

    根据树的递归性质,nodes 列表的第一个元素就是一棵树的根节点,所以只要将列表的第一个元素取出作为根节点,剩下的交给递归函数去解决即可。

    2.后序遍历解法

    二叉树的后续遍历框架:

    void traverse(TreeNode root) {
        if (root == null) return;
        traverse(root.left);
        traverse(root.right);
    
        // 后序遍历的代码
    }
    

    与前序的序列化代码相比,只需要稍微修改辅助方法即可:

    /* 辅助函数,将二叉树存入 StringBuilder */
    void serialize(TreeNode root, StringBuilder sb) {
        if (root == null) {
            sb.append(NULL).append(SEP);
            return;
        }
    
        serialize(root.left, sb);
        serialize(root.right, sb);
    
        /****** 后序遍历位置 ******/
        sb.append(root.val).append(SEP);
        /***********************/
    }
    

    我们把对 StringBuilder 的拼接操作放到了后续遍历的位置,后序遍历导致结果的顺序发生变化:
    image-20210205082803419

    关键的难点在于,如何实现后序遍历的 deserialize 方法呢?是不是也简单地将关键代码放到后序遍历的位置就行了呢:

    /* 辅助函数,通过 nodes 列表构造二叉树 */
    TreeNode deserialize(LinkedList<String> nodes) {
        if (nodes.isEmpty()) return null;
    
        root.left = deserialize(nodes);
        root.right = deserialize(nodes);
    
        /****** 后序遍历位置 ******/
        String first = nodes.removeFirst();
        if (first.equals(NULL)) return null;
        TreeNode root = new TreeNode(Integer.parseInt(first));
        /***********************/
    
        return root;
    }
    

    显然上述代码是错误的,变量都没声明呢,就开始用了
    我们可以理一下不同遍历方式造成的差别在哪里,不同的遍历方式,导致序列化出来的结果不同,即序列化结果中哪里是root,哪一段是左子树,哪一段代表右子树,这里是主要区别,也就是序列化过程造成的区别,对于反序列化来说,关键在于找到root,左子树和右子树在序列中的位置,找到之后,其实就是填入二叉树的过程,那填的过程其实顺序影响不大,尤其是填root,左子树部分与右子树部分的过程相互独立
    所以:方法首先寻找 root 节点的值,然后递归计算左右子节点root 的值是列表的最后一个元素。我们应该从后往前取出列表元素,先用最后一个元素构造 root,然后递归调用生成 root 的左右子树。注意,根据上图,从后往前在 nodes 列表中取元素,一定要先构造 root.right 子树,后构造 root.left 子树(因为代码中取值的过程还会从列表中删去这个值,而中序遍历,右子树在左子树部分的后面,所以要先取右子树,还是一样的道理,反序列化过程中 的 填值其实顺序影响不大)

    /* 主函数,将字符串反序列化为二叉树结构 */
    TreeNode deserialize(String data) {
        LinkedList<String> nodes = new LinkedList<>();
        for (String s : data.split(SEP)) {
            nodes.addLast(s);
        }
        return deserialize(nodes);
    }
    
    /* 辅助函数,通过 nodes 列表构造二叉树 */
    TreeNode deserialize(LinkedList<String> nodes) {
        if (nodes.isEmpty()) return null;
        // 从后往前取出元素
        String last = nodes.removeLast();
        if (last.equals(NULL)) return null;
        TreeNode root = new TreeNode(Integer.parseInt(last));
        // 限构造右子树,后构造左子树
        root.right = deserialize(nodes);
        root.left = deserialize(nodes);
    
        return root;
    }
    

    3.中序遍历

    先说结论,中序遍历的方式行不通,因为无法实现反序列化方法 deserialize

    序列化方法 serialize 依然容易,只要把字符串的拼接操作放到中序遍历的位置就行了:

    /* 辅助函数,将二叉树存入 StringBuilder */
    void serialize(TreeNode root, StringBuilder sb) {
        if (root == null) {
            sb.append(NULL).append(SEP);
            return;
        }
    
        serialize(root.left, sb);
        /****** 中序遍历位置 ******/
        sb.append(root.val).append(SEP);
        /***********************/
        serialize(root.right, sb);
    }
    
    

    但是,我们刚才说了,要想实现反序列方法,首先要构造 root 节点。前序遍历得到的 nodes 列表中,第一个元素是 root 节点的值;后序遍历得到的 nodes列表中,最后一个元素是 root 节点的值。

    你看上面这段中序遍历的代码,root 的值被夹在两棵子树的中间,也就是在nodes 列表的中间,我们不知道确切的索引位置,所以无法找到 root 节点,也就无法进行反序列化。

    4.层级遍历

    层级遍历二叉树的代码框架:

    void traverse(TreeNode root) {
        if (root == null) return;
        // 初始化队列,将 root 加入队列
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);
    
        while (!q.isEmpty()) {
            TreeNode cur = q.poll();
    
            /* 层级遍历代码位置 */
            System.out.println(cur.val);
            /*****************/
    
            if (cur.left != null) {
                q.offer(cur.left);
            }
    
            if (cur.right != null) {
                q.offer(cur.right);
            }
        }
    }
    

    序列化代码:

    String SEP = ",";
    String NULL = "#";
    
    /* 将二叉树序列化为字符串 */
    String serialize(TreeNode root) {
        if (root == null) return "";
        StringBuilder sb = new StringBuilder();
        // 初始化队列,将 root 加入队列
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);
    
        while (!q.isEmpty()) {
            TreeNode cur = q.poll();
    
            /* 层级遍历代码位置 */
            if (cur == null) {
                sb.append(NULL).append(SEP);
                continue;
            }
            sb.append(cur.val).append(SEP);
            /*****************/
    
            q.offer(cur.left);
            q.offer(cur.right);
        }
    
        return sb.toString();
    }
    
    image-20210205084722475

    每一个非空节点都会对应两个子节点,那么反序列化的思路也是用队列进行层级遍历,同时用索引 i 记录对应子节点的位置

    /* 将字符串反序列化为二叉树结构 */
    TreeNode deserialize(String data) {
        if (data.isEmpty()) return null;
        String[] nodes = data.split(SEP);
        // 第一个元素就是 root 的值
        TreeNode root = new TreeNode(Integer.parseInt(nodes[0]));
    
        // 队列 q 记录父节点,将 root 加入队列
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);
    
        for (int i = 1; i < nodes.length; ) {
            // 队列中存的都是父节点
            TreeNode parent = q.poll();
            // 父节点对应的左侧子节点的值
            String left = nodes[i++];
            if (!left.equals(NULL)) {
                parent.left = new TreeNode(Integer.parseInt(left));
                q.offer(parent.left);
            } else {
                parent.left = null;
            }
            // 父节点对应的右侧子节点的值
            String right = nodes[i++];
            if (!right.equals(NULL)) {
                parent.right = new TreeNode(Integer.parseInt(right));
                q.offer(parent.right);
            } else {
                parent.right = null;
            }
        }
        return root;
    }
    
  • 相关阅读:
    DS4700磁盘阵列的控制器微码升级操作记录(收录百度文库)
    Android 解决布局无法对齐的情况
    android 模仿大众点评团购卷列表多余3条时折叠,点击时显示剩余全部的功能
    android 解决ScrollView中的子布局不能够填充整个ScrollView的情况。
    Android在代码中设置控件的drawableLeft,drawableRight,drawableTop,drawableBottom。
    android RadioGroup中设置selector后出现多个别选中的RadioButton的解决办法
    Android 动态的给Button、TextView、ImageView等控件设置了background后,再设置padding属性时该属性不起作用
    Android Universal Image Loader java.io.FileNotFoundException: http:/xxx/lxx/xxxx.jpg
    Android2.3系统 自定义的PopupWindow在实例化时报空指针异常
    android精品开源项目整理
  • 原文地址:https://www.cnblogs.com/shiji-note/p/14378683.html
Copyright © 2020-2023  润新知