leetcode297 树的序列化与反序列化

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

20201020更新
引用b站扶我起来继续study
主要思路:
序列化:把一棵二叉树序列化为一个字符串序列:按照先序遍历dfs序列化每一个节点,空节点的情况,非空节点的情况
反序列化:
通过序列化的序列,按照先序遍历构建一颗二叉树:
node->val=num; // 需要一个idx定位数字符号,len定位’,’
node->left=dfs2();
node->right=def2();
return node;

代码如下:

// Encodes a tree to a single string.
  string serialize(TreeNode *root)
  {
    string res;
    // 如果要返回字符,那么:
    // char* p=new char[res.size()+1];
    // strcpy(p,res.c_str());
    dfs1(root, res);
    // return p;
    return res;
  }

  void dfs1(TreeNode *root, string &res)
  {
    // 空节点
    if (!root)
    {
      //#
      res += "#,";
      return;
    }
    // 序列化采用的是先序遍历
    // 非空节点
    res += to_string(root->val) + ",";
    // 递归
    dfs1(root->left, res);
    dfs1(root->right, res);
  }

  // Decodes your encoded data to tree.
  // 反序列化就是传入一个字符数组构建一颗二叉树
  TreeNode *deserialize(string data)
  {
    int idx = 0; // 序列化后为:1, 2, #, #, 3, 4, #, #, 5, #, #, idx指向1的位置
    return dfs2(data, idx);
  }

  TreeNode *dfs2(string data, int &idx)
  {
    // 确定长度
    int len = idx;
    while (data[len] != ',')
      len++; // idx指向数字1,len指向数字1后的","
             // 然后再用idx与len来解析字符串str
    // 如果空节点
    if (data[idx] == '#')
    {
      idx = len + 1; // 指向下一个数字字符
      return NULL;
    }
    // 非空节点
    // 计算数字字符的数值
    int num = 0;
    // 考虑符号正负
    int sign = 1;
    if (idx < len && data[idx] == '-')
      sign = -1, idx++; // idx指向-后的数字
    for (int i = idx; i < len; i++)
      num = num * 10 + data[i] - '0';
    num *= sign;

    // idx走到下一个数字字符
    idx = len + 1;

    // 构建二叉树
    auto root = new TreeNode(num);
    root->left = dfs2(data, idx);
    root->right = dfs2(data, idx);

    return root;
  }

方法1:DFS(递归)

  • 递归遍历一棵树,只关注当前单个节点就好,剩下的工作交给递归完成
    • “serialize 函数,麻烦帮我序列化我的左右子树,我等你的返回结果,再追加到我身上
  • 选择前序遍历是因为「根|左|右根∣左∣右」的顺序在反序列化时,更易定位出根节点值
  • 遇到 null 节点也要翻译成一个特殊符号,反序列化时才知道这里对应 null 节点
const serialize = (root) =>{
  if(root==null) return 'X,' // 遇到null节点
  const leftSerialized = serialize(root.left) // 左子树的序列化字符串
  const rightSerialized = serialize(root.right) // 右子数的序列化字符串
  return root.val +','+leftSerialized + rightSerialized // 根|左|右
}

  • 反序列化也是递归

    • 序列化时是前序遍历,所以序列化后的字符串呈现这样的排列:
      “根|(根|(根|左|右)|(根|左|右))|(根|(根|左|右)|(根|左|右))”
  • 写一个构建树的辅助函数 buildTree,接收的是由序列化字符串转成 list 数组

  • 按照前序遍历的顺序构建:先构建根节点,再构建左子树、右子树

  • 将 list 数组的首项弹出,它是当前子树的 root 节点

    • 如果它为 ‘X’ ,返回 null
    • 如果它不为 ‘X’,则为它创建节点,并递归调用 buildTree 构建左右子树,构建完当前子树构建,向上返回
  • 反序列化代码

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;
}
const deserialize = (data) => {
  const list = data.split(',')       // 转成list数组
  return buildTree(list)             // 构建树,dfs的入口
}
  • 按照大佬思路翻译的DFS C++版本
class Solution{
  public:
    // Encodes a tree to a single string.
    static string serialize(TreeNode* root){
      if(root == NULL) return "X,";
      string leftNode = serialize(root->left);
      string rightNode = serialize(root->right);
      return to_string(root->val) + ',' + leftNode + rightNode;
    }

    // Decodes your encoded data to tree.
    static TreeNodes* deserialize(string data){
      list<string> list = split(data, ',');
      TreeNode* res = buildTree(list);
      return res;
    }

    static TreeNode* buildTree(list<string>& strList){
      string strtmp = strList.front();
      strList.pop_front();
      if(strtmo == 'X') return NULL;
      TreeNode* node = new TreeNode(stoi(strtmp));
      node->left = buildTree(strList);
      node->right = buildTree(strList);
      return node;
    }

    static list<string> split(string& str, char c){
      list<string> res;
      for(int lastpos =-1, pos = 0; pos<str.length(); pos++){
        if(str[pos]==c){
          res.push_back(str.substr(++lastpos, pos - lastpos));
          lastpos = pos; // 用整数代表字符串的索引位置
        }
      }
      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
    • 除了第一个是根节点的值,其他节点值都是成对的,分别对应左右子节点
    • 从第二项开始遍历,每次考察两个节点值
    • 队列初始推入root节点。父节点出列,找出子节点入列
    • 出列的父节点,它对应到指针指向的左子节点值,和指针右边的右子节点值
      • 如果子节点值不为 ‘X’,则为它创建节点,并认父亲,并作为未来父亲入列
      • 如果子节点值为 ‘X’,什么都不做即可
    • 所有父节点(真实节点)都会在队列里走一遍
  • 反序列化代码
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(keftNode)            // 它是未来的爸爸,入列等待考察
    }
    if(rightVal !== 'X'){               // 右子节点值是有效值
      const leftNode = new TreeNode(leftVal)  // 创建节点
      node.left = rightNode            // 成为当前出列节点的右子节点
      queue.push(rightNode)            // 它是未来的爸爸,入列等待考察
    }
    cursor+=2                          // 指针前进2位
  }
  return root // 返回根节点

}
  • 按照大佬思路翻译的BFS C++版本
class Codec{
  public:
  // 二叉树序列化成字符串
  string serialize(TreeNode* root){
    rserialize(root);
    return deserializeRes;
  }
  // bfs遍历二叉树
   void rserialize(TreeNode* root){
     qtr.push(root);
     while(!qtr.empty()){
       TreeNode* top = qtr.front();
       qtr.pop();
       if(top==NULL) deserializeRes.append("null,");
       else{
         deserizeRes.append(to_string(top->val));
         deserizeRes.append(",");
         qtr.push(top->left);
         qtr.push(top->right);
       }
     }
   }

   // bfs构造二叉树
   TreeNode* rdeserialize(const vector<string>& tokens){
     if(tokens[0]=="null") return NULL;
     TreeNode* root = new TreeNode(stoi(tokens[0], nullptr, 10));
     int cursor = 1;
     qtr.push(root);
     while(cursor < tokens.size() && !qtr.empty()){
       TreeNode* top = qtr.front();
       qtr.pop();
       string lNode = tokens[cursor];
       string rNode = tokens[cursor+1];
       if(lNode!="null"){
         top->left=new TreeNode(stoi(lNode, nullptr, 10));
         qtr.push(top->left);
       }
       if(rNode!="null"){
         top->right=new TreeNode(stoi(rNode, nullptr, 10));
         qtr.push(top->right);
       }
       cursor+=2;
     }
     return root;
   }


  private:
    string deserializeRes = "";
    queue<TreeNode*> qtr;
}

public class Codec
    {
      // 把一颗二叉树序列化成字符串
      public string serialize(TreeNode root) {}

      // 把字符串反序列化成二叉树
      public TreeNode deserialize(string data) {}
    }

二、前序遍历解法

    void traverse(TreeNode root)
    {
      if(root==NULL) return;

      // 前序遍历的代码
      traverse(root.left);
      traverse(root.right);
    }

    // 如下伪代码
    LinkedList<Integer> res;
    void traverse(TreeNode root){
      if(root==null) {
        res.addList(-1);
        return;
      }

      /*前序遍历位置*/
      res.addList(root.val);

      traverse(root.left);
      traverse(root.right);
    }
2.1 那么将二叉树打平到一个字符串中也是完全一样的
    string SEP = ',';
    // 代表null空指针的字符
    string NULL = "#";
    // 用于拼接字符串
    StringBuilder sb = new StringBuilder();

    /*将二叉树打平为字符串*/
    void traverse(TreeNode root, StringBuilder sb){
      if(root==null){
          sb.append(null).append(SEP);
          return;
      }

      /*前序遍历位置*/
      sb.append(root.val).append(SEP);

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

    // 至此,我们已经可以写出序列化函数 serialize 的代码了:
    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);
    }
2.3现在,思考一下如何写 deserialize 函数,将字符串反过来构造二叉树
    // 首先我们可以把字符串转化成列表
    string data = "1,2,#,4,#,#,3,#,#,";
    string[] nodes = data.split(",");
    // 这样,nodes 列表就是二叉树的前序遍历结果
    // 问题转化为:如何通过二叉树的前序遍历结果还原一棵二叉树
    // PS:一般语境下,单单前序遍历结果是不能还原二叉树结构的
    // 因为缺少空指针的信息,至少要得到前、中、后序遍历中的两种才能还原二叉树
    // 但是这里的 node 列表包含空指针的信息,所以只使用 node 列表就可以还原二叉树
    // 根据我们刚才的分析,nodes 列表就是一棵打平的二叉树:
    // 那么,反序列化过程也是一样,先确定根节点 root
    // 然后遵循前序遍历的规则,递归生成左右子树即可:

    /* 主函数,将字符串反序列化为二叉树结构 */
    TreeNode deserializez(string data){
      // 将字符串转化为列表
      LinkedList<string> nodes = new LinkedList<>();
      for(string s : data.split(SEP)){ // SEP为,
        nodes.addList(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 = deserialiize(nodes);

      return root;
    }
    // 我们发现,根据树的递归性质,nodes 列表的第一个元素就是一棵树的根节点
    // 所以只要将列表的第一个元素取出作为根节点,剩下的交给递归函数去解决即可
3后序遍历法
    // ==3.1二叉树后续遍历框架:==
    void traverse(TreeNode root){
      if(root==null) return;
      traverse(root.left);
      traverse(root.right);

      // 后序遍历的代码
    }

    // 明白了前序遍历的解法,后序遍历就比较容易理解了
    // 首先实现 serialize 序列化方法,只需要稍微修改辅助方法即可:
    /* 辅助函数,将二叉树存入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 的拼接操作放到了后续遍历的位置
    // 后序遍历导致结果的顺序发生变化:
    // 关键的难点在于,如何实现后序遍历的 deserialize 方法呢
    // 是不是也简单地将关键代码放到后序遍历的位置就行了呢:

    /* 辅助函数,通过 nodes 列表构造二叉树 */
    TreeNode deserialize(LinkedList<string> nodes){
      if(nodes.isEmpty()) return null;
      root.left = deserialize(nodes);
      root.right = descrialize(nodes);

      /**后序遍历位置**/
      string first = nodes.removeFirst();
      if(first,equals(null)) return null;
      TreeNode root = new TreeNode(Integer.parseInt(first));

      return root;
    }

    // 回想刚才我们前序遍历方法中的 deserialize 方法,第一件事情在做什么
    // deserialize 方法首先寻找 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.addList(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;
    }
    // 至此,后续遍历实现的序列化、反序列化方法也都实现了
4中序遍历解法
   // 先说结论,中序遍历的方式行不通,因为无法实现反序列化方法deserialize
   // 序列化方法 serialize 依然容易,只要把字符串的拼接操作放到中序遍历的位置就行了:
   // 但是,我们刚才说了,要想实现反序列方法,首先要构造 root 节点。前序遍历得到的 nodes 列表中,第一个元素是 root 节点的值
   // 后序遍历得到的 nodes 列表中,最后一个元素是 root 节点的值
   // 你看上面这段中序遍历的代码,root 的值被夹在两棵子树的中间,也就是在 nodes 列表的中间
   // 我们不知道确切的索引位置,所以无法找到 root 节点,也就无法进行反序列化
5层级遍历解法
   // 先写出层级遍历二叉树的代码框架:
   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.parseln(root.val);

       if(cur.left!=null){
         q.offer(cur.left);
       }
       if(cur.right!=null){
         q.offer(cur.right);
       }
     }
   }

   // 上述代码是标准的二叉树层级遍历框架,从上到下,从左到右打印每一层二叉树节点的值,可以看到,队列 q 中不会存在 null 指针
   // 不过我们在反序列化的过程中是需要记录空指针 null 的,所以可以把标准的层级遍历框架略作修改:

   void traverse(TreeNode root){
     if(root==null) return;
     // 初始化队列,将 root 加入队列
     Queue<TreeNode> q = new LinkedList<>();
     q.offer(root);

     while(!q.isEmpty()){
       TreeNode cur = q.poll();

       /* 层级遍历代码位置 */
       if(cur==null) continue;
       System.out.parseInt(root.val);

       q.offer(cur.left);
       q.offer(cur.right);
     }
   }

   // 这样也可以完成层级遍历,只不过我们把对空指针的检验从「将元素加入队列」的时候改成了「从队列取出元素」的时候。
5.1那么我们完全仿照这个框架即可写出序列化方法:
    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();
    }

    // 可以看到,每一个非空节点都会对应两个子节点
    // 那么反序列化的思路也是用队列进行层级遍历,同时用索引 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;
    }
    // 这段代码可以考验一下你的框架思维。仔细看一看 for 循环部分的代码,发现这不就是标准层级遍历的代码衍生出来的嘛:
    while (!q.isEmpty()) {
    TreeNode cur = q.poll();

    if (cur.left != null) {
        q.offer(cur.left);
    }

    if (cur.right != null) {
        q.offer(cur.right);
    }
    }
    // 只不过,标准的层级遍历在操作二叉树节点 TreeNode,而我们的函数在操作 nodes[i],这也恰恰是反序列化的目的嘛
    // 到这里,我们对于二叉树的序列化和反序列化的几种方法就全部讲完了

引用来自labuladong

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值