zoukankan      html  css  js  c++  java
  • 【LeetCode】297.二叉树的序列化与反序列化(手绘图解,BFS+DFS两种方法)

    题目

    链接:https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/

    问题描述:image-20200620134118967

    解析

    方法1:DFS(递归)

    递归可以理解为:转交职责

    • 递归一棵树,只关注当前的单个节点就好
    • 剩下的工作,交给递归完成
      • “serialize 函数,你能不能帮我序列化我的左右子树?我等你的返回结果,再追加到我身上。”
    • 为什么选择 前序遍历,因为在反序列化时,根|左|右,更容易定位根节点值
    • 遇到 null 节点也要翻译成一个特殊符号,反序列化时才知道这里对应 null 节点

    image.png

    序列化 代码

    const serialize = (root) => {
      if (root == null) return 'X,' // 遇到null节点
      const leftSerialized = serialize(root.left)   //左子树的序列化字符串
      const rightSerialized = serialize(root.right) //右子树的序列化字符串
      return root.val + ',' + leftSerialized + rightSerialized // 根|左|右
    }
    
    

    反序列化——也是递归

    序列化时是前序遍历,所以序列化字符串呈现这样的排列:
    “根|(根|(根|左|右)|(根|左|右))|(根|(根|左|右)|(根|左|右))”

    image.png

    构建树的函数 buildTree

    • buildTree 接收的 “状态” 是 list 数组,由序列化字符串转成
    • 按照前序遍历的顺序:先构建根节点,再构建左子树,再构建右子树
    • list 数组首项是 当前子树的根节点,弹出,先构建它

    buildTree 关注当前节点,然后职责转交

    • 将 list 数组的首项弹出,考察它
    • 如果它为 ‘X’ ,直接返回 null ,没有子树可构建
    • 如果它不为 ‘X’,则为它创建 node 节点,并构建子树
    • 递归调用 buildTree 构建左子树
    • 递归调用 buildTree 构建右子树
    • 以 node 为根节点的子树,构建完毕,向上返回

    image.png

    反序列化 代码

    const buildTree = (list) => {        // dfs函数
      const nodeVal = list.shift()       // 当前考察的节点
      if (nodeVal == 'X') return null    // 是X,返回null给父调用
      const node = new TreeNode(nodeVal) // 创建node节点
      node.left = buildTree(list)        // 构建node的左子树
      node.right = buildTree(list)       // 构建node的右子树
      return node                        // 返回以node为根节点的子树给父调用
    }
    const deserialize = (data) => {
      const list = data.split(',')       // 转成list数组
      return buildTree(list)             // 构建树,dfs的入口
    }
    
    

    完整实现:

    实现1:耗时比较大

    public class Codec {
        public String rserialize(TreeNode root, String str) {
            if (root == null) {
                str += "None,";
            } else {
                str += str.valueOf(root.val) + ",";
                str = rserialize(root.left, str);
                str = rserialize(root.right, str);
            }
            return str;
        }
      
        public String serialize(TreeNode root) {
            return rserialize(root, "");
        }
      
        public TreeNode rdeserialize(List<String> l) {
            if (l.get(0).equals("None")) {
                l.remove(0);
                return null;
            }
      
            TreeNode root = new TreeNode(Integer.valueOf(l.get(0)));
            l.remove(0);
            root.left = rdeserialize(l);
            root.right = rdeserialize(l);
        
            return root;
        }
      
        public TreeNode deserialize(String data) {
            String[] data_array = data.split(",");
            List<String> data_list = new LinkedList<String>(Arrays.asList(data_array));
            return rdeserialize(data_list);
        }
    };
    
    

    实现2:采用StringBuilder类,耗时小

    public class Codec {
    
        public String serialize(TreeNode root) {      //用StringBuilder
            StringBuilder res = ser_help(root, new StringBuilder());
            return res.toString();
        }
        
        public StringBuilder ser_help(TreeNode root, StringBuilder str){
            if(null == root){
                str.append("null,");
                return str;
            }
            str.append(root.val); 
            str.append(",");
            str = ser_help(root.left, str);
            str = ser_help(root.right, str);
            return str;
        }
    
        public TreeNode deserialize(String data) {
            String[] str_word = data.split(",");
            List<String> list_word = new LinkedList<String>(Arrays.asList(str_word));
            return deser_help(list_word);
        }
        
        public TreeNode deser_help(List<String> li){
            if(li.get(0).equals("null")){
                li.remove(0);
                return null;
            }
            TreeNode res = new TreeNode(Integer.valueOf(li.get(0)));
            li.remove(0);
            res.left = deser_help(li);
            res.right = deser_help(li);
            return res;
        }
    }
    

    方法2:BFS

    序列化 —— 标准的 BFS

    • 我让 null 也入列,说它是真实节点也行,它有对应的"X",只是没有子节点入列
      考察出列节点
    • 如果不为 null ,则将它的值推入 res 数组,并将它的左右子节点入列
    • 如果是 null ,则将 ‘X’ 推入 res 数组
    • 出列…入列…直到队列为空,所有节点遍历完,res 数组也构建完,转成字符串

    序列化 代码

    const serialize = (root) => {
      const queue = [root]
      let res = []
      while (queue.length) {
        const node = queue.shift()
        if (node) { // 出列的节点 带出子节点入列
          res.push(node.val)
          queue.push(node.left) // 不管是不是null节点都入列
          queue.push(node.right)
        } else {
          res.push('X')
        }
      }
      return res.join(',')
    }
    
    

    反序列化——也是BFS:父节点出列,子节点入列

    下图是BFS得到的序列化字符串:

    image.png

    • 除了第一个 ROOT 值,其他节点值都是成对的,分别对应左右子节点
    • 我们从第二项开始遍历,每次考察两个节点值
    • 先构建的节点,是之后构建的节点的父亲,用一个 queue 暂存一下
    • queue 初始放入 ROOT 。父节点出列,找出子节点入列

    同时考察父节点,和两个子节点值

    • 出列的父节点,它对应到指针指向的左子节点值,和指针右边的右子节点值

    • 如果子节点值不为 ‘X’ ,则为它创建节点,并认父亲,并作为未来父亲入列

    • 如果子节点值为 ‘X’,什么都不做(父节点本来就有 null 子节点)

    • 所有父节点(真实节点)都会在 queue 里走一次

    image.png

    反序列化代码:

    const deserialize = (data) => {
      if (data == 'X') return null       // 就一个'X',只有一个null
      const list = data.split(',')       // 序列化字符串转成list数组
      const root = new TreeNode(list[0]) //首项是根节点值,为它创建节点
      const queue = [root] // 初始放入root,待会出列考察
      let cursor = 1       // 从list第二项开始遍历
      while (cursor < list.length) {      // 指针越界就退出
        const node = queue.shift()        // 父节点出列考察
        const leftVal = list[cursor]      // 获取左子节点值
        const rightVal = list[cursor + 1] // 获取右子节点值
        if (leftVal !== 'X') {   // 左子节点值是有效值
          const leftNode = new TreeNode(leftVal) // 创建节点
          node.left = leftNode   // 成为当前出列节点的左子节点
          queue.push(leftNode)   // 它是未来的爸爸,入列等待考察
        }
        if (rightVal !== 'X') {  // 右子节点值是有效值
          const rightNode = new TreeNode(rightVal) // 创建节点
          node.right = rightNode // 成为当前出列节点的右子节点
          queue.push(rightNode)  // 它是未来的爸爸,入列等待考察
        }
        cursor += 2              // 指针前进2位
      }
      return root // 返回根节点
    }
    
    

    完整实现:

    serialize() 序列化

    分析

    1. 使用 前序遍历 每个节点,如果不为null,则将值放入字符串中,如果为null,则放入"null"
    2. 题目有说明不让用类成员、变量,所以不能用递归处理遍历
    3. 由于前序遍历正好符合先进先出的原则,所以我们考虑用队列实现前序遍历
    4. 将当前节点加入队列,判断节点是否为空,若不为空则当前节点出队列并将值拼接到字符串后,然后将当前节点的左右子节点入队列
    5. 若当前节点为空,则没有子节点,直接拼接"null"字符串
    6. 重复4、5步骤,直到队列为空
    7. 对字符串进行处理后将其返回

    deserialize() 反序列化

    分析

    1. 将字符串截取并分割,返回字符串数组,数组中的每一项都是一个节点值
    2. 写一个方法,传节点值,返回该节点值的节点,如果值为"null",则返回null
    3. 此时我们可以将数组中的每一项看成一个节点
    4. 问题变成了如何将数组中的每一个节点按照原先的顺序连接起来
    5. 思路依然是用队列存储节点
    6. 遍历数组,如果某一个父节点的左右子节点都已找到,那我们就寻找下一个父节点
    7. 所以我们的队列应该存储尚未找到子节点的父节点
    8. 那如何判断左右子节点都已找到呢?
    1.我们可以用一个变量 isLeft 来存储是否为左节点   		
    2.isLeft默认为true
    3.让父节点连接左节点,然后对isLeft取反,此时isLeft为false
    4.然后连接右节点,再将isLeft取反,此时isLeft为true 
    5.如果isLeft又变回true,说明找到了左右子节点
    6.此时父节点应该变为队列的队首
    

    当数组遍历完成之后,返回数组第一个节点

    完成代码如下:

    public String serialize(TreeNode root) {
            StringBuilder res = new StringBuilder("["); // 拼接的字符串
    
            Queue<TreeNode> queue = new LinkedList(); // 保存节点队列
            queue.add(root); // 先从根节点开始
    
            while (!queue.isEmpty()) {
                TreeNode cur = queue.remove(); // 出队首节点
                if (cur == null) {
                    /*
                    如果节点为空,说明没有子节点,直接拼接null到字符串中
                     */
                    res.append("null,");
                } else {
                    /*
                    节点不为空,则将值加入字符串中
                    并且将其左右节点加入队列中
                     */
                    res.append(cur.val + ",");
                    queue.add(cur.left);
                    queue.add(cur.right);
                }
            }
    
            res.setLength(res.length() - 1); // 此时res中的值最后会多一个逗号,所以需要去掉最后一个字符
            res.append("]");
            return res.toString();
        }
    
    public TreeNode deserialize(String data) {
            // 将字符串截取并分割,返回字符串数组,数组中的每一项都是一个节点值
            String[] arr = data.substring(1, data.length() - 1).split(",");
    
            Queue<TreeNode> queue = new LinkedList<>(); // 存储尚未找到子节点的父节点
            TreeNode root = getNode(arr[0]); // 保存数组第一个节点,最终返回它
            TreeNode parent = root; // 临时的父节点变量
            boolean isLeft = true; // 判断是否已经找到了左右子节点
    
            for (int i = 1; i < arr.length; i++) {
                TreeNode cur = getNode(arr[i]); // 遍历数组
    
                if (isLeft) {
                    // 说明cur是parent的左节点
                    parent.left = cur;
                } else {
                    // 说明cur是parent的右节点
                    parent.right = cur;
                }
    
                if (cur != null) {
                    // 说明cur是一个父节点,但其左右子节点可能为空
                    queue.add(cur);
                }
    
                isLeft = !isLeft; // 取反
    
                if (isLeft) {
                    /*
                     如果此时为真,说明上面的cur是右节点,左右节点已经找到
                     队首出列,保存为新的父节点
                     */
                    parent = queue.poll();
                }
            }
    
            return root;
        }
    
        /**
         * 返回以val为值的节点
         * @param val 如果val为"null",则返回null
         * @return 返回以val为值的节点
         */
        private TreeNode getNode(String val) {
            if (val.equals("null")) {
                return null;
            }
    
            return new TreeNode(Integer.valueOf(val));
        }
    
    
  • 相关阅读:
    php header 跳转 ie问题
    Filter过滤不了Action的问题
    DB2中sql语句占位符问题
    LL(1)文法
    算法分析实验报告词法分析器
    大数据概述
    2016.9.30 编译原理词法分析器
    131有确定性的有穷自动机
    Python基础综合练习
    组合数据类型练习
  • 原文地址:https://www.cnblogs.com/hzcya1995/p/13308053.html
Copyright © 2011-2022 走看看