先根遍历和先序遍历是同一个概念,只是叫法不同,也叫前序遍历,是一种节点遍历算法,指的是按照“根节点->左子树->右子树”的顺序遍历每个节点,可记做根左右,一般用于树或者森林中。
一、先根遍历和先序遍历
先根遍历和先序遍历是同一个概念,只是叫法不同,也叫前序遍历,是一种节点遍历算法,指的是按照“根节点->左子树->右子树”的顺序遍历每个节点,可记做根左右,一般用于树或者森林中。
不过严蔚敏老师在文章中写到自己的理解:我的看法是恐怕不能那么理解,“根”和“序”不完全一致。对二叉树可以混用,有的书上是“先根,中根,后根”遍历,有的书上是“先序,中序,后序”遍历,但是森林只有“先序和中序”遍历,而树只有“先根和后根遍历”。不过以上仅仅是我个人的看法,我认为应该如此严格区分,也有其它教材和我的理解不同。
事实上,在二叉树遍历的术语中,“先根遍历”和“先序遍历”通常是等价的,指的是按照“根节点->左子树->右子树”的顺序遍历每个节点。因此,严老师所说的“有的书上是‘先根、中根、后根’遍历,有的书上是‘先序、中序、后序’遍历”,实际上是指同样的遍历顺序。这个问题可能源自于不同的译者和学者之间对于术语的理解和使用不够一致,导致了这种混淆。
但是,对于森林和树来说,情况可能略有不同。树是一棵只有一个根节点的二叉树,而森林则由多个不相交的树构成。严老师提到,森林只有“先序和中序”遍历,而树只有“先根和后根遍历”。这里的区分可能是因为,对于树的遍历而言,先根遍历和后根遍历是最自然的选择,而对于森林而言,由于每个子树之间可能并不相关,因此只有先序和中序遍历比较适合。
总的来说,术语的使用是一个比较灵活的问题,不同的学者和作者可能存在不同的理解和表述方式。在实际应用中,应该尽可能在上下文中明确和一致地使用术语,以避免产生歧义和误解。
二、二叉树的前序遍历、中序遍历、后序遍历
二叉树是一种非常重要的数据结构,很多其它数据结构都是基于二叉树的基础演变而来的。对于二叉树,有前序、中序以及后序三种遍历方法。因为树的定义本身就 是递归定义,因此采用递归的方法去实现树的三种遍历不仅容易理解而且代码很简洁。而对于树的遍历若采用非递归的方法,就要采用栈去模拟实现。对于三种遍历,前序和中序遍历的非递归算法都很容易实现,非递归后序遍历实现起来相对来说要难一点。
1、前序遍历
前序遍历按照“根结点-左孩子-右孩子”的顺序进行访问。
递归实现:
void preOrder1(BinTree *root) //递归前序遍历
{
if(root!=NULL)
{
cout<<root->data<<" ";
preOrder1(root->lchild);
preOrder1(root->rchild);
}
}
非递归实现:
根据前序遍历访问的顺序,优先访问根结点,然后再分别访问左孩子和右孩子。即对于任一结点,其可看做是根结点,因此可以直接访问,访问完之后,若其左孩子不为空,按相同规则访问它的左子树;当访问其左子树时,再访问它的右子树。因此其处理过程如下:
对于任一结点P:
- 访问结点P,并将结点P入栈;
- 判断结点P的左孩子是否为空,若为空,则取栈顶结点并进行出栈操作,并将栈顶结点的右孩子置为当前的结点P,循环至1);若不为空,则将P的左孩子置为当前的结点P;
- 直到P为NULL并且栈为空,则遍历结束。
void preOrder2(BinTree *root) //非递归前序遍历
{
stack<BinTree*> s;
BinTree *p=root;
while(p!=NULL||!s.empty())
{
while(p!=NULL)
{
cout<<p->data<<" ";
s.push(p);
p=p->lchild;
}
if(!s.empty())
{
p=s.较好();
s.pop();
p=p->rchild;
}
}
}
2、中序遍历
中序遍历按照“左孩子-根结点-右孩子”的顺序进行访问。
递归实现:
void inOrder1(BinTree *root) //递归中序遍历
{
if(root!=NULL)
{
inOrder1(root->lchild);
cout<<root->data<<" ";
inOrder1(root->rchild);
}
}
非递归实现:
根据中序遍历的顺序,对于任一结点,优先访问其左孩子,而左孩子结点又可以看做一根结点,然后继续访问其左孩子结点,直到遇到左孩子结点为空的结点才进行访问,然后按相同的规则访问其右子树。因此其处理过程如下:
对于任一结点P:
- 若其左孩子不为空,则将P入栈并将P的左孩子置为当前的P,然后对当前结点P再进行相同的处理;
- 若其左孩子为空,则取栈顶元素并进行出栈操作,访问该栈顶结点,然后将当前的P置为栈顶结点的右孩子;
- 直到P为NULL并且栈为空则遍历结束。
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.较好();
cout<<p->data<<" ";
s.pop();
p=p->rchild;
}
}
}
3、后序遍历
后序遍历按照“左孩子-右孩子-根结点”的顺序进行访问。
递归实现:
void postOrder1(BinTree *root) //递归后序遍历
{
if(root!=NULL)
{
postOrder1(root->lchild);
postOrder1(root->rchild);
cout<<root->data<<" ";
}
}
非递归实现:
后序遍历的非递归实现是三种遍历方式中最难的一种。因为在后序遍历中,要保证左孩子和右孩子都已被访问并且左孩子在右孩子前访问才能访问根结点,这就为流程的控制带来了难题。下面介绍两种思路。
- 名列前茅种思路:对于任一结点P,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,此时该结点出现在栈顶,但是此时不能将其出栈并访问, 因此其右孩子还为被访问。所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,此时可以将其出栈并访问。这样就 保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是 否是名列前茅次出现在栈顶。
- 第二种思路:要保证根结点在左孩子和右孩子访问之后才能访问,因此对于任一结点P,先将其入栈。如果P不存在左孩子和右孩子,则可以直接访问它;或者P存 在左孩子或者右孩子,但是其左孩子和右孩子都已被访问过了,则同样可以直接访问该结点。若非上述两种情况,则将P的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,左孩子在右孩子前面被访问,左孩子和右孩子都在根结点前面被访问。
延伸阅读1:二叉树是什么
树(Tree)是n(n>=0)个结点的有限集。n=0时称为空树。在任意一颗非空树中:
- 有且仅有一个特定的称为根(Root)的结点;
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tn,其中每一个集合本身又是一棵树,并且称为根的子树。