【Leetcode】428. Serialize and Deserialize N-ary Tree

题目地址:

https://leetcode.com/problems/serialize-and-deserialize-n-ary-tree/description/

给定一棵多叉树,要求实现其序列化和反序列化函数。多叉树节点的定义如下:

class Node {
public:
  int val;
  vector<Node *> children;
  Node() {}
  Node(int _val) {
    val = _val;
  }
  Node(int _val, vector<Node *> _children) {
    val = _val;
    children = _children;
  }
};

给个多叉树的例子:
在这里插入图片描述
序列化的方式有很多种,我们这里采用一种“括号层次”的方法,思路是DFS。在序列化的时候,我们规定,对于每个节点,如果这个节点有孩子,那么其孩子都在这个节点紧跟的括号里,并且这些孩子的“括号深度”都相等。这样在反序列化的时候,括号深度每进一步,也就说明了走到了下一层。如上图的多叉树,其序列化之后就是1(2(5,6)3(7)4),由于 1 1 1后面跟着一个括号,所以我们就知道了括号里深度相等的数字都是其儿子,即 2 , 3 , 4 2,3,4 2,3,4都是其儿子;类似的,由于 2 2 2后面跟着一个括号,从而我们知道 5 , 6 5,6 5,6是其儿子。同一深度的儿子需要区分开来,所以我们中间加个逗号。那么序列化函数是很好写的:

string serialize(Node *root) {
  string s;
  dfs(root, s);
  return s;
}

void dfs(Node *cur, string &s) {
  if (!cur) return;
  s += to_string(cur->val) + ",";
  if (cur->children.empty()) return;

  if (s.back() == ',') s.pop_back();
  s += '(';
  for (auto *ne: cur->children) dfs(ne, s);
  if (s.back() == ',') s.pop_back();
  s += ')';
}

上面要注意几个细节,为了节省空间,我们在节点有孩子或者该节点是其父亲的最后一个孩子的情况下,将其后面的逗号去掉了。同时,如果一个节点有孩子,那么其后面会接一对封闭括号,那么这个节点与其右边的兄弟节点也不需要逗号的分隔就可以区分。当然,当这个节点是叶子的时候,后面需要强制加逗号,否则无法区分其和其兄弟节点。这造成了一个副作用,如果整棵树只有一个树根,序列化之后会以逗号结尾。但以逗号结尾也意味着整棵树只有一个树根,我们可以在序列化的时候特判。空树也是一种特殊情况,其也不以括号结尾。别的情况,序列化的结果都是以右括号结尾的。

序列化的过程比较复杂。首先特判掉两个特殊情况,即空串(对应空树),和以逗号结尾(对应整棵树只有一个树根节点)。对于一般情况,算法如下:
1、开一个栈,同时开始遍历字符串;
2、如果遇到数字,那么将整个数字截取出来,new出节点,这个节点要么是树根(从而没有父亲),也就是当栈空的时候,那么就将该节点入栈;如果栈不空,那么我们保持栈顶为DFS树的上一层节点,即当前节点为其儿子,从而加入其儿子列表中;
3、如果遇到左括号,此时说明要进入下一层了,这里有个关键操作,如果当前栈顶的儿子列表不空,我们需要将其最后一个儿子入栈。这是因为我们要保持栈顶是当前遍历的节点的父亲。如果栈顶儿子列表为空,那么下一层的节点都是栈顶的子孙,我们不需要做什么;但是如果栈顶儿子列表不空,进入下一层的时候,其实下一层节点的父亲就是栈顶的最后一个儿子。以上面的图为例,其序列化为1(2(5,6)3(7)4)。当遍历到3(的这个左括号的时候,当前栈顶还是 1 1 1(因为我们要保持栈顶为当前遍历的节点的父亲),那么进入下一层的时候,父亲应该变成 3 3 3了,而 3 3 3恰好是 3 3 3自己的父亲的最后一个孩子。
4、如果遇到右括号,说明当前栈顶的所有孩子都遍历完了,我们要回到上一层,栈pop。这里需要注意,当栈顶就是树根的时候,pop掉树根之后就找不到了(事实上最后一个pop的元素也必然是树根),所以我们干脆将栈pop的元素直接记为树根变量,那么全pop完之后,那个树根变量就是真正的树根。

代码如下:

class Codec {
public:
  // Encodes a tree to a single string.
  string serialize(Node *root) {
    string s;
    dfs(root, s);
    return s;
  }

  void dfs(Node *cur, string &s) {
    if (!cur) return;
    s += to_string(cur->val) + ",";
    if (cur->children.empty()) return;

    if (s.back() == ',') s.pop_back();
    s += '(';
    for (auto *ne: cur->children) dfs(ne, s);
    if (s.back() == ',') s.pop_back();
    s += ')';
  }

  // Decodes your encoded data to tree.
  Node *deserialize(string s) {
    // 两个特例
    if (s.empty()) return nullptr;
    if (s.back() == ',') return new Node(stoi(s.substr(0, s.size() - 1)));
    Node *root = nullptr;
    stack<Node *> stk;
    for (int i = 0; i < s.size(); i++) {
      if (s[i] == '(') {
        auto children = stk.top()->children;
        if (children.size()) stk.push(children.back());
      } else if (isdigit(s[i])) {
        int j = i;
        while (j < s.size() && isdigit(s[j])) j++;
        Node *node = new Node(stoi(s.substr(i, j - i)));
        if (stk.size()) stk.top()->children.push_back(node);
        else stk.push(node);
        i = j - 1;
	  } else if (s[i] == ')') {
        root = stk.top();
        stk.pop();
      }
    }

    return root;
  }
};

序列化和反序列化的时间复杂度都是 O ( n ) O(n) O(n),空间都是 O ( h ) O(h) O(h)

还有一种比较简单的BFS的序列化方法。我们直接对树BFS,每个节点对应的序列化的部分要含其值和其孩子个数。例如上面的树的序列化即为1,3,2,2,3,1,4,0,5,0,6,0,7,0,,开头的1,3表示BFS的时候当前节点值为 1 1 1,有 3 3 3个孩子,这样由BFS性质,我们就知道了其三个孩子就是后面的2,2,3,1,4,0,即值为 2 , 3 , 4 2,3,4 2,3,4的三个节点。这样就能反序列化的时候用BFS再将树构造出来。代码如下:

class Codec {
 public:
  // Encodes a tree to a single string.
  string serialize(Node* root) {
    string s;
    if (!root) return s;
    queue<Node*> q;
    q.push(root);
    while (q.size()) {
      auto* t = q.front();
      q.pop();
      s += to_string(t->val) + "," + to_string(t->children.size()) + ",";
      for (auto* ne : t->children) q.push(ne);
    }
    return s;
  }

  // Decodes your encoded data to tree.
  Node* deserialize(string s) {
    if (s.empty()) return nullptr;
    int idx = 0;
    auto [val, cnt] = get_num(s, idx);
    Node* root = new Node(val);
    queue<pair<Node*, int>> q;
    q.push({root, cnt});
    while (idx < s.size()) {
      auto t = q.front();
      q.pop();
      Node* node = t.first;
      int cnt = t.second;
      for (int i = 0; i < cnt; i++) {
        auto [val, child_cnt] = get_num(s, idx);
        auto* child = new Node(val);
        node->children.push_back(child);
        q.push({child, child_cnt});
      }
    }

    return root;
  }

  // 从i开始截取两个数,第一个数为节点的值,第二个为其孩子数量
  pair<int, int> get_num(string& s, int& i) {
    int j = i;
    while (j < s.size() && isdigit(s[j])) j++;
    int val = stoi(s.substr(i, j - i));
    i = j + 1;
    j = i;
    while (j < s.size() && isdigit(s[j])) j++;
    int cnt = stoi(s.substr(i, j - i));
    i = j + 1;
    return {val, cnt};
  }
};

时空复杂度 O ( n ) O(n) O(n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值