遍历
先序遍历
从树根开始绕着整棵树的外围转一圈,经过结点的顺序就是先序遍历的顺序
先序遍历结果:ABDHIEJCFKG
中序遍历
中序遍历可以想象成,按树画好的左右位置投影下来就可以了
中序遍历结果:HDIBEJAFKCG
后序遍历
后序遍历就像是剪葡萄,我们要把一串葡萄剪成一颗一颗的。
还记得我们先序遍历绕圈的路线么?
就是围着树的外围绕一圈,如果发现一剪刀就能剪下的葡萄(必须是一颗葡萄),就把它剪下来,组成的就是后序遍历了。
后序遍历结果:HIDJEBKFGCA
让我们来看下动画
层序遍历
层序遍历太简单了,就是按照一层一层的顺序,从左到右写下来就行了。
后序遍历结果:ABCDEFGHIJK
算法思想:
用一个队列保存被访问的当前节点的左右孩子以实现层次遍历
在进行层次遍历的时候,设置一个队列结构,遍历从二叉树的根节点开始,首先将根节点指针入队列,然后从队头取出一个元素,每取一个元素,执行下面两个操作:
- 访问该元素所指向的节点
- 若该元素所指节点的左右孩子节点非空,则将该元素所指节点的左孩子指针和右孩子指针顺序入队。此过程不断进行,当队列为空时,二叉树的层次遍历结束。
图:
二叉树的深度优先遍历DFS和广度优先遍历BFS:
-
DFS深度优先遍历:
- 从根节点出发,沿着左子树进行纵向遍历,知道找到叶子节点为止。然后回溯到前一个节点,进行右子树节点的遍历,知道遍历完所有可达节点为止
- 利用数据结构“栈”,父节点入栈,父节点出栈,先右子节点入栈,后左子节点入栈。递归遍历全部节点。
- 前序遍历、中序遍历、后续遍历
-
BFS广度优先遍历:
- 从根节点出发,在横向遍历二叉树层段节点的基础上纵向遍历二叉树的层次。
- 利用数据结构“队列”,父节点入队,父节点出队列,先左子节点入队,后右子节点入队。递归遍历全部节点。
- 层次遍历
深入理解三种遍历
来,让我们先把所有空结点都补上。
还记得我们先序和后序遍历时候跑的顺序么?按照这个顺序再跑一次,就是围着树的外围跑一整圈。
让我们来理解一下绕着外围跑一整圈的真正含义是:遍历所有结点时,都先往左孩子走,再往右孩子走。
观察一下,你有什么发现?
有没有发现,除了根结点和空结点,其他所有结点都有三个箭头指向它。
一个是从它的父节点指向它,一个是从它的左孩子指向它,一个是从它的右孩子指向它。
一个结点有三个箭头指向它,说明每个结点都被经过了三遍。一遍是从它的父节点来的时候,一遍是从它的左孩子返回时,一遍是从它的右孩子返回时。
其实我们在用递归算法实现二叉树的遍历的时候,不管是先序中序还是后序,程序都是按照上面那个顺序跑遍所有结点的。
先序中序和后序唯一的不同就是,在经过结点的三次中,哪次访问(输出或者打印或者做其他操作)了这个结点。有点像大禹治水三过家门,他会选择一次进去。
先序遍历顾名思义,就是在第一次经过这个结点的时候访问了它。就是从父节点来的这个箭头的时候,访问了它。
中序遍历也和名字一样,就是在第二次经过这个结点的时候访问了它。就是从左孩子返回的这个箭头的时候,访问了它。
后序遍历,就是在第三次经过这个结点的时候访问了它。就是从右孩子返回的这个箭头的时候,访问了它。
其实不管是前序中序还是后序,在程序里跑的时候都是按照同样的顺序跑的,每个结点经过三遍,第几遍访问这个结点了,就叫什么序遍历。
下面做一个实例吧
代码实现加以理解
基础实例图(下面代码均围绕此展开)
我们先用代码基本实现一下遍历
void TraverseBiTree(BiTree T)
{
if (T == NULL)
return ;
TraverseBiTree(T->lChild);
TraverseBiTree(T->rChlid);
}
这个递归就能实现当遇到空指针时候返回,记住,每个结点先往左孩子走,再往右孩子走这个顺序。
如果我们想实现先序遍历,只需要在第一次经过这个结点的时候访问(输出)他就可以了,只需要加上一句printf。
//先序遍历二叉树
void TraverseBiTree(BiTree T)
{
if (T == NULL)
return ;
printf("%c ", T->data);
TraverseBiTree(T->lChild);
TraverseBiTree(T->rChlid);
}
中序遍历,就是在第二次经过这个结点的时候访问它。
//中序遍历二叉树
void InOrderBiTree(BiTree T)
{
if (T == NULL)
return ;
InOrderBiTree(T->lChild);
printf("%c ", T->data);
InOrderBiTree(T->rChlid);
}
后序遍历,就是在第三次经过这个结点的时候访问它。
//后序遍历二叉树
void PostOrderBiTree(BiTree T)
{
if (T == NULL)
return ;
PostOrderBiTree(T->lChild);
PostOrderBiTree(T->rChlid);
printf("%c ", T->data);
}
我们可以看到,差别仅仅是printf出现的位置,也就是我们访问结点的位置,在递归算法中其他并没有差别。
前序遍历的递推公式:
p
r
e
O
r
d
e
r
(
r
)
=
p
r
i
n
t
r
−
>
p
r
e
O
r
d
e
r
(
r
−
>
l
e
f
t
)
−
>
p
r
e
O
r
d
e
r
(
r
−
>
r
i
g
h
t
)
preOrder(r) = print r->preOrder(r->left)->preOrder(r->right)
preOrder(r)=printr−>preOrder(r−>left)−>preOrder(r−>right)
中序遍历的递推公式:
i
n
O
r
d
e
r
(
r
)
=
i
n
O
r
d
e
r
(
r
−
>
l
e
f
t
)
−
>
p
r
i
n
t
r
−
>
i
n
O
r
d
e
r
(
r
−
>
r
i
g
h
t
)
inOrder(r) = inOrder(r->left)->print r->inOrder(r->right)
inOrder(r)=inOrder(r−>left)−>printr−>inOrder(r−>right)
后序遍历的递推公式:
p
o
s
t
O
r
d
e
r
(
r
)
=
p
o
s
t
O
r
d
e
r
(
r
−
>
l
e
f
t
)
−
>
p
o
s
t
O
r
d
e
r
(
r
−
>
r
i
g
h
t
)
−
>
p
r
i
n
t
r
postOrder(r) = postOrder(r->left)->postOrder(r->right)->print r
postOrder(r)=postOrder(r−>left)−>postOrder(r−>right)−>printr
二叉树遍历的时间复杂度:每个节点最多会被访问两次,所以遍历操作的时间复杂度,跟节点的个数 n 成正比,也就是说二叉树遍历的时间复杂度是O(n)。
c语言实现
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
typedef char ElemType; //数据类型
//定义二叉树结构
typedef struct BiTreeNode
{
ElemType data; //数据域
struct BiTreeNode *lChild;
struct BiTreeNode *rChlid;
} BiTreeNode, *BiTree;
//先序创建二叉树
void CreateBiTree(BiTree *T)//要改变指针,所以要把指针的地址传进来
{
ElemType ch;
scanf("%c", &ch);//注意数据类型
getchar();//吸收空格或者回车
if (ch == '#')
*T = NULL;
else
{
*T = (BiTree)malloc(sizeof(BiTreeNode));
if (!(*T))//检查是否分配成功
exit(-1);
(*T)->data = ch;
CreateBiTree(&(*T)->lChild);//printf("输入%d的左孩子:", ch);
CreateBiTree(&(*T)->rChlid);//printf("输入%d的右孩子:", ch);
}
}
//先序遍历二叉树
void TraverseBiTree(BiTree T)
{
if (T == NULL)
return ;
printf("%c ", T->data);
TraverseBiTree(T->lChild);
TraverseBiTree(T->rChlid);
}
//中序遍历二叉树
void InOrderBiTree(BiTree T)
{
if (T == NULL)
return ;
InOrderBiTree(T->lChild);
printf("%c ", T->data);
InOrderBiTree(T->rChlid);
}
//后序遍历二叉树
void PostOrderBiTree(BiTree T)
{
if (T == NULL)
return ;
PostOrderBiTree(T->lChild);
PostOrderBiTree(T->rChlid);
printf("%c ", T->data);
}
//主函数
int main(void)
{
BiTree T;
printf("请输入先序遍历顺序下各个结点的值,#表示没有结点:\n");
CreateBiTree(&T);
printf("先序遍历二叉树:\n");
TraverseBiTree(T);
printf("\n");
printf("中序遍历二叉树:\n");
InOrderBiTree(T);
printf("\n");
printf("后序遍历二叉树:\n");
PostOrderBiTree(T);
printf("\n");
return 0;
}
c++实现
#include<iostream>
using namespace std;
typedef char ElemType; //数据类型
typedef struct BiTreeNode//定义结构体
{
ElemType data; //数据域
struct BiTreeNode *lChild;//左孩子
struct BiTreeNode *rChlid;//右孩子
} BiTreeNode, *BiTree;
//先序创建二叉树
void CreateBiTree(BiTree &T)//要改变指针,C++可以把指针的引用传进来
{
ElemType ch;
cin >> ch;
if (ch == '#')
T = NULL;
else
{
T = new BiTreeNode;
T->data = ch;
CreateBiTree(T->lChild);//cout<<"输入"<<ch<<"的左孩子:" ;
CreateBiTree(T->rChlid);//cout<<"输入"<<ch<<"的右孩子:" ;
}
}
//先序遍历二叉树
void TraverseBiTree(BiTree T)
{
if (T == NULL)
return ;
cout << T->data <<" ";
TraverseBiTree(T->lChild);
TraverseBiTree(T->rChlid);
}
//中序遍历二叉树
void InOrderBiTree(BiTree T)
{
if (T == NULL)
return ;
InOrderBiTree(T->lChild);
cout << T->data <<" ";
InOrderBiTree(T->rChlid);
}
//后序遍历二叉树
void PostOrderBiTree(BiTree T)
{
if (T == NULL)
return ;
PostOrderBiTree(T->lChild);
PostOrderBiTree(T->rChlid);
cout << T->data <<" ";
}
int main(void)
{
BiTree T;
cout << "请输入先序遍历顺序下各个结点的值,#表示没有结点:" << endl;
CreateBiTree(T);
cout<<"先序遍历二叉树:"<<endl;
TraverseBiTree(T);
cout<<endl;
cout<<"中序遍历二叉树:"<<endl;
InOrderBiTree(T);
cout<<endl;
cout<<"后序遍历二叉树:"<<endl;
PostOrderBiTree(T);
cout<<endl;
return 0;
}
题目
1、已知一颗二叉树的前序遍历序列为ABCDEF,中序遍历为CBAEDF,请问这棵二叉树的后续遍历是?
- 三种遍历都是从根节点开始的:
- 前序遍历的结果是ABCDEF,所以根节点是
A
。 - 中序遍历的结果为CB
A
EDF,所以得出了A的左右节点:
- 前序遍历的结果是ABCDEF,所以根节点是
- 因为前序遍历A
BC
DEF, 先打印B
(A的后面就马上打印了B
),所以B一定是A的左孩子,而C只能是B的孩子。 - 因为中序遍历
CB
AEDF, C在B前面打印,所以C是B的左孩子
- 因为前序遍历ABC
DEF
。所以D是A的右孩子。EF是D的子孙。(注意,它们中有一个不一定是孩子,有可能是孙子) - 因为中序遍历CBA
EDF
, 由于E在D的左侧,F在右侧。所以可以确定E是D的左孩子,F是D的右孩子。
所以后序遍历为:CBEFDA
2、二叉树的中序遍历是ABCDEFG,后序遍历是BDCAFGE,求前序遍历。
- 因为后续遍历是BDCAFGE,所以E是根节点。前序遍历的首字母是E
- 于是中序遍历分为两棵树ABCD和FG。接下来判断E的右孩子是F还是G
- 中序遍历FG,因为是打印左孩子—>自己---->右孩子,所以F是G的左孩子。
* 对于中序遍历
ABCD
EFG,与后序遍历BDCAFGE
- 对于中序遍历A
BCD
EFG,因为后序遍历BDC
AFGE,