热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

二叉树面试题之二叉树的遍历方式

一、基本概念1、二叉树的概念一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵分别称为左子树和右子树的二叉树组成(即一个根节点最多只有两个孩子结点)。2、二

一、基本概念
1、二叉树的概念
一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵分别称为左子树和右子树的二叉树组成(即一个根节点最多只有两个孩子结点)。
2、二叉树的特点
(1)每个结点最多有两棵子树,即二叉树不存在度大于2的结点(分支数最大不超过2)
(2)二叉树的子树有左右之分,其子树的次序不能颠倒
3、完全二叉树与满二叉树
(1)满二叉树:在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子节点都在同一层上。
(2)完全二叉树:如果一棵具有N个结点的二叉树的结构与满二叉树的前N个结点的结构相同,称为完全二叉树。
《二叉树面试题之二叉树的遍历方式》
4、二叉树的性质
(1)若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2 i-1 (i>=1)个结点
(2)若规定只有根节点的二叉树的深度为1,则深度为K的二叉树的最大结点数是2 k -1(k>=0)
(3)对任何一棵二叉树, 如果其叶结点个数为 n0 , 度为2的非叶结点个数为 n2 ,则有n0 =n 2 +1
(4)具有n个结点的完全二叉树的深度k为log2(n+1)上取整。
(5)对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:
《二叉树面试题之二叉树的遍历方式》
二、二叉树的基本操作
1、二叉树的创建
利用前序的规则,即先创建根节点,然后在创建右节点,最后创建左结点
2、在二叉树中,赋值运算符的重载
(1)先销毁原来的树
(2)拷贝现在的树
3、二叉树的销毁
利用后序的规则,防止将根节点销毁之后,其的左孩子或者右孩子找不到
上面几个基本操作的代码如下所示:

//二叉树结点的定义
template<class T> struct BinTreeNode {
//构造函数
BinTreeNode(const T&data)
:_data(data)
, _pLeft(NULL)
, _pRight(NULL)
{}
BinTreeNode *_pLeft;
BinTreeNode *_pRight;
T _data;
};

//二叉树的定义
template<class T>
class BinTree
{
typedef BinTreeNode Node;
typedef BinTreeNode* pNode ;
public:
//构造函数
BinTree()
:_pRoot(NULL)
{}
BinTree(const T* array, size_t size, const T& invalid)
{
size_t index = 0;//防止常引用
_CreateBinTree(_pRoot, array, size, index, invalid);//构造树
}
//拷贝构造函数
BinTree(const BinTree& bt)
{
_pRoot = _CopyBinTree(bt._pRoot);
}
//赋值运算符重载
BinTree& operator=(const BinTree& bt)
{
if (this != &bt)
{
//先销毁原来的树
_DestroyBinTree(_pRoot);
//拷贝现在的树
_pRoot = _CopyBinTree(bt._pRoot);
}
return *this;
}
private:
//利用后序遍历
void _DestroyBinTree(PNode pRoot)
{
if (pRoot)
{
//销毁左子树
_DestroyBinTree(pRoot->_pLeft);
//销毁右子树
_DestroyBinTree(pRoot->_pRight);
//销毁根节点
delete pRoot;
pRoot = NULL;
}
}
PNode _CopyBinTree(PNode pRoot)
{
PNode pNewNode = NULL;
if (pRoot)
{
//拷贝根节点
pNewNode = new Node(pRoot->_data);
//拷贝根节点的左孩子
if (pRoot->_pLeft)
pNewNode->_pLeft = _CopyBinTree(pRoot->_pLeft);
//拷贝根节点的右孩子
if (pRoot->_pRight)
pNewNode->_pRight = _CopyBinTree(pRoot->_pRight);
}
return pNewNode;
}
void _CreateBinTree(PNode& pRoot, const T* array, size_t size, size_t& index, const T& invalid)
{
if (index array[index])//防止无效值的插入
{
//创建根节点
pRoot = new Node(array[index]);
//创建左子树-
_CreateBinTree(pRoot->_pLeft, array, size, ++index, invalid);
//创建右子树
_CreateBinTree(pRoot->_pRight, array, size, ++index, invalid);
}
}
private:
PNode _pRoot;
};

树的形式:
《二叉树面试题之二叉树的遍历方式》
三、二叉树的遍历方式
(一)层序遍历
结果:abcdef
思路:层序遍历:即一层访问完成之后,才去进行下一层,在遍历当前层的时候,需要将当前层的当前结点的左右孩子保存在队列中,再将其当前结点弹出,直至队列中为空,利用队列+循环
代码实现如下所示;

void _LevelOrder(pNode pRoot)
{
if (_pRoot == NULL)
return;
queue q;
q.push(pRoot);
pNode pCur = NULL;
while (!q.empty())
{
pCur = q.front();
cout <_data <<" ";
if (pCur->_pLeft)
q.push(pCur->_pLeft);
if (pCur->_pRight)
q.push(pCur->_pRight);
q.pop();
}
cout < }

验证结果如下所示:
《二叉树面试题之二叉树的遍历方式》
(二)前序遍历
结果:abdcef
思路:在前序遍历中,先访问一棵树的根节点,其次访问一棵树的左子树,最后访问右子树,根据思路,实现递归与非递归。
1、递归:同思路
代码实现如下:

void _FrontOrder(pNode pRoot)
{
if (pRoot)
{
cout << pRoot->_data << " ";
_FrontOrder(pRoot->_pLeft);
_FrontOrder(pRoot->_pRight);
}
}

2、非递归:利用栈,首先将二叉树的根节点压入到栈中,利用循环,循环的终止条件是,栈中不为空,取栈顶元素,访问并弹出,如果栈中的右孩子存在,且将栈顶元素的右孩子压入到栈中,如果栈中的左孩子存在,且将栈顶元素的左孩子压入到栈中,重复以上过程,直到栈为空。
代码实现如下:

void _FrontOrder_Nor1(pNode pRoot)
{
if (NULL == pRoot)
return;
stack s;
s.push(pRoot);
while (!s.empty())
{
pNode pCur = s.top();
cout <_data <<" ";
s.pop();
if (pCur->_pRight)
s.push(pCur->_pRight);
if (pCur->_pLeft)
s.push(pCur->_pLeft);
}
}

3、非递归:由于在前序遍历中,一直先访问二叉树中的根节点与左孩子,因此,为了优化程序,因此如果当前结点的右孩子存在,将当前结点的右孩子压入到栈中,在此时,取二叉树的左孩子并且访问,直到当前结点没有右孩子,且栈中元素已经访问完成即可(即栈为空)。
代码实现如下所示:

void _FrontOrder_Nor2(pNode pRoot)
{
if (NULL == pRoot)
return;
stack s;
s.push(pRoot);
while (!s.empty())
{
pNode pCur = s.top();
s.pop();
while (pCur)
{
cout <_data <<" ";
if (pCur->_pRight)
s.push(pCur->_pRight);
pCur = pCur->_pLeft;
}
}
}

整体验证结果如下所示:
《二叉树面试题之二叉树的遍历方式》
(三)中序遍历
结果:dbaecf
思路:在中序遍历中,先访问一棵树的左子树,其次访问一棵树的根节点,最后访问右子树,根据思路,实现递归与非递归。
1、递归:同思路
代码实现如下所示:

void _InOrder(pNode pRoot)
{
if (pRoot)
{
_InOrder(pRoot->_pLeft);
cout << pRoot->_data << " ";
_InOrder(pRoot->_pRight);
}
}

2、非递归
利用栈,定义临时变量,让其指向根节点,其次利用循环,循环条件while (pCur || !s.empty()),(出口的条件),在循环内部,让其的当前结点入栈,然后让其取当前结点的左孩子,一直入栈,直到当前结点为空为止,然后取其栈顶元素并且访问,让其临时变量指向栈顶元素,在取栈顶的右孩子,如果存在,压入到栈中,不存在,继续取栈顶元素即可,直到栈中为空且临时变量取到空为止。
代码实现如下所示:

void _InOrder_Nor(pNode pRoot)
{
if (NULL == pRoot)
return;
stack s;
pNode pCur = pRoot;
while (pCur || !s.empty())
{
while (pCur)
{
s.push(pCur);
pCur = pCur->_pLeft;
}
pCur = s.top();
cout <_data <<" ";
s.pop();
pCur = pCur->_pRight;
}
}

验证结果如下所示:
《二叉树面试题之二叉树的遍历方式》
(四)后序遍历
结果:dbefca
思路:在后序遍历中,先访问一棵树的左子树,其次访问一棵树的右子树,最后访问根节点,根据思路,实现递归与非递归。
1、递归:同思路
代码实现如下所示:

void _PostOrder(pNode pRoot)
{
if (pRoot)
{
_PostOrder(pRoot->_pLeft);
_PostOrder(pRoot->_pRight);
cout << pRoot->_data << " ";
}
}

2、非递归
后序遍历的难点在于:需要判断上次访问的节点是位于左子树,还是右子树。若是位于左子树,则需跳过根节点,先进入右子树,再回头访问根节点;若是位于右子树,则直接访问根节点。
代码实现如下所示:

void _PostOrder_No1(pNode pRoot)
{
if (NULL == pRoot)
return;
pNode pCur = pRoot;
pNode pTop = NULL;
pNode Prev = NULL;
stack s;
while (pCur||!s.empty())
{
//左子树入栈
while (pCur)
{
s.push(pCur);
pCur = pCur->_pLeft;
}
//左子树入栈之后,取栈顶元素
pTop = s.top();
//判断栈顶元素是否有右子树
//右子树不存在,且右子树没有被访问
if (NULL == pTop->_pRight || Prev == pTop->_pRight)
{
//访问并弹出
cout <_data <<" ";
Prev = pTop;
s.pop();
}
else
pCur = pTop->_pRight;
}
}
void _PostOrder_No2(pNode pRoot)
{
if (NULL == pRoot)
return;
pNode pCur = pRoot;
pNode Prev = NULL;
stack s;
while (pCur || !s.empty())
{
//左子树入栈
//这块pCur != Prev改变了pCur的指向,因此是为了防止循环入栈
while (pCur&&pCur != Prev)
{
s.push(pCur);
pCur = pCur->_pLeft;
}
//这样防止形成死循环
if (s.empty())
return;
//左子树入栈之后,取栈顶元素
pCur = s.top();
//判断栈顶元素是否有右子树
//右子树不存在,且右子树没有被访问
if (NULL == pCur->_pRight || Prev == pCur->_pRight)
{
//访问并弹出
cout <_data <<" ";
Prev = pCur;
s.pop();
}
else
pCur = pCur->_pRight;
}
}

验证结果如下所示:
《二叉树面试题之二叉树的遍历方式》

整体结果如下所示:
《二叉树面试题之二叉树的遍历方式》
对于二叉树的遍历方式,就分为以上四种,如果大家还有更好的建议,还请大家指正!!

只有不停的奔跑,才能不停留在原地!!!


推荐阅读
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • C语言常量与变量的深入理解及其影响
    本文深入讲解了C语言中常量与变量的概念及其深入实质,强调了对常量和变量的理解对于学习指针等后续内容的重要性。详细介绍了常量的分类和特点,以及变量的定义和分类。同时指出了常量和变量在程序中的作用及其对内存空间的影响,类似于const关键字的只读属性。此外,还提及了常量和变量在实际应用中可能出现的问题,如段错误和野指针。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 本文讨论了一个数列求和问题,该数列按照一定规律生成。通过观察数列的规律,我们可以得出求解该问题的算法。具体算法为计算前n项i*f[i]的和,其中f[i]表示数列中有i个数字。根据参考的思路,我们可以将算法的时间复杂度控制在O(n),即计算到5e5即可满足1e9的要求。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
  • Learning to Paint with Model-based Deep Reinforcement Learning
    本文介绍了一种基于模型的深度强化学习方法,通过结合神经渲染器,教机器像人类画家一样进行绘画。该方法能够生成笔画的坐标点、半径、透明度、颜色值等,以生成类似于给定目标图像的绘画。文章还讨论了该方法面临的挑战,包括绘制纹理丰富的图像等。通过对比实验的结果,作者证明了基于模型的深度强化学习方法相对于基于模型的DDPG和模型无关的DDPG方法的优势。该研究对于深度强化学习在绘画领域的应用具有重要意义。 ... [详细]
author-avatar
DaybreakCP
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有