【经典面试题二】二叉树的递归与非递归遍历(前序、中序、后序) - SHERO_Vae - 博客园
二叉树的性质:什么是二叉树(包含满二叉树和完全二叉树)
二叉树遍历:从树的根节点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问仅且一次。
这里有两个关键词:访问和次序。
三种遍历方式 访问节点的顺序是一致的,不同之处在于,有的遍历流程把访问到的节点暂存起来,达成某种条件后再将节点输出。约定, 根节点V, 其左孩子为L, 右孩子为R, 那么遍历顺序可以记为:
前序遍历:到达一个节点后,即刻输出该节点的值,并继续遍历其左右子树。
中序遍历:到达一个节点后,将其暂存,遍历完左子树后,再输出该节点的值,然后遍历右子树
后序遍历:到达一个节点后,将其暂存,遍历完左右子树后,再输出该节点的值
1、前序遍历
基本思想:先访问根结点,再先序遍历左子树,最后再先序遍历右子树即 根—左—右
前序递归遍历的代码实现,如下所示
//前序递归遍历
void PreOrderTraverse(BiTree t)
{
//注意跳出条件
if(t != NULL)
{
//注意访问语句顺序
printf("%c ", t->data);
PreOrderTraverse(t->lchild);
PreOrderTraverse(t->rchild);
}
}
1)前序非递归遍历:
对于任一结点p:
a. 访问结点p,并将结点p入栈;
b. 判断结点p的左孩子是否为空,若为空,则取栈顶结点并进行出栈操作,并将栈顶结点的右孩子置为当前的结点p,循环至a;若不为空,则将p的左孩子置为当前结点p;
c. 直到p为空,并且栈为空,则遍历结束。
//前序非递归遍历
void preOrder2(BinTree *root) //非递归前序遍历
{
stack<BinTree*> s;
BinTree *p=root;
while(p!=NULL||!s.empty())
{
// 现将左子树输出并且压入栈,直到左子树为null
while(p!=NULL) //若当前的节点非空
{
cout<<p->data<<" "; //则输出该节点的值
s.push(p); //该节点压入栈中
p=p->lchild; // 我们继续向左子树前进
}
// 取栈顶结点并进行出栈操作,并将栈顶结点的右孩子置为当前的结点p
if(!s.empty())
{
p=s.top();
s.pop();
p=p->rchild;
}
}
}
2、中序遍历
基本思想:先中序遍历左子树,然后再访问根结点,最后再中序遍历右子树即 左—根—右。
中序遍历迭代代码
//中序递归遍历
void InOrderTraverse(BiTree t)
{
if(t != NULL)
{
InOrderTraverse(t->lchild);
printf("%c ", t->data);
InOrderTraverse(t->rchild);
}
}
2)中序非递归遍历
根据中序遍历的顺序,对于任一结点,优先访问其左孩子,而左孩子结点又可以看做一个根结点,然后继续访问其左孩子结点,直到遇到左孩子结点为空的结点才停止访问,然后按相同的规则访问其右子树。其处理过程如下:
对于任一结点:
a. 若其左孩子不为空,则将p入栈,并将p的左孩子设置为当前的p,然后对当前结点再进行相同的操作;
b. 若其左孩子为空,则取栈顶元素并进行出栈操作,访问该栈顶结点,然后将当前的p置为栈顶结点的右孩子;
c. 直到p为空并且栈为空,则遍历结束。
与先序遍历类似,唯一区别是到达当前节点时 并不直接输出该节点。
void inOrder2(BinTree *root) //非递归中序遍历
{
stack<BinTree*> s;
BinTree *p=root;
while(p!=NULL||!s.empty())
{
while(p!=NULL)
{
s.push(p);
p=p->lchild;
}
if(!s.empty())
{
p=s.top();
cout<<p->data<<" ";
s.pop();
p=p->rchild;
}
}
}
3、后序遍历
基本思想:先后序遍历左子树,然后再后序遍历右子树,最后再访问根结点即左—右—根。
后序递归遍历代码实现,如下所示。
//后序递归遍历
void PostOrderTraverse(BiTree t)
{
if(t != NULL)
{
PostOrderTraverse(t->lchild);
PostOrderTraverse(t->rchild);
printf("%c ", t->data);
}
}
后序遍历的非递归实现是三种遍历方式中最难的一种。因为在后序遍历中,要保证左孩子和右孩子都已被访问,并且左孩子在右孩子之前访问才能访问根结点,这就为流程控制带来了难题。下面介绍一种思路。
思路1:要保证根结点在左孩子和右孩子访问之后才能访问,因此对于任一结点p,先将其入栈。若p不存在左孩子和右孩子,则可以直接访问它,或者p存在左孩子或右孩子,但是其左孩子和右孩子都已经被访问过了,则同样可以直接访问该结点。若非上述两种情况,则将p的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,左孩子在右孩子之前别访问,左孩子和右孩子都在根结点前面被访问。
void postOrder3(BinTree *root) //非递归后序遍历
{
stack<BinTree*> s;
BinTree *cur; //当前结点
BinTree *pre=NULL; //前一次访问的结点
s.push(root);
while(!s.empty())
{
cur=s.top();
if((cur->lchild==NULL&&cur->rchild==NULL)||
(pre!=NULL&&(pre==cur->lchild||pre==cur->rchild)))
{
cout<<cur->data<<" "; //如果当前结点没有孩子结点或者孩子节点都已被访问过
s.pop();
pre=cur;
}
else
{
if(cur->rchild!=NULL)
s.push(cur->rchild);
if(cur->lchild!=NULL)
s.push(cur->lchild);
}
}
}
思路2::对于任一结点P,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,此时该结点出现在栈顶,但是此时不能将其出栈并访问, 因此其右孩子还为被访问。所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,此时可以将其出栈并访问。这样就 保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是 否是第一次出现在栈顶。
void postOrder2(BinTree *root) //非递归后序遍历
{
stack<BTNode*> s;
BinTree *p=root;
BTNode *temp;
while(p!=NULL||!s.empty())
{
while(p!=NULL) //沿左子树一直往下搜索,直至出现没有左子树的结点
{
BTNode *btn=(BTNode *)malloc(sizeof(BTNode));
btn->btnode=p;
btn->isFirst=true;
s.push(btn);
p=p->lchild;
}
if(!s.empty())
{
temp=s.top();
s.pop();
if(temp->isFirst==true) //表示是第一次出现在栈顶
{
temp->isFirst=false;
s.push(temp);
p=temp->btnode->rchild;
}
else //第二次出现在栈顶
{
cout<<temp->btnode->data<<" ";
p=NULL;
}
}
}
}
4、二叉树最大路径和
LeetCode:力扣