【LeetCode】二叉树的序列化与反序列化 | 验证二叉树的前序序列化

​🌠 作者:@阿亮joy.
🎆专栏:《阿亮爱刷题》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

👉二叉树的序列化与反序列化👈

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

在这里插入图片描述

二叉树的序列化本质上是对其值进行编码,更重要的是对其结构进行编码。可以通过遍历树来完成上述任务。众所周知,我们一般有两个策略来遍历树:广度优先搜索和深度优先搜索。

  • 广度优先搜索可以按照层次的顺序从上到下遍历所有的节点。
  • 深度优先搜索可以从一个根开始,一直延伸到某个叶,然后回到根,到达另一个分支。根据根节点、左节点和右节点之间的相对顺序,可以进一步将深度优先搜索策略区分为:
    • 前序遍历
    • 中序遍历
    • 后序遍历

深度优先遍历进行序列化与反序列化

在这里,我采用前序遍历的方式进行二叉树的序列化与反序列化,中序和后序的序列化与反序列化跟前序的序列化与反序列化类似。

二叉树的序列化是通过某种遍历方式遍历二叉树并将遍历的结果用字符串来表示,反序列化是通过字符串转成对应的二叉树。注意:序列化和反序列化的遍历方式要对应起来。

二叉树的序列化还需要能够清晰地表示二叉树的节点。在这里,我采用节点的值和下划线 _ 来表示一个非空节点,用 # 号来表示一个空节点。

在这里插入图片描述

反序列化首先要将序列化的节点解析出来,将其解析成能够清晰表示一个个节点的结果,然后根据解析出来的结果递归去构建二叉树。

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;
    }
};

在这里插入图片描述

👉总结👈

本篇博客主要讲解了什么是序列化与反序列化、二叉树的序列化与反序列化以及验证二叉树的前序序列化等等。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

  • 19
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
Rust 是一种现代的编程语言,特别适合处理内存安全和线程安全的代码。在 LeetCode 中,链表是经常出现的题目练习类型,Rust 语言也是一种非常适合处理链表的语言。接下来,本文将从 Rust 语言的特点、链表的定义和操作,以及 Rust 在 LeetCode 中链表题目的练习等几个方面进行介绍和讲解。 Rust 语言的特点: Rust 是一种现代化的高性能、系统级、功能强大的编程语言,旨在提高软件的可靠性和安全性。Rust 语言具有如下几个特点: 1. 内存安全性:Rust 语言支持内存安全性和原语级的并发,可以有效地预防内存泄漏,空悬指针以及数据竞争等问题,保证程序的稳定性和可靠性。 2. 高性能:Rust 语言采用了“零成本抽象化”的设计思想,具有 C/C++ 等传统高性能语言的速度和效率。 3. 静态类型检查:Rust 语言支持静态类型检查,可以在编译时检查类型错误,避免一些运行时错误。 链表的定义和操作: 链表是一种数据结构,由一个个节点组成,每个节点保存着数据,并指向下一个节点。链表的定义和操作如下: 1. 定义:链表是由节点组成的数据结构,每个节点包含一个数据元素和一个指向下一个节点的指针。 2. 操作:链表的常用操作包括插入、删除、查找等,其中,插入操作主要包括在链表首尾插入节点和在指定位置插入节点等,删除操作主要包括删除链表首尾节点和删除指定位置节点等,查找操作主要包括根据数据元素查找节点和根据指针查找节点等。 Rust 在 LeetCode 中链表题目的练习: 在 LeetCode 中,链表是常见的题目类型,而 Rust 语言也是一个非常适合练习链表题目的语言。在 Rust 中,我们可以定义结构体表示链表的节点,使用指针表示节点的指向关系,然后实现各种操作函数来处理链表操作。 例如,针对 LeetCode 中的链表题目,我们可以用 Rust 语言来编写解法,例如,反转链表,合并两个有序链表,删除链表中的重复元素等等,这样可以更好地熟悉 Rust 语言的使用和链表的操作,提高算法和编程能力。 总之,在 Rust 中处理链表是非常方便和高效的,而 LeetCode 中的练习也是一个非常好的机会,让我们更好地掌握 Rust 语言和链表数据结构的知识。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿亮joy.

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值