一步一步学习数据结构(4)-树和二叉树基础知识

来源:互联网 发布:linux自动登录ssh脚本 编辑:程序博客网 时间:2024/05/19 02:06

(1)树中的基本术语

结点:结点不仅包含数据元素,而且包含指向子树的分支。

结点的度:结点拥有的子树的个数或者分支的个数。

树的度:树中各结点度的最大值。

叶子结点:又叫做终端结点,指度为0的结点。

非终端结点:又叫做分支节点,指度不为0的结点。

孩子:结点的子树的跟。

双亲:与孩子的定义对应。

兄弟:同一个双亲的孩子之间互为兄弟。

祖先:从根到某结点的路径上的所有结点,都是这个结点的祖先。

子孙:以某结点为根的子树中的所有结点,都是这个结点的祖先。

层次:从根开始,根的孩子为第二层,根的孩子的孩子为第三层,依次类推。

树的高度(或者深度):树中结点的最大层次。

结点的深度和高度:结点深度是从根结点算起的,根结点的深度为1;结点高度是从最底层的叶子结点算起的,最底层叶子结点的高度为1

 

2)双亲存储结构

双亲存储结构是一种树的顺序存储结构,用一维数组即可实现。Eg:如知道结点i,则Tree[i]即为i的双亲结点。

 

3)二叉树的重要性质

性质1:非空二叉树上的叶子节点数等于双分支节点数加1.

Eg:二叉树中总的节点数为n,则树中空指针的个数是什么?

可以将所有的空指针看作是叶子节点,则图中所有的所有节点都成了双分支节点。因此,可得空指针域的个数为树中所有结点的个数加1,即n+1个。

 

扩展:在一棵度为m的树上,度为1的结点数为n1,度为2的结点数为n2...度为m的结点数为nm,则叶子结点数n0=1+n2+...+(m-1)nm。推导过程如下:

总结点数:n0+n1+...+nm

总分支数:1*n1+2*n2+....+m*nm

性质2:二叉树的第i层上最多有个结点。(i>=1)

性质3:高度(或深度)为k的二叉树最多有个节点(k>=1),即满二叉树中前k层的节点个数为-1.

性质4:有n个结点的完全二叉树,对各结点从上到下,从左到右依次编号(编号范围为1n),则结点之间有如下关系:

i为某结点a的编号,则

如果i=1,则a双亲的编号为[i/2];

如果2i<=n,则a左孩子的编号为2i;若2i>n,a无左孩子;

如果2i+1<=n,则其a右孩子的编号为2i+1;若2i+1>n,a无右孩子。

性质5:catalan函数:给定n个结点,能构成h(n)中不同的二叉树,.

性质6:具有n个结点的完全二叉树的高度(或深度)为[log2(n+1]

 

4)二叉树的顺序存储结构:

二叉树的顺序存储结构是用一个数组来存储一棵二叉树,这种存储方式最适合于完全二叉树,用于存储一般的二叉树则会浪费大量的存储空间。

将完全二叉树的结点值按编号依次存入一个一维数组当中即完成了一棵二叉树的顺序存储。

E.g:若知道了顶点A的下标为1,要得到A的左孩子结点只需要访问BTree[1*2]即可。类似地,如果知道了一个节点i,若2i不大于n,则i的左孩子结点就存在BTree[2*i]内。

 

5)链式存储结构

二叉树的每个结点用一个链结点来存放。结点结构如下:

lchild

data

rchild

其中,data表示结点的数据域,用于存储对应的数据元素,lchildrchild分别表示左指针域和右指针域,分别用于存储左孩子结点和右孩子结点的位置。这种存储结构称为二叉树链式存储结构。

Typedef struct BTNode{

char data;//这里默认结点data域为char型,如果题目中需要其他类型则只需要修改此处

structBTNode  *lchild;

structBTNode  *rchild;

}BTNode

 

6)二叉树的遍历算法(先序遍历,中序遍历,后序遍历和层次遍历)

1)二叉树的先序遍历

先序遍历的操作过程如下,

如果二叉树是空树,什么都不做;否则:

1)访问根结点;

2)先序遍历左子树;

3)先序遍历右子树。

对应算法的描述如下:

void preorder(BTNode *p){

If(p!=null){

visit(p);//假设访问函数visit(),已经定义过,其中包含了对结点p的各种访问操作,比如可以打印出p对应的数值

Preorder(p->lchild);//先序遍历左子树

Preorder(p->rchild);//先序遍历右子树

2)二叉树的中序遍历

中序遍历的操作过程如下:

如果二叉树为空树,则什么都不做;否则:

1)中序遍历左子树;

2)访问根结点;

3)中序遍历右子树。

void inorder(BTNode *p){

If(p!=null){

Inorder(p->lchild);

Visit(p);

inorder(p->rchild);

}

3)二叉树的后序遍历

后序遍历的操作过程如下:

如果二叉树为空树,则什么都不做;否则:

1)后序遍历左子树;

2)后序遍历右子树;

3)访问根结点。

void postorder(BTNode *p){

If(p!=null){

postorder(p->lchild);

postorder(p->rchild);

Visit(p);

}

4)二叉树的层次遍历

要进行层次遍历,需要建立一个循环队列。先将二叉树头结点入队列,然后出队列,访问该结点,如果它有左子树,则将左子树根结点入队;如果它有右子树,则将右子树根结点入队。然后出队列,对出队结点访问,如此反复,直到队列为空为止。

对应算法如下:

Void level(BTNode *p)

{

int front,rear;

BTNode *que[maxsize];//定义一个循环队列,用来记录将要访问的层次上的结点

front=rear=0;

BTNode *q;

If(p!=NULL){

rear=(rear+1)%maxsize;

que[rear[=p;

while(front!=rear){

Front=(front+1)%maxsize;

q=que[front];

visit(q);

if(q->lchild!=NULL){

rear=(rear+1)%maxsize;

que[rear]=q->lchild;

}

if(q->rchild!=NULL){

rear=(rear+1)%maxsize;

que[rear]=q->rchild;

}

}

}

}

 

(7)线索二叉树的基本概念和构造

1)线索二叉树的基本概念

前面我们知道,对于二叉链表的存储结构,n个结点的二叉树有n+1个空链域,能不能把这些空链域有效地利用起来,以使二叉树的链表更加高效呢?答案是肯定的,这就是线索二叉树的由来。在一般的二叉树中,我们只知道某个结点的左,右孩子,并不知道某个结点在某种遍历方式下的直接前驱和直接后继,如果能够知道“前驱”和“后继”信息,就可以把二叉树看作一个链表结构,从而可以像遍历链表那样来遍历二叉树,进而提高效率。

2)线索二叉树的构造

线索二叉树的结点结构如下:

lchild

Itag

data

rtag

rchild

其中,ltagrtag为标志域,他们的意义是:

      1:如果Itag=0,则表示lchild为指针,指向结点的左孩子;如果ltag=1,则表示lchild为线索,指向结点的直接前驱。

  2:如果rtag=0,则表示rchild为指针,指向结点的右孩子;如果rtag=1,则表示rchild为线索,指向结点的直接后继。

对应的线索二叉树的结点定义如下:

Typedef  struct  TBTNode

{

char data

int  ltag,rtag;//线索标记

Struct  TBTNode  *lchild

Struct  TBTNode  *rchild

}TBTNode

线索二叉树可以分为前序线索二叉树,中序线索二叉树和后序线索二叉树。构建二叉树的过程是一个遍历并修改指针的过程。对一个二叉树中的所有结点的空指针域按照某种遍历方式加线索的过程叫做线索化,被线索化了的二叉树称为线索二叉树。

若设指针p指向正在访问的结点,则遍历时设立一个指针pre,使其始终指向刚刚访问过的结点,这样就记下了遍历过程中结点被访问的先后关系。

通过中序遍历对二叉树线索化的递归算法如下:

void  InThreadTBTNode *p,TBTNode *pre

{

If(p!=null){

Inthread(p->lchild,pre);//递归,左子树线索化

If(p->lchild==null)

{ //建立当前结点的前驱线索

P->lchild=pre;

P->ltag=1;

}

If(pre!=null&&pre->rchild==NULL){//建立前驱结点的后继线索

pre->rchild=p;

pre->rtag=1;

}

pre=p; //前驱跟上,当前指针向前遍历

Inthreadp->rchild,pre); //递归,左子树线索化

}

通过中序遍历建立中序线索二叉树的主过程算法如下:

void   createInThread(TBTNode *root){

TBTNode  *pre=NULL; //前驱结点指针

If(root!=NULL){

InThread(root,pre);

pre->rchild=NULL; //非空二叉树,线索化

Pre->rtag=1; //后处理中序最后一个结点

}

}

3)访问线索二叉树

访问运算主要是为遍历中序线索二叉树服务的。这种遍历不在需要栈,因为它利用了隐含在线索二叉树中的前驱和后继信息。

求以p为根的中序线索二叉树中中序序列下的第一个结点的算法如下:

TBTNode *FirstTBTNode *p

{

Whilep->ltag==0)

p=p->lchild;   //最左下结点(不一定是叶结点)

Return p;

}

求在中序线索二叉树中结点p在中序下的后继结点的算法如下:

TBTNode *NextTBTNode *p

{

If(p->rtag==0)

return first(p->rchild);

Else

return p->rchild;    //rtag==1,直接返回后继线索

如果把程序中FirstItaglchild换成rtagrchild同时把程序中的First运算换成Last运算,则可得到求中序序列下最后一个结点的运算Last;如果把程序中Nextrtagrchild换成ltaglchild,则得到中序序列下前驱结点的运算Prior

在中序线索二叉树上执行中序遍历的算法:

Void Inorder(TBTNode *root){

For(TBTNode *p=First(root);p=NULLp=Next(p));

Visit(p);//visit()是已经定义的访问p所指结点的函数

(8)树和森林

孩子兄弟存储结构

把树或者森林转化为二叉树所对应的存储结构就恰好是这种孩子兄弟存储结构,所以孩子兄弟存储结构可以方便地实现树或者森林与二叉树的转换。

将树转换为二叉树的过程如下:

1)将同一个结点的各孩子用线串起来;

2)将每个结点的分支从左往右除了第一个以外,其余的都剪掉;

3)将各结点调节成为常见的位置。

 

森林与二叉树的转换

 

(9)树和森林的遍历

1)树的遍历

树的遍历有两种方式:先根遍历和后根遍历。先根遍历是先访问根结点,再依次访问根结点的每棵子树,访问子树时依然遵循先根子树的规则;后根遍历是先依次访问根结点的没棵子树,再访问根结点,访问子树时依然遵循先子树再根的规则。

2)森林的遍历

森林的遍历方式有两种:先序遍历和中序遍历。

先序遍历的过程:先访问森林中第一棵树的根结点,先序遍历第一棵树中根结点的子树,先序遍历森林中除去第一棵树的其他树。

中序遍历的过程:中序遍历第一棵树中根结点的子树,访问第一棵树的根结点,中序遍历森林中除去第一棵树的其他树。

 

树与二叉树的应用

 

二叉排序树与平衡二叉树

赫夫曼树和赫夫曼编码

1.赫夫曼树又叫做最优二叉树,它的特点是带权路径最短

路径。路径是指从树中一个结点到另一个结点的分支所构成的路线;

路径长度。路径长度是指路径上的分支数目;

树的路劲长度。树的路径长度是指从根到每个结点的路径长度之和;

带权路径长度。结点具有权值,从该结点到根之间的路径长度乘以结点的权值,就是该结点的带权路径长度。

树的带权路径长度。树的带权路径长度(WPL)是指树中所有叶子结点的带权路径长度之和。     

2.赫夫曼树的构造方法

对于同一组结点,构造出的赫夫曼树可能不是唯一的。但是他们的WPL是相同的。

3.赫夫曼树的特点

1)权值越大的结点,距离跟结点越近。

2)树中没有度为1的结点。这类树又叫做正则(严格)二叉树。

4.赫夫曼编码

前缀编码:任一字符的编码,都不是另一个字符编码的前缀。

赫夫曼编码就是长度最短的前缀编码。       


0 0