二叉树的序列化和反序列化
1. 二叉树序列化:
(a) 如果当前的树为空,则表示成X
(b) 如果当前的树不为空,则表示成(<left_sub_tree>)cur_num(<right_sub_tree>),其中,
<left_sub_tree>: 是左子树序列化后的结果
<right_sub_tree>: 是右子树序列化后的结果
cur_num: 当前节点的值
中序遍历可以得到序列化的结果。
2. 二叉树的反序列化
根据二叉树序列化,我们可以推导出这样的巴科斯范式:
T -> (T) num (T) | x
用T代表一棵树序列化的结果,| 表示T构成的为(T) num (T) 或者X,| 左边是对T的递归定义,右边规定了递归终止的边界条件。
T 的定义中,序列中的第一个字符要么是 X,要么是 (,所以这个定义是不含左递归的,当我们开始解析一个字符串的时候,如果开头是 X,我们就知道这一定是解析一个「空树」的结构,如果开头是 (,我们就知道需要解析 (T) num (T) 的结构,因此这里两种开头和两种解析方法一一对应,可以确定这是一个无二义性的文法
我们只需要通过开头的第一个字母是 X 还是 ( 来判断使用哪一种解析方法,所以这个文法是 LL(1) 型文法,如果你不知道什么是 LL(1) 型文法也没有关系,你只需要知道它定义了一种递归的方法来反序列化,也保证了这个方法的正确性——我们可以设计一个递归函数:
这个递归函数传入两个参数,带解析的字符串和当前当解析的位置 ptr,ptr 之前的位置是已经解析的,ptr 和 ptr 后面的字符串是待解析的。如果当前位置为 X 说明解析到了一棵空树,直接返回。否则当前位置一定是 (,对括号内部按照 (T) num (T) 的模式解析
class Codec {
public:
string serialize(TreeNode* root) {
if (!root) {
return "X";
}
auto left = "(" + serialize(root->left) + ")";
auto right = "(" + serialize(root->right) + ")";
return left + to_string(root->val) + right;
}
inline TreeNode* parseSubtree(const string &data, int &ptr) {
++ptr; // 跳过左括号
auto subtree = parse(data, ptr);
++ptr; // 跳过右括号
return subtree;
}
inline int parseInt(const string &data, int &ptr) {
int x = 0, sgn = 1;
if (!isdigit(data[ptr])) {
sgn = -1;
++ptr;
}
while (isdigit(data[ptr])) {
x = x * 10 + data[ptr++] - '0';
}
return x * sgn;
}
TreeNode* parse(const string &data, int &ptr) {
if (data[ptr] == 'X') {
++ptr;
return nullptr;
}
auto cur = new TreeNode(0);
cur->left = parseSubtree(data, ptr);
cur->val = parseInt(data, ptr);
cur->right = parseSubtree(data, ptr);
return cur;
}
TreeNode* deserialize(string data) {
int ptr = 0;
return parse(data, ptr);
}
};
3.复杂度分析
时间复杂度:序列化时做了一次遍历,渐进时间复杂度为 O(n)O(n)。反序列化时,在解析字符串的时候 ptr 指针对字符串做了一次顺序遍历,字符串长度为 O(n)O(n),故这里的渐进时间复杂度为 O(n)O(n)。
空间复杂度:考虑递归使用的栈空间的大小,这里栈空间的使用和递归深度有关,递归深度又和二叉树的深度有关,在最差情况下,二叉树退化成一条链,故这里的渐进空间复杂度为 O(n)O(n)。
摘自:https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/solution/er-cha-shu-de-xu-lie-hua-yu-fan-xu-lie-hua-by-le-2