👉二叉树的序列化与反序列化👈
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
二叉树的序列化本质上是对其值进行编码,更重要的是对其结构进行编码。可以通过遍历树来完成上述任务。众所周知,我们一般有两个策略来遍历树:广度优先搜索和深度优先搜索。
广度优先搜索可以按照层次的顺序从上到下遍历所有的节点。
深度优先搜索可以从一个根开始,一直延伸到某个叶,然后回到根,到达另一个分支。根据根节点、左节点和右节点之间的相对顺序,可以进一步将深度优先搜索策略区分为:
前序遍历
中序遍历
后序遍历
深度优先遍历进行序列化与反序列化
在这里,我采用前序遍历的方式进行二叉树的序列化与反序列化,中序和后序的序列化与反序列化跟前序的序列化与反序列化类似。
二叉树的序列化是通过某种遍历方式遍历二叉树并将遍历的结果用字符串来表示,反序列化是通过字符串转成对应的二叉树。注意:序列化和反序列化的遍历方式要对应起来。
二叉树的序列化还需要能够清晰地表示二叉树的节点。在这里,我采用节点的值和下划线 _ 来表示一个非空节点,用 # 号来表示一个空节点。
反序列化首先要将序列化的节点解析出来,将其解析成能够清晰表示一个个节点的结果,然后根据解析出来的结果递归去构建二叉树。
class Codec { private: // str是二叉树的序列化结果,注意一定要加引用 void _serialize(TreeNode* root, string& str) { // 如果该节点为空,则用#号来表示 if(root == nullptr) { str += "#"; return; } // 如果是该节点不为空,则该节点桶节点的值加下划线来表示 str += to_string(root->val) + "_"; // 递归去求左右子树的结果 _serialize(root->left, str); _serialize(root->right, str); } // i一定是要引用修饰 TreeNode* _deserialize(const vector<string>& preRet, size_t& i) { // 如果是#号,说明该节点为空,返回nullptr if(preRet[i] == "#") { return nullptr; } // 先构建根 TreeNode* root = new TreeNode(stoi(preRet[i])); // 构建左子树 ++i; // i需要自加,才能够拿到表示左右孩子的字符串 root->left = _deserialize(preRet, i); // 构建右子树 ++i; root->right = _deserialize(preRet, i); return root; } public: string serialize(TreeNode* root) { string str; _serialize(root, str); return str; } TreeNode* deserialize(string data) { vector<string> preRet; string str; // 将前序遍历的字符串解析成一个个节点的字符串 for(auto ch : data) { if(ch == '_') { preRet.push_back(str); str.clear(); } else if(ch == '#') preRet.push_back("#"); else str += ch; } size_t i = 0; return _deserialize(preRet, i); } };
广度优先遍历进行序列化与反序列化
二叉树的广度优先遍历就是层序遍历,就是一层一层地遍历二叉树的节点。
class Codec { public: string serialize(TreeNode* root) { // 如果树的根节点为空,直接返回"#"即可 if(root == nullptr) { return "#"; } // str是二叉树层序遍历序列化的结果 string str; queue<TreeNode*> q; // 根节点入队列 q.push(root); while(!q.empty()) { // 注:nullptr也要入队列,这样才能够表示 // 哪个节点没有左孩子或者右孩子 TreeNode* front = q.front(); q.pop(); // 当前节点为空,直接在str的尾部插入'#'即可 // 当前节点不为空,需要在str尾部插入节点的值和下划线_ // 还需要将该节点的左右孩子入队列(即使没有左右孩子) if(front == nullptr) { str += '#'; } else { str += to_string(front->val) + '_'; q.push(front->left); q.push(front->right); } } return str; } TreeNode* deserialize(string data) { if(data == "#") return nullptr; vector<string> ret; string str; // 先将层序遍历的字符串解析成一个个节点的字符串 // 这样更加方便我们进行反序列化 for(auto ch : data) { // ch等于下划线_ ,说明str已经存储了一个节点的值 // ch等于#号,说明该节点是空节点 // 除此之外,都是节点的值的部分 if(ch == '_') { ret.push_back(str); str.clear(); } else if(ch == '#') ret.push_back("#"); else str += ch; } // 根节点先入队列 queue<TreeNode*> q; TreeNode* root = new TreeNode(stoi(ret[0])); q.push(root); int i = 1; while(!q.empty()) { TreeNode* Node = q.front(); q.pop(); // 如果ret[i] == "#" 时,说明其没有左孩子或者右孩子 // 就不需要new节点了,如果new节点时节点的左右孩子已经弄成了nullptr if(ret[i] != "#") { Node->left = new TreeNode(stoi(ret[i])); q.push(Node->left); } // 不管是nullptr还是不是nullpt,i都要自加 // 这样才能拿到下一个节点的信息 ++i; if(ret[i] != "#") { Node->right = new TreeNode(stoi(ret[i])); q.push(Node->right); } ++i; } return root; } };
👉验证二叉树的前序序列化👈
序列化二叉树的一种方法是使用 前序遍历 。当我们遇到一个非空节点时,我们可以记录下这个节点的值。如果它是一个空节点,我们可以使用一个标记值记录,例如 #。
我们可以定义一个概念,叫做槽位。一个槽位可以被看作「当前二叉树中正在等待被节点填充」的那些位置。
二叉树的建立也伴随着槽位数量的变化。每当遇到一个节点时:
如果遇到了空节点,则要消耗一个槽位;
如果遇到了非空节点,则除了消耗一个槽位外,还要再补充两个槽位。
此外,还需要将根节点作为特殊情况处理。
我们使用栈来维护槽位的变化。栈中的每个元素,代表了对应节点处剩余槽位的数量,而栈顶元素就对应着下一步可用的槽位数量。当遇到空节点时,仅将栈顶元素减 1 ;当遇到非空节点时,将栈顶元素减 1 后,再向栈中压入一个 2。无论何时,如果栈顶元素变为 0,就立刻将栈顶弹出。
遍历结束后,若栈为空,说明没有待填充的槽位,因此是一个合法序列;否则若栈不为空,则序列不合法。此外,在遍历的过程中,若槽位数量不足,则序列不合法。
class Solution { public: bool isValidSerialization(string preorder) { int n = preorder.size(); int i = 0; stack<int> st; st.push(1); while(i < n) { // 遍历过程槽位为空,说明序列不合法 if(st.empty()) return false; // 空节点需要消耗一个槽位 // 非空节点也需要消耗一个槽位,同时还会增加两个槽位 // 栈顶元素为0时,需要将栈顶元素弹出 if(preorder[i] == ',') ++i; else if(preorder[i] == '#') { st.top() -= 1; if(st.top() == 0) { st.pop(); } ++i; } else { // 读取一个数字,因为数组有可能是大于等于10的 while(i < n && preorder[i] != ',') { ++i; } st.top() -= 1; if(st.top() == 0) { st.pop(); } st.push(2); ++i; } } return st.empty(); } };
空间复杂度优化:如果把栈中元素看成一个整体,即所有剩余槽位的数量,也能维护槽位的变化。因此,我们可以只维护一个计数器,代表栈中所有元素之和,其余的操作逻辑均可以保持不变。
class Solution { public: bool isValidSerialization(string preorder) { int n = preorder.length(); int i = 0; int slots = 1; while (i < n) { if (slots == 0) return false; if (preorder[i] == ',') { i++; } else if (preorder[i] == '#') { slots--; i++; } else { // 读一个数字 while (i < n && preorder[i] != ',') { i++; } slots++; // slots = slots - 1 + 2 } } return slots == 0; } };
👉总结👈
本篇博客主要讲解了什么是序列化与反序列化、二叉树的序列化与反序列化以及验证二叉树的前序序列化等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️