C++--二叉树经典例题

news2024/11/20 3:23:31

       本文,我们主要讲解一些适合用C++的数据结构来求解的二叉树问题,其中涉及了二叉树的遍历,栈和队列等数据结构,递归与回溯等知识,希望可以帮助你进一步理解二叉树。

目录​​​​​​​

1.二叉树的层序遍历

2.二叉树的公共祖先

3.二叉搜索树与双向链表

4.二叉树的创建(前序+中序,后序+中序)

前序+中序:

中序+后序:

5.二叉树的三种迭代法遍历


1.二叉树的层序遍历

题目链接:二叉树的层序遍历

思路分析

         这个题目的难点是如何控制层序遍历的同时还能保持入队操作,我们知道,二叉树的的层序遍历一般和队列联系在一起使用,包括一些bfs的习题也经常以队列、栈作为遍历的容器,本题,我们需要思考如何能够遍历某一层的同时,将该层的下一层的节点保存下来,同时还需要将该层的节点按从左到右的顺序输出,显然,我们需要将每一层的节点聚在一起保存,这样才能保证输出的正确性,我们可以提供一个输出的变量len,表示该层需要输出几个节点,然后将该层的节点输出的同时把它的左右孩子都入队,这样一来,就能保证每一层的节点都聚在一起并且按从左到右的顺序排列,之后保存输出即可。

AC代码:

class Solution {
public:
    queue<TreeNode*> q;
    vector<vector<int>> ans;
    vector<vector<int>> levelOrder(TreeNode* root) {
        if(!root)
          return ans;
        vector<int> cot;
        q.push(root);
        int len=1;
        while(!q.empty())
        {
            while(len--)
            {
                TreeNode* tmp=q.front();
                q.pop();
                if(tmp->left!=nullptr) q.push(tmp->left);
                if(tmp->right!=nullptr) q.push(tmp->right);
                cot.push_back(tmp->val);
            }
            len=q.size();
            ans.push_back(cot);
            cot.clear();
        }        
              
        return ans;
    }
};

2.二叉树的公共祖先

题目链接:二叉树的公共祖先

思路分析

        难点在于如何在找到两个节点的条件下,回溯双方的祖先节点来确定最近公共祖先,这里仅提供其中一个思路,我们可以保存寻找路径,然后让两个路径中较长的路径(也就是离公共祖先节点较远的)逐渐向上走,直到两条路径交合,交合点就是最近公共祖先

class Solution {
public:
    bool isexist(TreeNode*root,TreeNode *key)
    {
        if(root==nullptr)
          return false;
        else if(root==key)
          return true;
        else
          return isexist(root->left,key)||isexist(root->right,key);
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        stack<TreeNode*>ppath;
        stack<TreeNode*>qpath;
        TreeNode*cur=root;
        ppath.push(root);
        qpath.push(root);
        //寻找p的路径
        while(cur)
        {
            if(cur==p)
                break;
            else if(isexist(cur->left,p)) 
            {
                ppath.push(cur->left);
                cur=cur->left;
            }
            else if(isexist(cur->right,p))
            {
                ppath.push(cur->right);
                cur=cur->right;
            }
        }
        //寻找q的路径
        cur=root;
        while(cur)
        {
            if(cur==q)
                break;
            else if(isexist(cur->left,q)) 
            {
                qpath.push(cur->left);
                cur=cur->left;
            }
            else if(isexist(cur->right,q))
            {
                qpath.push(cur->right);
                cur=cur->right;
            }
        }
        //开始寻找
        TreeNode *ans=nullptr;
        while(ppath.size()&&qpath.size())
        {
            if(ppath.size() > qpath.size())
                ppath.pop();
            else if(ppath.size() < qpath.size())
                qpath.pop();
            else if(ppath.size() == qpath.size())
            {
                if(ppath.top()==qpath.top())
                {
                    ans=ppath.top();
                    break;
                }
                else
                {
                    ppath.pop();
                    qpath.pop();
                }
            }
        }
        return ans;
    }
};

3.二叉搜索树与双向链表

题目链接:二叉搜索树与双向链表

思路分析

       

       本体的难点在于,如何在原树上做修改而不影响接下来的操作,当然,暴力也可以做,但是这里要求我们在原树上直接做修改,所以,这里不解释暴力做法,我们就按题目规定的思路来,我们知道,二叉搜索树最左端的元素一定最小,最右端的元素一定最大,因此二叉搜索树的中序遍历就是一个递增序列,我们只要对它中序遍历就可以组装成为递增双向链表。

      我们可以使用一个指针pre指向当前结点root的前继。(例如root为指向10的时候,preNode指向8),对于当前结点root,有root->left要指向前继pre(中序遍历时,对于当前结点root,其左孩子已经遍历完成了,此时root->left可以被修改。);同时,pr->right要指向当前结点(当前结点是pre的后继),此时对于pre结点,它已经完全加入双向链表。

class Solution {
public:
    TreeNode *head=nullptr;
	TreeNode*pre=nullptr;
    TreeNode* Convert(TreeNode* pRootOfTree) {
 
		if(pRootOfTree==nullptr)
		   return nullptr;
		Convert(pRootOfTree->left);//找到最左节点,也就是最小的那个点

		if(head==nullptr)//此时是第一个最小值,也就是头结点
		{
            head=pRootOfTree;
			pre=pRootOfTree;
		}
		else
		{
			pre->right=pRootOfTree;
			pRootOfTree->left=pre;
			pre=pRootOfTree;
		}
		Convert(pRootOfTree->right);
		return head;
        
    }
};

4.二叉树的创建(前序+中序,后序+中序)

 这应该算是一个老生常谈的问题了,算是经典中的经典了,其实不论是前序+中序还是后序+中序,其原理都大同小异,所以,我们将这两个例子放在一起讲解,有助于理解和记忆。

题目链接:105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)

                  106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)

思路分析

前序+中序:

递归法:

       只要我们在中序遍历中定位到根节点,那么我们就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此我们就可以对应到前序遍历的结果中,也就是每次都从中序遍历中寻找根节点,然后通过中序遍历将左右子树划分开来,再递归构造新的左右子树即可;

class Solution {
public:
    TreeNode *root=nullptr;
    TreeNode* build(vector<int>& pre, vector<int>& in,int pl,int pr,int il,int ir)
    {
        if(pl>pr)//如果越界直接返回空即可
           return nullptr;
        TreeNode* cur=new TreeNode(pre[pl]);//前序遍历第一个元素一定是某棵子树的根节点
        int idx=0,len=0;//分别表示根节点在中序遍历的位置和左子树的节点个数
        for(int i=il;i<=ir;i++)
        {
            if(in[i]==pre[pl]) //找到根节点所在的中序遍历的位置以确定根的左右子树
            {
                idx=i;
                len=i-il;//找到对应的左子树的节点个数
                break;
            }
        }
        cur->left=build(pre,in,pl+1,pl+len,il,idx-1);
        cur->right=build(pre,in,pl+1+len,pr,idx+1,ir);

        return cur;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
           
        root= build(preorder,inorder,0,preorder.size()-1,0,inorder.size()-1);
        return root;
    }
};

迭代法:

       我们先来假设一种极端情况,如果一棵二叉树只有左孩子,那么其结构就类似于一张单链表,它的前序遍历和中序遍历一定就相反,我们将这个由此就可以判断此之前的过程中的每一个节点都没有右孩子,为了便于理解,我们用例子加以说明:

      那么此时我们就可以维护一个栈,用来保存还没有考虑过右孩子的节点,按前序遍历的序列一直向二叉树的左端去遍历,当某个时刻遍历到了二叉树的最左节点,此时说明前序遍历的下一个元素一定是栈中某个节点的右孩子了,然后我们再依次对比栈中元素和中序序列,取出栈中的最后一个满足前序和中序序列相反的元素,将新加入的节点加入其右孩子,再将其入栈,我们无需再考虑已经出栈的元素,因为他们的右子树都已经被考虑完了,所以其树结构已经形成了,重复该过程直到前序序列遍历完毕即可形成一棵完整的树。

class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
           
            if(!preorder.size())
                return nullptr;
            TreeNode *root=new TreeNode(preorder[0]);//先序遍历的第一个节点一定是根
            stack<TreeNode*>st;
            st.push(root);
            int idx=0;//初始idx指向中序遍历的第一个元素(整棵二叉树的最左节点)
            for(int i=1;i<preorder.size();i++)//根节点已经加入,所以前序遍历从下标1开始即可
            {
               TreeNode* node=st.top();
               if(node->val!=inorder[idx])//如果加入节点还没判断是不是有右孩子
               {
                   node->left=new TreeNode(preorder[i]);
                   st.push(node->left);//节点入栈等待判断
               }
               else //此时说明已经到达了某棵子树的最左节点,开始判断新加入的节点(preorder[i])是哪一个栈中节点的右孩子
               {
                    TreeNode* parent=nullptr;
                    while(!st.empty()&& st.top()->val==inorder[idx]) //前中序遍历序列一致,此时该节点没有右孩子
                    {
                        parent=st.top();//最后一个出栈的节点一定是那个父节点,用于保存上次出栈记录
                        st.pop();
                        ++idx;
                    }
                    parent->right=new TreeNode(preorder[i]);//创建其右孩子
                    st.push(parent->right);//不能忘了将新创建的节点加入,因为该节点可能还有左右子树需要创建
               }
            }

            return root;
    }
};

中序+后序:

递归法:

       后序遍历的数组最后一个元素代表的即为根节点。知道这个性质后,我们可以利用已知的根节点信息在中序遍历的数组中找到根节点所在的下标,然后根据其将中序遍历的数组分成左右两部分,左边部分即左子树,右边部分为右子树,针对每个部分可以用同样的方法继续递归下去构造。

class Solution {
public:
    TreeNode*build(vector<int>& in, vector<int>& pos,int il,int ir,int pl,int pr)
    {
        if(pl>pr)
          return nullptr;
        TreeNode*root=new TreeNode(pos[pr]);//后序遍历的最后一个节点一定是根节点
        int idx=0,len=0;
        for(int i=il;i<=ir;i++)
        {
            if(pos[pr]==in[i])
            {
               idx=i;
               len=i-il;
               break;
            }
        }
        root->left=build(in,pos,il,idx-1,pl,pl+len-1);
        root->right=build(in,pos,idx+1,ir,pl+len,pr-1);

        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {

         return build(inorder,postorder,0,inorder.size()-1,0,postorder.size()-1);
    }
};

迭代法:

      此处和前中序创建的方法大同小异,我们只需要注意前序和后序的区别即可,因为后序遍历中,和根节点挨在一块的是右子树,所以,我们转变一下思路,可以通过构建右子树的过程中插入左节点来实现,其余的原理均和前中序的原理类似。

class Solution {
public:
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {

        if(!postorder.size())
            return nullptr;
        stack<TreeNode*>st;
        TreeNode* root=new TreeNode(postorder[postorder.size()-1]);
        st.push(root);
        int idx=postorder.size()-1;
        for(int i=postorder.size() - 2;i>=0;i--)
        {
            TreeNode* node=st.top();
            if(node->val!=inorder[idx])//这次是县创建右子树
            {
                node->right=new TreeNode(postorder[i]);
                st.push(node->right);
            }
            else//找到了第一个左孩子节点
            {
                TreeNode* parent=nullptr;
                while(!st.empty()&&st.top()->val==inorder[idx])
                {
                    parent=st.top();
                    st.pop();
                    --idx;
                }
                parent->left=new TreeNode(postorder[i]);
                st.push(parent->left);
            }

        }

        return root;
    }
};

5.二叉树的三种迭代法遍历

题目链接:二叉树的前序遍历、二叉树的中序遍历、二叉树的后序遍历

思路分析

前序遍历:

       对于前序遍历,根左右的形式,根节点一定是最先输出的,难点在于如何转换方向,也就是当遍历到左子树最后一个节点时,如何将其转换为右子树再进行输出,此时,我们可以设置一个指针cur,初始时,cur一直向着左子树走,当左子树遍历到最左节点时,我们可以将当前的cur更新其父节点的右孩子节点,然后cur再次往左走,走到叶节点再次转向右子树,重复递归这个过程即可。

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> ans;
        stack<TreeNode*> st;
        TreeNode*cur=root;
        while(cur || !st.empty())
        {
            while(cur)
            {
                ans.push_back(cur->val);//将每棵子树的根节点直接加入
                st.push(cur);
                cur=cur->left;//接着寻找最左节点
            }
            //此时节点已经找到了最左节点
            cur=st.top()->right;//转而进入最左节点的右子树
            st.pop();//删除最左节点    
        }
        return ans;
    }
};

中序遍历:

       和前序遍历的大同小异,此时我们的输出模式变成了左根右,那么此时我们循环找到最左节点的时候,就需要先输出最左节点,在回溯找到根节点输出,然后再递归到右子树进行输出即可。

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ans;
        stack<TreeNode*> st;
        if(!root)
            return ans;
        TreeNode* cur=root;
        while(cur||!st.empty())
        {
            while(cur)
            {
                st.push(cur);//将左子树根节点入栈
                cur=cur->left;//向左查找最左节点
            }
            //此时cur已经是该子树上的最左节点
            TreeNode* p=st.top();
            st.pop();
            ans.push_back(p->val);

            cur=p->right; //无论最左节点是否有右子树,我们转换为右子树的子问题,此时cur如果为空,可直接回溯到最左节点的父节点进行操作
        }
       
        return ans;
    }
};

后序遍历:

       后序遍历与上面的两种方式有着些许不同,其中,后序遍历的难点是如何判断当前的节点能否作为根输出,也就相当于,如果左节点存在右子树且还没有输出,那么我们就需要先考虑右子树的输出而不能先输出左节点,反之,如果其没有右子树或者右子树已经输出完了,那么就可以直接输出,所以,每个左节点都需要进行特殊分析,由于我们知道后序遍历的形式是左右根,那么每个子树输出的最后个一定是其根节点,而如果一个左节点有右子树,那么此时其后序遍历的输出,在左节点之前的节点一定是其右孩子节点,我们可以以这个条件作为其右子树是否输出完毕的条件,设置一个指针pre指向上一个被访问的节点,此时就有两种情况:

       1.当前节点的右子树为空或者当前节点的右子树是上一个被访问的节点,此时说明该节点作为根节点,其右子树的输出已经完毕,可以输出根节点了;(注意此时要更新pre指针

       2.如果右树存在且没有被访问,那么就需要转到右树上去继续输出,此时根节点不能删除。

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> ans;
        stack<TreeNode*> st;
        
        TreeNode* cur=root;
        TreeNode* pre=nullptr;
        while(cur || !st.empty())
        {
            while(cur)
            {
                st.push(cur);
                cur=cur->left;
            }
            //此时已经到达了最左节点
            TreeNode* top=st.top();
            if(top->right==nullptr || top->right==pre) //如果此时最左节点的右子树为空或者右子树已经被访问直接输出该左节点即可
            {
                st.pop();
                ans.push_back(top->val);    
                pre=top;//更新上次访问的节点
            }
            else
            {
                cur=top->right;//先访问其右子树,再输出根
            }           
        }
        return ans;
    }
};

后面有好的题目会继续补充,敬请期待......

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1204178.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Redis】Zset有序集合

上一篇&#xff1a; Hash哈希类型 https://blog.csdn.net/m0_67930426/article/details/134382507?spm1001.2014.3001.5502 目录 Zadd Zrange Zcard Zcount Zrem set是一个无序且元素不可重复的集合 而Zset是一个有序的集合,集合里的每个元素都有一个评分&#xff08;…

Linux网络——自定义协议

目录 一.什么是协议 二.协议与报文 三.自定义协议 1.封装套接字 2.构建请求与响应 3.序列化和反序列化 4.报头添加和去除 5.报文读取 四.服务器端程序 五.客户端程序 一.什么是协议 协议在生活中泛指&#xff1a;双方或多方为了完成某项任务或达成某种目的而制定的共…

文件操作(上)

目录 为什么使用文件什么是文件&#xff1f;程序文件数据文件文件名 二进制文件和文本文件文件的打开和关闭流和标准流流标准流 文件指针文件的打开和关闭 感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接 &#x1f412;&#x1f412;&#x1f412; 个人主页 &…

DMDEM部署说明-详细步骤-(DM8达梦数据库)

DMDEM部署说明-详细步骤-DM8达梦数据库 环境介绍1 部署DM8 数据库1.1 创建一个数据库作为DEM后台数据库1.2 创建数据库用户 DEM1.3 使用DEM用户导入dem_init.sql 2 配置tomcat2.1 配置/tomcat/conf/server.xml2.2 修改jvm启动参数 3 配置JAVA 1.8及以上版本的运行时环境3.1 配置…

数据分析 - 思考题

上班路上刷到的有趣题

基于教与学算法优化概率神经网络PNN的分类预测 - 附代码

基于教与学算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于教与学算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于教与学优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神经网络…

详解IP安全:IPSec协议簇 | AH协议 | ESP协议 | IKE协议

目录 IP安全概述 IPSec协议簇 IPSec的实现方式 AH&#xff08;Authentication Header&#xff0c;认证头&#xff09; ESP&#xff08;Encapsulating Security Payload&#xff0c;封装安全载荷&#xff09; IKE&#xff08;Internet Key Exchange&#xff0c;因特网密钥…

eNSP启动路由器一直出#号、以为是安装配置winpcap的问题。。。。(以为是win10安装winpcap失败的问题。。。)

问题描述&#xff1a;eNSP启动一直出#号的一种参考方法_ensp一直#_Hong的博客-CSDN博客 原因是看了这篇博客&#xff0c;觉得ensp启动路由器的时候一直出现&#xff03;号是因为winpcap安装的时候出现的问题。查看自己的winpcap安装成功之后的目录是&#xff1a; 然后因为那篇…

【C++】类和对象(2)--构造函数

目录 一 概念 二 构造函数特性 三 默认构造函数 一 概念 对于以下Date类&#xff1a; class Date { public:void Init(int year, int month, int day){_year year;_month month;_day day;}void Print(){cout << _year << "-" << _month <…

Apache Airflow (五) :DAG调度触发时间

&#x1f3e1; 个人主页&#xff1a;IT贫道_大数据OLAP体系技术栈,Apache Doris,Clickhouse 技术-CSDN博客 &#x1f6a9; 私聊博主&#xff1a;加入大数据技术讨论群聊&#xff0c;获取更多大数据资料。 &#x1f514; 博主个人B栈地址&#xff1a;豹哥教你大数据的个人空间-豹…

CLIP:万物分类(视觉语言大模型)

本文来着公众号“AI大道理” ​ 论文地址&#xff1a;https://arxiv.org/abs/2103.00020 传统的分类模型需要先验的定义固定的类别&#xff0c;然后经过CNN提取特征&#xff0c;经过softmax进行分类。然而这种模式有个致命的缺点&#xff0c;那就是想加入新的一类就得重新定义…

Lasso回归和岭回归详解

当数据特征存在多重共线性&#xff0c;特征矩阵不满秩&#xff0c;或者用普通线性回归过拟合的状况时&#xff0c;我们需要用lasso回归或岭回归来构建模型。 左边是lasso回归&#xff0c;右边是岭回归。 Lasso使用的是系数 的L1范式&#xff08;L1范式则是系数 的绝对值&#…

PCL安装与使用

1 apt安装 ubuntu20.04及以上版本下可以直接通过apt方式安装pcl编译好的二进制文件,二进制安装的版本为1.10。 sudo apt update sudo apt install libpcl-dev 2 源码安装 在pcl的github上下载对应的版本进行安装&#xff1a; https://github.com/PointCloudLibrary/pcl/rel…

分组取每组数据的最大值和最小值的方法思路,为类似场景的数据分析提取提供思路,例如提取宗地内建筑的最高层数等可参考此方法思路

目录 一、实现效果 二、实现过程 1.读取并剔除无效数据 2.数据分组 3.提取最大值 4.提取最小值 三、总结 使用FME实现批量分组取每组数据的最大值和最小值&#xff0c;为类似场景的数据分析提取提供思路&#xff0c;例如提取宗地内建筑的最高层数等可参考此方法思路。关…

【数据结构】非递归实现二叉树的前 + 中 + 后 + 层序遍历(听说面试会考?)

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前学习C和算法 ✈️专栏&#xff1a;数据结构 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&…

mysql之正则表达式匹配

题目&#xff1a; 今天在牛客网看到一道关于数据库正则表达式匹配的问题&#xff0c;发现自己一点不会做。 正则表达式&#xff1a; 一、正则表达式 MySQL 正则表达式通常是在检索数据库记录的时候&#xff0c;根据指定的匹配模式匹配记录中 符合要求的特殊字符串。MySQL 的…

利用角色roles上线wordpress项目

角色订制&#xff1a;roles ① 简介 对于以上所有的方式有个弊端就是无法实现复用假设在同时部署Web、db、ha 时或不同服务器组合不同的应用就需要写多个yml文件。很难实现灵活的调用。   roles 用于层次性、结构化地组织playbook。roles 能够根据层次型结构自动装载变量文…

linux下使用Docker Compose部署Spug实现公网远程访问

&#x1f4d1;前言 本文主要是linux下使用Docker Compose部署Spug实现公网远程访问的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &am…

openlayers 注册投影

注册投影 openlayers 默认支持的坐标系有4326&#xff08;基于美国wgs84坐标系&#xff09;和3857&#xff08;墨卡托投影&#xff09;两种。 所以如果我们想要使用比如4490坐标系&#xff0c;或者4547坐标系&#xff0c;就需要先注册&#xff0c; 注册4490示例代码如下 如…

【Linux进程】进程控制

目录 一、进程创建 1.2 fork函数初识 1.2 fork函数返回值 1.3 写时拷贝 1.4 fork常规用法 1.5 fork调用失败的原因 二、进程终止 2.1 进程退出场景 2.2 进程退出码 2.2.1 用strerror打印错误信息 2.2.2 errno全局变量 2.3 进程常见退出方法 2.3.1 进程正常退出 2…