Fork me on GitHub

二叉树学习笔记(一)之遍历二叉树

前言

考试月了,得好好学习下数据结构,本文主要总结了二叉树的四种遍历方法以及所对应的递归与非递归实现方式

二叉树基础知识

概念

  • 二叉树定义:含有n(n>=0)个节点的有限集合
  • 非空二叉树中:
    • 有且仅有一个根节点
    • 其余节点划分为两个互不相交的子集L和R,L称为左子树,R称为右子树
  • 任一结点的左、右子树的根称为该结点的左、右孩子,反过来,该结点称为孩子的双亲
  • 度:结点的孩子个数
  • 叶子结点:度为0 的结点
  • 非叶子结点:度大于0,也称为内部结点或分支结点
  • 二叉树的深度(或高度):结点的最大层次称为二叉树的深度(或高度)。(所谓层次,根节点即为第一层,以此类推)

二叉树的性质

  1. 在非空二叉树的第 i 层上最多右 2^(i-1) 个结点(i>=1)
  2. 深度为 k 的二叉树最多有 2^k - 1 个结点(k>=1)
  3. 对于任意一颗二叉树,如果度为 0 的结点个数为 n0 ,度为 2 的结点个数为 n2,则 n0 = n2+1
  4. 具有 n 个结点的完全二叉树的深度 k = [log2n] + 1
  5. 对于含 n 个结点的完全二叉树中编号为 i (1<=i<=n) 的结点
    1. 如果 i = 1,则 i 结点是这颗完全二叉树的根,没有双亲;否则其双亲的编号为 [i/2]
    2. 如果 2i>n,则 i 结点没有左孩子;否则其左孩子的编号为 2i
    3. 如果 2i+1>n,则 i 结点没有右孩子;否则其右孩子的编号为 2i+1

满二叉树与完全二叉树

  • 满二叉树:深度为 k 且有 2^k - 1 个结点的二叉树

  • 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。深度为K且含 n 个结点的二叉树,如果其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应,则称之为完全二叉树。换句话讲,在一棵二叉树中,除最后一层外,若其余层都是满的,并且最后一层或者是满的,或者是在右边缺少连续若干节点,则此二叉树为完全二叉树。

二叉树的存储结构

顺序存储

1
2
3
4
5
6
// 一维数组实现
typedef char TElemType; // 假设结点元素类型为字符
typedef struct {
TElemType * elem; // 0号单元闲置
int lastIndex; // 二叉树最后一个结点的编号
} SqBiTree;

链式存储

  • 二叉链表
1
2
3
4
typedef struct BiTNode{
TElemType data; // 数据域
struct BiTNode *lchild,*rchild; // 左、右孩子指针域
} BiTNode,*BiTNode;
  • 三叉链表
1
2
3
4
typedef struct TriTNode{
TElemType data; // 数据域
struct TriTNode *parent,*lchild,*rchild;// 双亲左、右孩子指针域
} TriTNode,*TriTNode;

二叉树的遍历

首先来认识一下遍历

遍历:树的遍历(也称为树的搜索)是图的遍历的一种,指的是按照某种规则,不重复地访问某种树的所有结点,使得每个结点被且仅被访问的过程。具体的访问操作可能是检查节点的值、更新节点的值等。二叉树的遍历也适用于其它树的遍历。

遍历是二叉树的一类重要操作,也是二叉树的其他一些操作和各种应用算法的基本框架。

遍历有两种类别,一种是深度优先遍历,另一种是广度优先遍历

  • 深度优先遍历:先访问子节点,再访问父节点,最后是第二个子节点

    • 先序遍历:VLR,即根结点->左结点->右节点
    • 中序遍历:LVR,即左结点->根结点->右节点
    • 后序遍历:LRV,即左结点->右结点->根节点
  • 广度优先遍历:先访问第一个子节点,再访问第二个子节点,最后访问父节点,二叉树的广度优先遍历就是层次遍历

遍历.jpg

由于从给定的某个节点出发,有多个可以前往的下一个节点(树不是线性数据结构),所以在顺序计算(即非并行计算)的情况下,只能推迟对某些节点的访问——即以某种方式保存起来以便稍后再访问。常见的做法是采用栈(LIFO)或队列(FIFO)。由于树本身是一种自我引用(即递归定义)的数据结构,因此很自然也可以用递归方式。

所以,下面我重点总结了这两种不同的遍历实现方式。

注:本文所讲的数据结构均为C语言版

先序遍历

递归遍历

1
2
3
4
5
6
7
8
9
Status PreOrderTraverse(BiTree T,Status(*visit)(TElemType e)){
if(NULL == T) return OK;
if(ERROR == visit(T->data))
return ERROR; //访问结点的数据域
if(ERROR == PreOrderTraverse(T->lchild,visit))
return ERROR; //递归遍历T的左子树
return PreOrderTraverse(T->rchild,visit);//递归遍历T的右子树

}

非递归遍历

1.使用栈的非递归先序遍历算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**********
二叉链表类型定义:
typedef struct BiTNode {
TElemType data;
struct BiTNode *lchild,*rchild;
} BiTNode, *BiTree;
栈类型Stack的相关定义:
typedef BiTree SElemType; // 栈的元素类型
Status InitStack(Stack &S); // 初始化栈
Status StackEmpty(Stack S); // 判栈空
Status Push(Stack &S, SElemType e); // 进栈
Status Pop(Stack &S, SElemType &e); // 出栈
Status GetTop(Stack S, SElemType &e); // 取栈顶元素
**********/
void PreOrder(BiTree T, void (*visit)(TElemType))
/*对每个结点的元素域data调用函数visit进行访问 */
{
Stack S; InitStack(S);
BiTree p = T;
// 先序访问根节点,遍历左节点 ,左节点入栈
// StackEmpty(Stack S);S为空返回true反之false;
// 当栈不空或 p非空时
while(!StackEmpty(S) || p!=NULL){
while(p!=NULL){
visit(p->data);
Push(S,p);
p = p->lchild;
}
if(!StackEmpty(S)){
Pop(S,p); // 执行完 p指向S出栈的元素
p = p->rchild;
}
}
}

先序遍历.jpg

2.不使用栈,使用三叉链表的非递归先序遍历算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**********  
三叉链表类型定义:
typedef struct TriTNode {
TElemType data;
struct TriTNode *parent, *lchild, *rchild;
} TriTNode, *TriTree;
**********/
void PreOrder(BiTree T, void (*visit)(TElemType)){
TriTree p, pr;
if(T!=NULL){
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结点
//找不到则结束
do{
pr = p;
p = p->parent;
}while(p!=NULL&&(p->rchild==pr||NULL==p->rchild)) //p有右孩子但不是pr,结束循环
if(p) p = p->rchild;//找到后,p指向右孩子结点
}
}
}
}

中序遍历

递归遍历

1
2
3
4
5
6
7
8
9
Status InOrderTraverse(BiTree T,Status(*visit)(TElemType e)){
if(NULL == T) return OK;
if(ERROR == InOrderTraverse(T->lchild,visit))
return ERROR; //递归遍历T的左子树
if(ERROR == visit(T->data))
return ERROR; //访问结点的数据域
return InOrderTraverse(T->rchild,visit);//递归遍历T的右子树

}

非递归遍历

1.使用栈的非递归中序遍历算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**********
二叉链表类型定义:
typedef struct BiTNode {
TElemType data;
struct BiTNode *lchild,*rchild;
} BiTNode, *BiTree;
栈类型Stack的相关定义:
typedef BiTree SElemType; // 栈的元素类型
Status InitStack(Stack &S); // 初始化栈
Status StackEmpty(Stack S); // 判栈空
Status Push(Stack &S, SElemType e); // 进栈
Status Pop(Stack &S, SElemType &e); // 出栈
Status GetTop(Stack S, SElemType &e); // 取栈顶元素
**********/
BiTNode *GoFarLeft(BiTree T,Stack &S){
//从T结点出发,沿左分支走到底,沿途结点的指针入栈S,返回左上结点的指针
if(!T) return NULL;
while(T->lchild!=NULL){
Push(S,T);
T = T->lchild;
}
return T;
}
void InOrderTraverse(BiTree T,void(*visit)(TElemType e)){
Stack S; InitStack(S);
BiTree p;
p = GoFarLeft(T, S);//找到最左下的结点,并将沿途结点的指针入栈S
while(p!=NULL){
visit(p->data);//访问结点
if(p->rchild!=NULL){
//令p指向其右孩子为根的子树的最左下结点
p = GoFarLeft(p->rchild, S);
}
else if(!StackEmpty(S)){
//栈不空时退栈
Pop(S,p);
}
else p = NULL;//栈空表明遍历结束
}
}

中序遍历.jpg

2.不使用栈,使用三叉链表的非递归中序遍历算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**********  
三叉链表类型定义:
typedef struct TriTNode {
TElemType data;
struct TriTNode *parent, *lchild, *rchild;
} TriTNode, *TriTree;
**********/
void InOrder(TriTree PT, void (*visit)(TElemType)){
TriTree p=PT, pr;
while(NULL != p) {
if (p->lchild != NULL) {
p = p->lchild; //寻找最左下结点
}
else {
visit(p->data);
//当前节点左子树为空,但右子树不一定为空,
//所以该节点可能是这个子树的根节点,要先访问,
//如果有右节点,才能在右节点之前访问
if (p->rchild != NULL) {
//若有右子树,转到该子树,继续寻找最左下结点
p =p->rchild;
}
else {
//执行到这里
//p 要不是最左下节点,要不是没有孩子的右节点
//其实也就是叶子节点
pr = p; //否则返回其父亲
p = p->parent;
while (p != NULL && (p->lchild != pr || NULL == p->rchild))//若其不是从左子树回溯来的,或左结点的父亲并没有右孩子
{
if (p->lchild == pr) {//若最左结点的父亲并没有右孩子
visit(p->data);//直接访问父亲
}
pr = p; //父亲已被访问,故返回上一级
p = p->parent;
//该while循环沿双亲链一直查找,若无右孩子则访问
//直至找到第一个有右孩子的结点为止
//(但不访问该结点,留给下步if语句访问
}
if (NULL != p) {
//访问父亲,并转到右孩子
//(经上步while处理,可以确定此时p有右孩子)
visit(p->data);
p = p->rchild;
}
}
}
}
}

后序遍历

递归遍历

1
2
3
4
5
6
7
8
9
Status PostOrderTraverse(BiTree T,Status(*visit)(TElemType e)){
if(NULL == T) return OK;
if(ERROR == PostOrderTraverse(T->lchild,visit))
return ERROR; //递归遍历T的左子树
if(ERROR == PostOrderTraverse(T->rchild,visit))
return ERROR; //递归遍历T的右子树
return visit(T->data);//访问结点的数据域

}

非递归遍历

1.使用栈的非递归后序遍历算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**********
二叉链表类型定义:
typedef struct BiTNode {
TElemType data;
struct BiTNode *lchild,*rchild;
} BiTNode, *BiTree;
为分辨后序遍历时两次进栈的不同返回点,需在指针进栈时同时将一个标志进栈
typedef struct {
struct BiTNode *ptr; // 二叉树结点的指针类型
int tag; // 标志 值为 0 或 1
} SElemType; // 栈的元素类型
栈类型Stack的相关定义:
typedef BiTree SElemType; // 栈的元素类型
Status InitStack(Stack &S); // 初始化栈
Status StackEmpty(Stack S); // 判栈空
Status Push(Stack &S, SElemType e); // 进栈
Status Pop(Stack &S, SElemType &e); // 出栈
Status GetTop(Stack S, SElemType &e); // 取栈顶元素
**********/
void PostOrder(BiTree T, void (*visit)(TElemType))
/* 使用栈,非递归后序遍历二叉树T, */
/* 对每个结点的元素域data调用函数visit */
{
Stack S; InitStack(S);
SElemType e; BiTree p=T;
e.tag=0;
while((!StackEmpty(S)||p==T)&&T!=NULL){
while(p){// 这个小 while循环的目的是先遍历每个子树的左节点到底,并将沿途的左节点入栈
e.ptr=p;
e.tag=0;
Push(S,e); // 入栈
p=p->lchild;
}// 小while结束,此时的栈顶元素也就是要访问的起始节点
if(!StackEmpty(S)){ // tag==1,e为第二次出现在栈顶
Pop(S,e); // 取出栈顶元素,并返回给e,e指向出栈元素,方便下面的tag值判断
if(e.tag == 1) {
p=e.ptr;
visit(p->data);// 访问该结点
p = NULL;
}
else {
p=e.ptr;
if(p->rchild){ // 如果存在右子树,设tag == 1
e.tag=1;
Push(S,e); // 重新入栈,它是根节点,留着等下再访问
p=p->rchild; // 使 p指向右子树
}
else{//没有右子树 ,直接访问该节点
p=e.ptr;
visit(p->data);
p = NULL;
}
}
}
else{//栈空则 p为NULL
p=NULL;
}
}//大while结束
}

后序遍历.jpg

2.不使用栈,使用三叉链表的非递归后序遍历算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**********  
在三叉链表的结点中增设一个标志域 (mark取值0,1或2)以区分遍历过程中到达该结点时应继续向左或向右或访问该结点
带标志域的三叉链表类型定义:
typedef struct TriTNode {
TElemType data;
struct TriTNode *lchild, *rchild, *parent;
int mark; // 标志域
} TriTNode, *TriTree;
**********/
void PostOrder(TriTree T, void (*visit)(TElemType))
/* 不使用栈,非递归后序遍历二叉树T, */
/* 对每个结点的元素域data调用函数visit */
{ //mark == 0 向左访问,1 访问自身,2 向右访问
TriTree p= T, pr;
if(p==NULL) return ;
while(true){
while(p!=NULL&&p->mark!=1){
if(p->rchild!=NULL) //若有右孩子
p->mark=2;
else p->mark=1;
pr=p;
p=p->lchild;
}
p=pr; // p为最左下节点,可能有右孩子
pr=pr->parent;
if(p->mark==2) { // p有右孩子
p->mark=1; // p访问完右边后下次要访问自己
p=p->rchild;
}
else{
visit(p->data); // 访问
p->mark=1;
}
if(p->data==T->data) break;
}
}

层次遍历

层次遍历是按二叉树的层次从小到大且每层从左到右的顺序依次访问结点

对于顺序存储结构的二叉树,其结点在数组中的顺序下标序列与层次遍历的访问顺序一致,因此可直接根据数组得到层次遍历的结果。

对于链式存储结构的二叉树,难以获得结点的同一层的下一层结点,从层末结点也难以得到下一层的层首结点。所以可使用一个辅助队列存储当前层被访问过的结点。

使用队列的非递归遍历

算法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**********
二叉链表类型定义:
typedef struct BiTNode {
TElemType data;
struct BiTNode *lchild,*rchild;
} BiTNode, *BiTree;
**********/
void LevelOrderTraverse(BiTree T,Status(*visit)(TEleType e)){
if(T!=NULL){
Queue Q; InitQueue(Q);
BiTree p = T; //初始化
visit(p->data); //访问根节点
EnQueue(Q,p); //并将根节点入队
}
while(OK==DeQueue(Q,p))//当队非空时重复执行出队操作
{
if(p->lchild!=NULL)//访问左孩子并入队
{
visit(p->lchild->data);
EnQueue(Q,p->lchild);
}
if(p->rchild!=NULL)//访问右孩子并入队
{
visit(p->rchild->data);
EnQueue(Q,p->rchild);
}
}
}

遍历的应用

求二叉树深度

利用后序遍历框架递归求左、右子树深度,然后取两者的较大值加 1(根节点)作为深度值返回。代码如下:

1
2
3
4
5
6
7
8
9
int BiTreeDepth(BiTree T){
int depthLeft,depthRight;
if(NULL==T) return 0; //二叉树深度为0
else{
depthLeft = BiTreeDepth(T->lchild);
depthRight = BiTreeDepth(T->rchild);
return 1+((depthLeft>depthRight)?depthLeft:depthRight);
}
}

叶子节点计数

利用先序遍历框架递归实现,算法如下:

1
2
3
4
5
6
7
8
9
void CountLeaf(BiTree T, int &count){
if(T!=NULL){
if(NULL == T->lchild && NULL == T->rchild){
count++; // 对叶子结点计数
}
CountLeaf(T->lchild, count); // 对左子树递归计数
CountLeaf(T->rchild, count); // 对右子树递归计数
}
}

销毁二叉树

代码如下:

1
2
3
4
5
6
7
void DestroyBiTree(BiTree &T){
if(T!=NULL){
DestroyBiTree(T->lchild);// 递归销毁左子树
DestroyBiTree(T->rchild);// 递归销毁右子树
free(T); // 释放根节点
}
}

删除指定元素结点的子树

代码如下:

1
2
3
4
5
6
7
8
9
void ReleaseX(BiTree &T, char x)
/* 对于二叉树T中每一个元素值为x的结点, */
/* 删去以它为根的子树,并释放相应的空间 */
{
if(!T) return ;
ReleaseX(T->lchild,x);
ReleaseX(T->rchild,x);
if(T->data == x) free(T);
}

复制二叉树

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void CopyBiTree(BiTree T, BiTree &TT)
/* 递归复制二叉树T得到TT */
{
TT = (BiTree)malloc(sizeof(BiTNode));
if(T){
TT->data = T->data;
TT->lchild = T->lchild;
TT->rchild = T->rchild;
if(T->lchild) CopyBiTree(T->lchild,TT->lchild);
if(T->rchild)CopyBiTree(T->rchild,TT->rchild);
}
else free(TT);
}
-------------Page's overThanks for reading-------------

本文标题:二叉树学习笔记(一)之遍历二叉树

本文作者:SherlockYang

发布时间:2018年12月06日 - 23:12

最后更新:2019年01月03日 - 10:01

原始链接:https://sherlock-y.com/2018/12/06/二叉树学习笔记(一)之遍历二叉树/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

如果文章内容对你有帮助,就赏博主一瓶维他奶吧~