树 -- 二叉树遍历方法思路大总结(10种方法)
Posted Y_ZhiWen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树 -- 二叉树遍历方法思路大总结(10种方法)相关的知识,希望对你有一定的参考价值。
遍历是二叉树的一类重要操作,也是二叉树的其它操作和应用的算法基本框架
二叉树(Binary Tree)
- 定义:含有n(n>=0)个结点的有限集合。当n=0时为空二叉树,
在非空二叉树中:有且仅有一个根结点;其余节点划分为两互不相交的子集L和R,其中L和R也是一棵二叉树,分别称为左子树和右子树 - 术语(部分)
- 层次:根为第1层,根的孩子为第2层,依次计数
- 深度(高度):最大层次称为高度
- 度:结点的孩子个数
- 内部结点(分支结点):非叶子结点
- 叶子结点:度为0的结点
- 满二叉树(Full Binary Tree):一棵深度为k且有 2k−1 个结点的二叉树
- 完全二叉树(Complete Binary Tree):深度为k且含有n个结点的二叉树,其每个结点都与深度为k的满二叉树中编号从1至n的结点一一对应。
- 性质:
- 在非空二叉树的第i层最多有 2k−1 个结点(i≥1)。 —- 可用数学归纳法证明
- 深度为k的二叉树最多有 2k−1 —- 基于上面性质,求等比数列和
- 对于任意的一棵二叉树,如果度为0的结点个数为n0,度为2的结点个数为n2,则n0 = n2+1
- 具有n个结点的完全二叉树的深度为「log2n」+1
- 对于含n个结点的完全二叉树中的编号为i(i≤i≤n)的结点:
- 如果i = 1,则该结点为数的根,没有双亲。否则其双亲为⌊i/2⌋
- 如果2i>n,则i结点没有左孩子,否则其左孩子编号为2i
- 如果2i+1>n,则i结点没有右孩子,否则其右孩子编号为2i+1
二叉树的存储结构
顺序存储结构
- 采用数组形式存储,根据上面最后一条性质判定父子关系,容易造成空间浪费,例如:
链式存储结构
二叉链表
typedef struct BiTNode
int data; // 数据域
struct BiTNode *lchild, *rchild; // 左右孩子
BiTNode, BiTree;
三叉链表
typedef struct BiTNode
int data; // 数据域
struct BiTNode *lchild, *rchild, *parent; // 左右孩子 ,及双亲
BiTNode, BiTree;
二叉树的遍历
遍历方式可分为:深度优先遍历,广度优先遍历。
深度优先遍历
而深度优先遍历又可分为:先序遍历、中序遍历、后序遍历。其中又可区分递归遍历跟非递归遍历。
递归遍历
如果L、D、R表示左子树、根、右子树,那么3种算法可表示(都是争对跟结点D而言的):
DLR:先序
LDR:中序
LRD:后序
这里只展示中序递归遍历:
// 中序递归遍历
void InOrderTraverse(BiTree T,Status (*visit)(int elem))
if( T == NULL ) return ;
InOrderTraverse(T->lchild,visit);
visit(T->data);
InOrderTraverse(T->rchild,visit);
非递归遍历:
这里又可分为使用栈和不使用栈的情况
使用栈非递归遍历
- 使用栈的中序非递归遍历
// 从T结点出发,沿左分支不断深入,直到左分支为空的结点,沿途结点入栈S
BiTNode *GoFastLeft(BiTree T,LStack &S)
if( NULL == T ) return NULL;
while( T->lchild != NULL )
Push(S,T);
T = T->lchild;
return T;
void InOrderTraverse(BiTree T, Status (*visit)(int elel))
LStack s; InitStack(s);
BiTree p;
p = GoFastLeft(T,s); // 找到最左下的结点
while(p!=NULL)
visit(p->data);
if( p->rchild != NULL ) // 令p指向其右孩子为根的子树的最左下结点
p = GoFastLeft(p->rchild,s);
else if( !StackEmpty(s) )
Pop(s,p);
else
p = NULL;
示意图:
- 使用栈的先序非递归遍历
根据示意图,可以改成先序非递归遍历,只是将访问语句改变一下位置,代码为:
// 从T结点出发,沿左分支不断深入,直到左分支为空的结点,沿途结点入栈S
BiTNode *GoFastLeft(BiTree T,LStack &S,Status (*visit)(int elem))
if( NULL == T ) return NULL;
while( T->lchild != NULL )
visit(T->data); // ...
Push(S,T);
T = T->lchild;
visit(T->data);// ...
return T;
void InOrderTraverse(BiTree T, Status (*visit)(int elem))
LStack s; InitStack(s);
BiTree p;
p = GoFastLeft(T,s); // 找到最左下的结点
while(p!=NULL)
if( p->rchild != NULL ) // 令p指向其右孩子为根的子树的最左下结点
p = GoFastLeft(p->rchild,s,visit);
else if( !StackEmpty(s) )
Pop(s,p);
else
p = NULL;
- 使用栈的后序非递归遍历
那么怎么改成后序呢??
二叉树的非递归后序遍历算法:
前序、中序、后序的非递归遍历中,要数后序最为麻烦,如果只在栈中保留指向结点的指针,那是不够的,必须有一些额外的信息存放在栈中。方法有很多,这里只举一种,先定义栈结点的数据结构。
typedef struct
Node * p;
int rvisited;
SNode //Node 是二叉树的结点结构,rvisited==1代表p所指向的结点的右结点已被访问过。
lastOrderTraverse(BiTree bt)
//首先,从根节点开始,往左下方走,一直走到头,将路径上的每一个结点入栈。
p = bt;
while(bt)
push(bt, 0); //push到栈中两个信息,一是结点指针,一是其右结点是否被访问过
bt = bt.lchild;
//然后进入循环体
while(!Stack.empty()) //只要栈非空
sn = Stack.getTop(); // sn是栈顶结点
//注意,任意一个结点N,只要他有左孩子,则在N入栈之后,N的左孩子必然也跟着入栈了(这个体现在算法的后半部分),所以当我们拿到栈顶元素的时候,可以确信这个元素要么没有左孩子,要么其左孩子已经被访问过,所以此时我们就不关心它的左孩子了,我们只关心其右孩子。
//若其右孩子已经被访问过,或是该元素没有右孩子,则由后序遍历的定义,此时可以visit这个结点了。
if(!sn.p.rchild || sn.rvisited)
p = pop();
visit(p);
else //若它的右孩子存在且rvisited为0,说明以前还没有动过它的右孩子,于是就去处理一下其右孩子。
//此时我们要从其右孩子结点开始一直往左下方走,直至走到尽头,将这条路径上的所有结点都入栈。
//当然,入栈之前要先将该结点的rvisited设成1,因为其右孩子的入栈意味着它的右孩子必将先于它被访问(这很好理解,因为我们总是从栈顶取出元素来进行visit)。由此可知,下一次该元素再处于栈顶时,其右孩子必然已被visit过了,所以此处可以将rvisited设置为1。
sn.rvisited = 1;
//往左下方走到尽头,将路径上所有元素入栈
p = sn.p.rchild;
while(p != 0)
push(p, 0);
p = p.lchild;
//这一轮循环已结束,刚刚入栈的那些结点我们不必管它了,下一轮循环会将这些结点照顾的很好。
而我自己写的算法则是不断访问左边子树后将其切断,虽然能实现,但是不是很好,这里就不展示。
不使用栈
- 不使用栈的先序非递归遍历
void PreOrderTraverse(TriTree T, Status (*visit)(int elem))
TriTree p, pr;
if( T == NULl ) return ;
p = T;
while(p!=NULL)
visit(p->data);
if(p->lchild != NULL)
p = p->lchild;
else if( p->rchild != NULL )
p = p->rchild;
else
// ★★往回查找,找到第一个有右孩子的p结点,并且该右子树没被访问过(由pr标记),找不到程序结束
while(p!=NULL && (p->rchild==pr||p->rchild==NULL))
pr = p;
p = p->parent;
if( p!=NULL ) p = p->rchild;
- 不使用栈的中序非递归遍历
TriTree GoFastLeft(TriTree T)
if( T == NULL ) return NULL;
while(T->lchild != NULL )
T = T->lchild;
return T;
void InOrder(TriTree PT, void (*visit)(TElemType))
TriTree t;
if( PT == NULL ) return ;
t = GoFastLeft(PT);
while(t != NULL)
visit(t->data);
if( t->rchild != NULL)
t = GoFastLeft(t->rchild);
else
// 右元素为NULL,返回上一层
if( t->parent != NULL )
// 如果是其双亲的左孩子,说明双亲还没visit操作
if( t->parent->lchild == t )
t = t->parent;
else
// 如果是其双亲的右孩子,说明双亲已经被visit操作,继续向上
while( t->parent != NULL && t->parent->rchild == t )
t = t->parent;
if( t->parent == NULL )
t = NULL;
else
t = t->parent;
else
t = NULL;
- 不使用栈的后序非递归遍历
typedef struct TriTNode
TElemType data;
struct TriTNode *lchild, *rchild, *parent;
int mark; // 标志域(在三叉链表的结点中增设一个标志域
// (mark取值0,1或2)以区分在遍历过程中到达该结点
// 时应继续向左或向右或访问该结点。)
TriTNode, *TriTree;
void PostOrder(TriTree T, void (*visit)(TElemType))
TriTree t;
if( T == NULL ) return ;
t = T;
while( t!= NULL )
if( t->mark == 0 ) // 向左(左边是否为空)
if( t->lchild == NULL && t->rchild != NULL ) // 左边为空,直接标记为1,并访问操作,
t->mark = 1; // 表示待visit操作,并向上
t = t->rchild;
else if( t->lchild == NULL && t->rchild == NULL ) // 左右为空,直接访问并向上
t->mark = 1;
visit(t->data);
t = t->parent;
else
t->mark = 2; // 进入左边,标记表示待向右
t = t->lchild;
else if( t->mark == 2 ) // 向右操作 (可能为空)
if( t->rchild == NULL )
t->mark = 1; // 表示待visit操作,并向上
visit(t->data);
t = t->parent;
else
t->mark = 1;
t = t->rchild;
else if( t->mark == 1 ) //visit操作,并向上
visit(t->data);
t = t->parent;
广度优先遍历
void LevelOrderTraverse(BiTree T, Status (*visit)(int elem))
if( T == NULL ) return ;
LQueue Q; InitQueue(Q);
BiTree p = T;
visit(T->data);
EnQueue(Q,p);
while( !QueueEmpty(Q) )
DeQueue(Q,p);
if(p->lchild != NULL) // 处理左孩子
visit(T->lchild->data);
EnQueue(Q,p->lchild);
if(p->rchild != NULL) // 处理右孩子
visit(T->rchild->data);
EnQueue(Q,p->rchild);
最后,如果有哪里错误或者不足,希望能够指出,谢谢!
以上是关于树 -- 二叉树遍历方法思路大总结(10种方法)的主要内容,如果未能解决你的问题,请参考以下文章