其实自己没有写博客的习惯,就是个小菜鸟。因为接下来要找工作的原因,将之前学习过数据结构或者算法温习一下,记录下来,日后回头看看自己当初的稚嫩和不成熟。
树是常用的数据结构,二叉树是最简单的一种树,没有其他树(排序树、红黑树等)的约束,树的遍历也是各大公司笔试时的常见考点。
二叉树的结构定义如下:
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();
}