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

       本文,我们主要讲解一些适合用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;elsereturn 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.mzph.cn/news/142414.shtml

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

相关文章

记录第一次

1.看接口 看控制台 报错吗&#xff1f; 控制台 空指针报错 前端控制台 2.找报错 看哪里报的错误&#xff0c;控制台的错误&#xff08;空指针报错&#xff09; 错误问题&#xff1a; 3.分析业务 业务问题 一定要问&#xff0c; 4. 找到出错点

设计模式之模版方法(TemplateMethod)

模版方法 钩子函数 回调函数 在父类里面有一个模版方法&#xff0c;在这个方法里面调用了op1&#xff0c;op2&#xff0c;op3… 在子类里面如果想要改变父类的op1和op2 只需要重写op1和op2&#xff0c;那么这个重写之后的方法&#xff0c;可以在父类里面直接调用的到 例子: J…

Postman小白安装和注册入门教程

近期在复习Postman的基础知识&#xff0c;在小破站上跟着百里老师系统复习了一遍&#xff0c;也做了一些笔记&#xff0c;希望可以给大家一点点启发。 一&#xff09;安装 访问官网https://www.getpostman.com/downloads/&#xff0c;直接下载安装。 二&#xff09;注册和登录…

【C/C++底层】内存分配:栈区(Stack)与堆区(Heap)

/*** poject * author jUicE_g2R(qq:3406291309)* file 底层内存分配&#xff1a;栈区(Stack)与堆区(Heap)* * language C/C* EDA Base on MVS2022* editor Obsidian&#xff08;黑曜石笔记软件&#xff09;* * copyright 2023* COPYRIGHT …

千万富翁分享:消费多少免单多少,电商运营高手实战秘籍拆解

千万富翁分享&#xff1a;消费多少免单多少&#xff0c;电商运营高手实战秘籍拆解 后疫情时代&#xff0c;国内电商圈层进程依然是在高速发展阶段&#xff0c;今年2023年双十一也彻底落下帷幕&#xff0c;但这次相较于往常却没有公布具体的成交规模数据&#xff0c;那么&#x…

国产双核DSP与 TI 的TMS320F28377 大PK

国产DSP&#xff0c;QX320F28377与 TI的 TMS320F28377 孰强孰弱

Redis的特性以及使用场景

分布式发展历程参考 陈佬 http://t.csdnimg.cn/yYtWK 介绍redis Redis&#xff08;Remote Dictionary Server&#xff09;是一个基于客户端-服务器架构的在内存中存储数据的中间件&#xff0c;属于NoSQL的一种。它可以用作数据库、缓存/会话存储以及消息队列。 作为一种内存数…

思维导图软件 Xmind mac中文版软件特点

XMind mac是一款思维导图软件&#xff0c;可以帮助用户创建各种类型的思维导图和概念图。 XMind mac软件特点 - 多样化的导图类型&#xff1a;XMind提供了多种类型的导图&#xff0c;如鱼骨图、树形图、机构图等&#xff0c;可以满足不同用户的需求。 - 强大的功能和工具&#…

C++学习---信号处理机制、中断、异步环境

文章目录 前言信号处理signal()函数关于异步环境 信号处理函数示例raise()函数 前言 信号处理 关于信号&#xff0c;信号是一种进程间通信的机制&#xff0c;用于在程序执行过程中通知进程发生了一些事件。在Unix和类Unix系统中&#xff0c;信号是一种异步通知机制&#xff0c…

Kibana使用Watcher监控服务日志并发送飞书报警(Markdown)

Watcher是什么 Kibana Watcher 是 Elasticsearch 的监控和告警工具&#xff0c;它允许你设置和管理告警规则以监控 Elasticsearch 数据和集群的状态。Kibana Watcher 可以监测各种指标和数据&#xff0c;然后在满足特定条件时触发警报。它提供了一种强大的方式来实时监控 Elas…

​Distil-Whisper:比Whisper快6倍,体积小50%的语音识别模型

内容来源&#xff1a;xiaohuggg Distil-Whisper&#xff1a;比Whisper快6倍&#xff0c;体积小50%的语音识别模型 ​该模型是由Hugging Face团队开发&#xff0c;它在Whisper核心功能的基础上进行了优化和简化&#xff0c;体积缩小了50%。速度提高了6倍。并且在分布外评估集上…

【Qt之QWizard】使用1

QWizard使用 描述方法枚举&#xff1a;enum QWizard::WizardButton枚举&#xff1a;enum QWizard::WizardOption枚举&#xff1a;enum QWizard::WizardStyle枚举&#xff1a;enum QWizard::WizardPixmap常用成员方法槽函数信号 示例设置标题添加page页设置按钮文本设置自定义按…

Windows配置IP-SAN(iSCSI)

之前写了《Linux配置IP-SAN&#xff08;iSCSI&#xff09;》&#xff0c;现在简单记录Windows配置IP-SAN&#xff08;iSCSI&#xff09;&#xff0c;基本过程都是一样的。一些原理请参考《Linux配置IP-SAN&#xff08;iSCSI&#xff09;》&#xff0c;更详细一些。 目录 一、确…

Windows conan环境搭建

Windows conan环境搭建 1 安装conan1.1 安装依赖软件1.1.1 python安装1.1.2 git bash安装1.1.3 安装Visual Studio Community 20191.1.3.1 选择安装的组件1.1.3.2 选择要支持的工具以及对应的SDK 1.1.4 vscode安装 1.3 验证conan功能1.4 查看conancenter是否包含poco包1.5 查看…

面试官问 Spring AOP 中两种代理模式的区别?很多面试者被问懵了

面试官问 Spring AOP 中两种代理模式的区别?很多初学者栽了跟头&#xff0c;快来一起学习吧&#xff01; 代理模式是一种结构性设计模式。为对象提供一个替身&#xff0c;以控制对这个对象的访问。即通过代理对象访问目标对象&#xff0c;并允许在将请求提交给对象前后进行一…

服务器中了locked勒索病毒怎么处理,locked勒索病毒解密,数据恢复

近几年&#xff0c;网络应用技术得到了迅速发展&#xff0c;越来越多的企业开始走向数字化办公&#xff0c;极大地为企业的生产运营提供了帮助&#xff0c;但是网络技术的发展也为网络安全埋下隐患。最近&#xff0c;locked勒索病毒非常嚣张&#xff0c;几乎是每隔两个月就会对…

媒体软文投放的流程与媒体平台的选择

海内外媒体软文&#xff1a;助力信息传播与品牌建设 在当今数字化时代&#xff0c;企业如何在庞大的信息海洋中脱颖而出&#xff0c;成为品牌建设的领军者&#xff1f;媒体软文投放无疑是一项强大的策略&#xff0c;通过选择合适的平台&#xff0c;精准投放&#xff0c;可以实…

动手学深度学习——序列模型

序列模型 1. 统计工具1.1 自回归模型1.2 马尔可夫模型 2. 训练3. 预测4. 小结 序列模型是一类机器学习模型&#xff0c;用于处理具有时序关系的数据。这些模型被广泛应用于自然语言处理、音频处理、时间序列分析等领域。 以下是几种常见的序列模型&#xff1a; 隐马尔可夫模型…

探索数据湖和大数据在亚马逊云服务云存储服务上的威力

文章作者&#xff1a;Libai 引言 在当今数字化的环境中&#xff0c;组织生成的数据量正以前所未有的速度增长。数据量的激增催生了对高效存储和管理解决方案的需求。数据湖和亚马逊云服务云存储服务上的大数据是一个强大的组合&#xff0c;使组织能够充分发挥其数据的潜力。 亚…

【ubuntu 快速熟悉】

ubuntu 快速熟悉 2.ubuntu桌面管理器3.ubuntu常见文件夹说明4.ubuntu任务管理器4.1 gnome桌面的任务管理器4.2 实时监控GPU4.3 top 命令 5.ubuntu必备命令5.1 .deb文件5.2 查找命令5.2.1 find文件搜索5.2.2 which查找可执行文件的路径5.2.3 which的进阶&#xff0c;whereis5.2.…