二叉树的遍历(前序、中序、后序、层序),递归和非递归实现

本文详细介绍了二叉树的遍历方法,包括先序、中序、后序及层序遍历,并提供了非递归实现的代码示例。重点阐述了如何利用栈来解决递归问题,提高程序的效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

其实自己没有写博客的习惯,就是个小菜鸟。因为接下来要找工作的原因,将之前学习过数据结构或者算法温习一下,记录下来,日后回头看看自己当初的稚嫩和不成熟。

树是常用的数据结构,二叉树是最简单的一种树,没有其他树(排序树、红黑树等)的约束,树的遍历也是各大公司笔试时的常见考点。


二叉树的结构定义如下:

struct BinaryTreeNode
{
	int m_nValue;                                       //结点的值
	BinaryTreeNode* m_pLeft;                            //结点的左子树
	BinaryTreeNode* m_pRight;                           //结点的右子树

	//为了接下来的编程的方便,我给了一个带参数的构造函数
	BinaryTreeNode(int val)
	{
		m_nValue=val;
		m_pLeft=NULL;
		m_pRight=NULL;
	}
};

先序遍历或者叫先根遍历,就是先从根结点开始访问,接着访问左子树、最后访问右子树。

所以,先序遍历的顺序是1、2、4、5、6、7、3

中序遍历或者叫中根遍历,就是先从左子树开始访问,接着访问根结点,最后访问右子树。

所以,中序遍历的顺寻是4、2、6、5、7、1、3

后序遍历或者叫后根遍历,就是先从左子树开始访问,接着访问右子树,最后访问根结点。

所以,中序遍历的顺寻是4、6、7、5、2、3、1

所谓的先序、中序、后序都是相对根结点而言的。

最后说一下层序遍历,顾名思义,就是一层一层访问,层序遍历的顺序是,1、2、3、4、5、6、7。

上数据结构课的时候,讲到树这一章,老师开篇明义就说到,树的问题95%都可以用递归解决(当时还不明白怎么一回事,但是这句话我记住了)。

先给出二叉树的结构不管是先序、中序、后序其实就是先访问根结点还是先访问子树的问题,就是一个递归的问题。首先给出递归实现,很简单,只想说一点,既然是递归实现,就一定要有一个终止条件。

void Visit(BinaryTreeNode* pNode)
{
	if(pNode!=NULL)
		printf("%d\t",pNode->m_nValue);
}
void PreOrderRecursion(BinaryTreeNode* pRoot)
{
	if(pRoot==NULL)               //终止条件
		return ;

	/*
	先序遍历就是先访问根结点,
	然后访问左子树,
	最后访问右子树
	*/
	Visit(pRoot);
	PreOrderRecursion(pRoot->m_pLeft);
	PreOrderRecursion(pRoot->m_pRight);
}

void InOrderRecursion(BinaryTreeNode* pRoot)
{
	if(pRoot==NULL)               //终止条件
		return ;

	/*
	中序遍历就是先访问左子树,
	然后访问根结点,
	最后访问右子树
	*/
	InOrderRecursion(pRoot->m_pLeft);
	Visit(pRoot);	
	InOrderRecursion(pRoot->m_pRight);
}

void PostOrderRecursion(BinaryTreeNode* pRoot)
{
	if(pRoot==NULL)               //终止条件
		return ;

	/*
	后序遍历就是先访问左子树,
	然后访问右子树,
	最后访问根结点
	*/
	PostOrderRecursion(pRoot->m_pLeft);
	PostOrderRecursion(pRoot->m_pRight);
	Visit(pRoot);	
}

递归实现最大的好处就是简洁清晰,几行代码就可以实现遍历。但是递归有一个很大的问题,递归需要系统堆栈,一系列函数和返回中所涉及到的参数传递和返回值,都要占用大量系统资源和空间,如果递归的深度很大时,系统根本支撑不了。

递归器在本质上就是堆栈。我们应该记住一句话,一般尾递归(即最后一句话实现递归)和单向递归(函数中只有一个递归调用地方)都可以用循环来避免递归,更复杂的情况则要引入堆栈来进行压栈出栈来改造成非递归。

树遍历就是这么个情况,在函数中出现了两次调用,改造成非递归需要使用使用栈。先从先序遍历说起,首先肯定访问根结点(即1),然后结点1的左子树(也就是2),接着访问2的左子树(4),只有当左子树访问完了,才会访问右子树。这就是一个进栈出栈的过程。

我们可以直接使用C++标准库里的stack容器

给出借助栈,非递归实现树的前序遍历的代码

void PreOrderWithStack(BinaryTreeNode* pRoot)
{
	stack<BinaryTreeNode* >st;
	BinaryTreeNode* p=pRoot;
	while(p!=NULL||!st.empty())
	{
		while(p!=NULL)
		{
			Visit(p);                      //访问根结点
			st.push(p);                    //将结点入栈
			p=p->m_pLeft;                //遍历左子树
		}

		//遍历完了左子树以后,从最后入栈的结点开始,检查它是否有右子树
		if(!st.empty())
		{
			p=st.top();                    //出栈
			st.pop();                
			p=p->m_pRight;                //遍历右子树
		}
	}

}

中序遍历,和前序遍历的区别在于先不访问根结点,而是先遍历左子树,当遍历完了左子树以后,结点开始出栈,访问结点,遍历结点的右子树。所以,很容易的联想到,就visit(p)放在结点出栈的时候访问,也就是下面if语句里面,事实上也的确如此。

void InOrderWithStack(BinaryTreeNode* pRoot)
{
	stack<BinaryTreeNode* >st;
	BinaryTreeNode* p=pRoot;
	while(p!=NULL||!st.empty())
	{
		/*先不访问,只将结点入栈,遍历左子树 */
		while(p!=NULL)
		{
			st.push(p);           
			p=p->m_pLeft;
		}

		//遍历完左子树以后,出栈,访问结点,遍历右子树
		if(!st.empty())
		{
			p=st.top();                      //出栈
			st.pop();
			Visit(p);                       //访问结点
			p=p->m_pRight;                //遍历右子树
		}

	}
}
前序遍历或者中序遍历,有一个共同点,就是访问结点在遍历右子树的前面,所以,很容易的就可以用栈将其解决。但是后序遍历,是先遍历左子树,再遍历右子树、最后访问结点。相比前序和中序遍历要复杂一点。还是以这棵树来看,不管是1的左子树,还是1的右子树都要比根结点1先访问。当然啦,我们还是需要先遍历左子树,依次将1、2、4


压入栈中,左边遍历完成,4应该出栈了,但是还是不能访问,因为4还有右子树(8),8应该在4的前面访问。还有必须保证访问的结点,你不能再访问了。代码里有详尽的注释

void PostOrderWithStack(BinaryTreeNode* pRoot)
{
	stack<BinaryTreeNode*>st;
	BinaryTreeNode* p=pRoot;
	BinaryTreeNode*visitedNode=NULL;          //标记上一次已经访问过的结点
	while(p!=NULL||!st.empty())
	{
		if(p!=NULL)
		{
			st.push(p);                      //只入栈
			p=p->m_pLeft;                 //遍历左子树
		}
		else
		{
			p=st.top();    //得到栈顶的结点,但是不出栈哦~因为你不知道它能不能访问。

                         //如果这结点的右子树不为空,并且它的右子树没有被访问过,接着遍历右子树
			if(p->m_pRight!=NULL&&p->m_pRight!=visitedNode)
			{
				p=p->m_pRight;     //遍历右子树
				st.push(p);              //将右子树的头一个结点入栈。
				p=p->m_pLeft;       //接着遍历右子树的头一个结点的左子树
			}

			//当结点的右子树为空时(因为此时最左边一定已经遍历到底了),或者它的右子树已经访问过了,可以出栈,访问了。
			else
			{
				p=st.top();
				st.pop();                    //出栈
				Visit(p);                    //访问
				visitedNode=p;         //将该结点标记为访问过的结点。
				p=NULL;                 //置为空值
			}
		}
	}
}

最后说一下层序遍历,遍历的顺序是1、2、3、4、5、6,也就是先访问根结点、接着依次访问1的左右孩子、然后再2的左右孩子,3的左右孩子,它不像堆栈那样后进先出,反而有点先进先出。是的,就是队列,先进先出。我们直接借助C++标准库的deque容器,它是双端队列。

void LevelOrder(BinaryTreeNode* pRoot)
{
	if(pRoot==NULL)
		return ;
	deque<BinaryTreeNode*>deq;
	deq.push_back(pRoot);                   //首先将根结点入列

	while(!deq.empty())                        
	{
		BinaryTreeNode* p=deq.front();     //出列
		deq.pop_front();
		Visit(p);                                        //访问结点
		if(p->m_pLeft!=NULL)                //当结点左孩子存在时,加入队列
			deq.push_back(p->m_pLeft);
		if(p->m_pRight!=NULL)             //当结点右孩子存在时,加入队列
			deq.push_back(p->m_pRight);
	}

}

就写到了,自己文字功底有限,有些东西讲的不是很明白,还在代码里面有比较详细的说明,现在给出完整的代码。

#include<iostream>
#include<stack>
#include<deque>
using std::stack;
using std::deque;

struct BinaryTreeNode
{
	int m_nValue;                              //结点的值
	BinaryTreeNode* m_pLeft;                  //结点的左子树
	BinaryTreeNode* m_pRight;                //结点的右子树

	//为了接下来的编程的方便,我给了一个带参数的构造函数
	BinaryTreeNode(int val)
	{
		m_nValue=val;
		m_pLeft=NULL;
		m_pRight=NULL;
	}
};
void ConnectTreeNode(BinaryTreeNode* pNode,BinaryTreeNode* pLeft,BinaryTreeNode* pRight)
{
	if(pNode==NULL)
	{
		printf("父结点为空!\n");
		return ;
	}
	pNode->m_pLeft=pLeft;
	pNode->m_pRight=pRight;
}
void Visit(BinaryTreeNode* pNode)
{
	if(pNode!=NULL)
		printf("%d\t",pNode->m_nValue);
}
void PreOrderRecursion(BinaryTreeNode* pRoot)
{
	if(pRoot==NULL)               //终止条件
		return ;

	/*
	先序遍历就是先访问根结点,
	然后访问左子树,
	最后访问右子树
	*/
	Visit(pRoot);
	PreOrderRecursion(pRoot->m_pLeft);
	PreOrderRecursion(pRoot->m_pRight);
}

void InOrderRecursion(BinaryTreeNode* pRoot)
{
	if(pRoot==NULL)               //终止条件
		return ;

	/*
	中序遍历就是先访问左子树,
	然后访问根结点,
	最后访问右子树
	*/
	InOrderRecursion(pRoot->m_pLeft);
	Visit(pRoot);	
	InOrderRecursion(pRoot->m_pRight);
}

void PostOrderRecursion(BinaryTreeNode* pRoot)
{
	if(pRoot==NULL)               //终止条件
		return ;

	/*
	后序遍历就是先访问左子树,
	然后访问右子树,
	最后访问根结点
	*/
	PostOrderRecursion(pRoot->m_pLeft);
	PostOrderRecursion(pRoot->m_pRight);
	Visit(pRoot);	
}
void PreOrderWithStack(BinaryTreeNode* pRoot)
{
	stack<BinaryTreeNode* >st;
	BinaryTreeNode* p=pRoot;
	while(p!=NULL||!st.empty())              //如果传入的是空树,根本不会进入循环
	{
		while(p!=NULL)
		{
			Visit(p);                      //访问根结点
			st.push(p);                 //将结点入栈
			p=p->m_pLeft;         //遍历左子树
		}

		//遍历完了左子树以后,从最后入栈的结点开始,检查它是否有右子树
		if(!st.empty())
		{
			p=st.top();                 //出栈
			st.pop();
			p=p->m_pRight;       //遍历右子树
		}
	}

}
void InOrderWithStack(BinaryTreeNode* pRoot)
{
	stack<BinaryTreeNode* >st;
	BinaryTreeNode* p=pRoot;
	while(p!=NULL||!st.empty())
	{
		/*先不访问,只将结点入栈,遍历左子树 */
		while(p!=NULL)
		{
			st.push(p);           
			p=p->m_pLeft;
		}

		//遍历完左子树以后,出栈,访问结点,遍历右子树
		if(!st.empty())
		{
			p=st.top();                      //出栈
			st.pop();
			Visit(p);                          //访问结点
			p=p->m_pRight;            //遍历右子树
		}

	}
}

void PostOrderWithStack(BinaryTreeNode* pRoot)
{
	stack<BinaryTreeNode*>st;
	BinaryTreeNode* p=pRoot;
	BinaryTreeNode*visitedNode=NULL;          //标记上一次已经访问过的结点
	while(p!=NULL||!st.empty())
	{
		if(p!=NULL)
		{
			st.push(p);                      //只入栈
			p=p->m_pLeft;              //遍历左子树
		}
		else
		{
			p=st.top();                       //得到栈顶的结点,但是不出栈哦~因为你不知道它能不能访问。

			//如果这结点的右子树不为空,并且它的右子树没有被访问过,接着遍历右子树
			if(p->m_pRight!=NULL&&p->m_pRight!=visitedNode)
			{
				p=p->m_pRight;     //遍历右子树
				st.push(p);              //将右子树的头一个结点入栈。
				p=p->m_pLeft;       //接着遍历右子树的头一个结点的左子树
			}

			//当结点的右子树为空时(因为此时最左边一定已经遍历到底了),或者它的右子树已经访问过了
			//可以出栈,访问了。
			else
			{
				p=st.top();
				st.pop();                    //出栈
				Visit(p);                    //访问
				visitedNode=p;         //将该结点标记为访问过的结点。
				p=NULL;                 //置为空值
			}
		}
	}
}
void LevelOrder(BinaryTreeNode* pRoot)
{
	if(pRoot==NULL)
		return ;
	deque<BinaryTreeNode*>deq;
	deq.push_back(pRoot);                   //首先将根结点入列

	while(!deq.empty())                        
	{
		BinaryTreeNode* p=deq.front();     //出列
		deq.pop_front();
		Visit(p);                                        //访问结点
		if(p->m_pLeft!=NULL)                //当结点左孩子存在时,加入队列
			deq.push_back(p->m_pLeft);
		if(p->m_pRight!=NULL)             //当结点右孩子存在时,加入队列
			deq.push_back(p->m_pRight);
	}

}
void test()
{
	BinaryTreeNode* p1=new BinaryTreeNode(1);
	BinaryTreeNode* p2=new BinaryTreeNode(2);
	BinaryTreeNode* p3=new BinaryTreeNode(3);
	BinaryTreeNode* p4=new BinaryTreeNode(4);
	BinaryTreeNode* p5=new BinaryTreeNode(5);
	BinaryTreeNode* p6=new BinaryTreeNode(6);
	BinaryTreeNode* p7=new BinaryTreeNode(7);
	ConnectTreeNode(p1,p2,p3);
	ConnectTreeNode(p2,p4,p5);
	ConnectTreeNode(p5,p6,p7);
	//PreOrderRecursion(p1);
    //InOrderRecursion(p1);
	//PostOrderRecursion(p1);
	//PreOrderWithStack(p1);
	//InOrderWithStack(p1);
	//PostOrderWithStack(p1);
	LevelOrder(p1);
}
int main()
{
	test();
}





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值