这一章介绍了树,特别是对二叉树的介绍。
【数据结构系列】【前一章:串】【后一章:图】
目录
前言
五、树
1、存储结构
(1)双亲表示法
(2)孩子表示法
(3)孩子兄弟表示法
2、二叉树的定义
3、二叉树的性质
4、二叉树的存储结构
(1)顺序存储
(2)链式存储
5、二叉树的遍历
6、推导遍历结果
7、二叉树的建立
8、线索二叉树
9、树转换成二叉树
10、森林转换成二叉树
11、二叉树转换成树
12、树的遍历
13、森林的遍历
14、赫夫曼树及其应用
(1)构造赫夫曼树
(2)赫夫曼编码
一对多结构,n个节点的有限集,当n=0时,则称为空树,在所有节点中,有且仅有一个根节点,当n>1时,剩下的节点可以分成m个互不相交的有限集,各自又是一棵树,也称为根的子树。
#define MAX_TREE_SIZE 100
typedef int TElemType;
typedef struct PTNode //节点结构
{
TElemType data; //节点数据
int parent; //指向双亲的指针
}PTNode;
typedef struct //树结构
{
PTnode nodes[MAX_TREE_SIZE]; //节点数组
int r,n; //根的位置和节点数
}PTree;
1)data | child1 | child2、、、
2)data | num | child1 | child2、、、childn
data fistchild rightsib
一个节点下面最多只有两棵子树
五种基本形态:(1)空二叉树 (2)只有一个根节点 (3)根节点只有左子树 (4)根节点只有右子树 (5)根节点既有左子树又有右子树
满二叉树:所有分支节点都有叶子节点,所有叶子节点都在同一层次
完全二叉树:(1)叶子节点只能出现在最下两层 (2)最后的叶子节点一定集中在左部连续位置 (3)倒数二层,若有叶子节点,一定一定都在右部连续位置 (4)如果节点度为1,则该节点只有左孩子 (5)同样节点数的二叉树,完全二叉树的深度最小
(1)第i层至多节点数:
(2)深度为k至多节点数:
(3)n0=n2+1(n0:度为0的节点数,n2:度为2的节点数)
推导:总节点数:n=n0+n1+n2,总枝条数:L=n-1=n0+n1+n2-1,或者:L=n1*1+n2*2,所以n0+n1+n2-1=n1+2*n2,即n0=n2+1
(4) 节点数为n的完全二叉树深度为:
推导:节点数小于等于同样深度的节点数:,但一定大于,所以,因为n和k都为整数,所以k=
(5)具有n个节点的完全二叉树,从上至下,从左至右依次编号1、2、、i、、n,双亲节点:;若2i>n,则无左孩子,否则左孩子是2i,如果2i+1>n,则无右孩子,否则右孩子是:2i+1。
按照从上到下,从左到右依次编号,再按照编号顺序依次把节点信息存入数组中,若有的节点没有,则以^代替。
二叉链表:
lchild | data | rchild |
lchild和rchild都为指针,分别指向左孩子和右孩子,data为该节点数据
(1)前序遍历:根--左--右
图1 二叉树的前序遍历
程序:
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
cout<data;
PreOrderTraverse(T->lchild); //先序遍历左子树
PreOrderTraverse(T->rchild); //再先序遍历右子树
}
(2)中序遍历:左--根--右
图2 二叉树的中序遍历
程序:
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return;
InOrdeTraverse(T->lchild); //中序遍历左子树
cout<data;
InOrderTraverse(T->rchild); //中序遍历右子树
}
(3)后序遍历:左--右--根
图3 二叉树的后序遍历
程序:
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild); //先后序遍历左子树
PostOrderTraverse(T->rchild); //再后序遍历右子树
cout<data;
}
(4)层次遍历:按照编号顺序依次访问
例如:前序遍历:ABCDEF,中序遍历:CBAEDF,问后序遍历?
1、由前序遍历可知根节点:A
2、结合中序遍历可知:左子树节点有:CB,右子树节点有:EDF,如下图所示:
3、由前序遍历左子树的顺序为BC可知:左子树中B为根,C为节点,由中序遍历左子树的顺序为CB可知,C为B的左节点
4、由前序遍历右子树的顺序为DEF可知:右子树中D为根,EF为节点,由中序遍历EDF可知,E为D的左节点,F为D的右节点
5、整个合起来如下图所示:
6、根据树的结构,后序遍历为:CBEFDA
关于二叉树的建立,我们推出一个扩展二叉树,如图所示:
图4 扩展二叉树
关于图4的树,我们在输入时就可以输入AB#D##C##,所以程序如下:
void CreateBiTree(BiTree *T)
{
TelemType ch;
cin>>c;
if(c=='#')
*T=NULL;
else
{
*T=new BiTNode;
if(!*T)
exit(OVERFLOW);
(*T)->data=ch; //生成根节点
CreateBiTree(&(*T)->lchild); //构造左子树
CreateBiTree(&(*T)->rchild); //构造右子树
}
}
按照扩展二叉树的方法构造二叉树,会出现很多节点多着没有得到充分的利用,再就是我们对于一个节点不知道它的前驱和后继是啥,必须整个遍历一次才能知道,这样效率太低,关于二叉树的中序遍历,我们可以清楚地知道一个节点的前驱和后继,所以我们可以利用那些空节点,使右边的空节点指向它的后继节点,左边的空节点指向它的前驱节点,这些指向前驱和后继的指针就称为线索。
图5 线索二叉树
如图5所示,前驱用实线表示,后继用虚线表示,实质上把整个二叉树构成了一个双向链表。
图六 二叉表转化为双向链表
当进行到这后,会有一个问题,那就是lchild指针到底是前驱还是左孩子了,还有rchild到底是后继还是右孩子了?所以在这里要引入两个标志位:ltag和rtag,注意这两个标志位是存放0或1的布尔型变量,比lchild和rchild的指针型变量需要的空间少,节点格式如下所示:
lchild | ltag | data | rtag | rchild |
ltag:为0代表左孩子,为1代表前驱
rtag:为0代表右孩子,为1代表后继
图七 线索二叉树
线索二叉树结构实现:
(1)节点定义
typedef enum {Link,Thread} PointerTag; //Link=0表示指向左右孩子指针,Thread=1表示前驱或后继 的线索
typedef struct BiThrNode
{
TElemType data; //节点数据
struct BiThrNode *lchild,*rchild; //左右孩子指针
PointerTag LTag;
PointerTag Rtag; //左右状态
}BiThrNode,*BiThrTree;
(2)中序遍历线索化的递归函数
BiThrTree pre; //始终指向刚刚访问过的节点
void InThreading(BiThrTree p)
{
if (p)
{
InThreading(p->lchild); //递归左子树线索化
if (!p->lchild) //没有左孩子
{
p->LTag = Thread; //前驱线索
p->lchild = pre; //左孩子指针指向前驱
}
if (!pre->rchild) //前驱没有右孩子
{
pre->Rtag = Thread; //后继线索
pre->rchild = p; //前驱右孩子指向后继(当前节点p)
}
pre = p; //保持pre指向p的前驱
InThreading(p->rchild); //递归右子树线索化
}
}
遍历代码如下:
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p = T->lchild;
while (p != T)
{
while (p->LTag == Link)//访问左孩子
{
p = p->lchild;
}
cout << p->data;
while (p->RTag == Thread&&p->rchild != T) //访问根节点
{
p = p->rchild;
cout << p->data;
}
p = p->rchild; //访问右孩子
}
return Ok;
}
在实际问题中,如果所用的二叉树需经常遍历或查找节点时需要遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
(1)加线。在所有兄弟节点之间加一条线。
(2)去线。对树中每个节点,只保留它与第一个孩子节点的连线,删除它与其他孩子节点之间的连线。
(3)层次调整。以树的根节点为轴心,注意第一个孩子是二叉树节点的左孩子,兄弟转换过来的孩子是节点的右孩子。
(1)把每个树转换成二叉树
(2)第一棵树不动,从第二棵二叉树开始,依次把后一颗二叉树的根节点作为前一棵二叉树的根节点的右孩子,用线连接起来。
(1)加线。若某节点的左孩子存在,则将该左孩子的右节点、右孩子的右孩子节点、右孩子的右孩子的右孩子节点...,反正就是左孩子的n个右孩子节点和最初的某节点用线连接起来。
(2)去线。删除原二叉树所有节点与其右孩子节点的连接线。
(3)层次调整
(1)先根遍历:即先访问树的根节点,然后依次先根遍历根的每棵子树。
(2)后根遍历:即依次后根遍历每棵子树,然后再访问根节点。
(1)前序遍历:先访问森林中第一棵树的根节点,然后再依次先根遍历根的每棵子树,再依次用同样的方式遍历除去第一棵树的剩余树构成的森林。
(2)后序遍历:先访问森林中的第一棵树,后根遍历的方式遍历每棵子树,然后再访问根节点,再依次同样的方式遍历除去第一棵树的剩余树构成的森林。
森林的前序遍历和我、二叉树的前序遍历结果相同,森林的后序遍历和二叉树的中序遍历结果相同
文件压缩即是在赫夫曼的研究上发展而来。
赫夫曼问题也可以称为最优路径问题。
1)把所有权值的叶子节点从小到大排序 2)把排在最前面的两个节点连起来组合成一个新的节点,这个新节点的权值就是这两个节点权值的和。3)让这个新的节点代替最初的两个节点,重新进行排序 4)这样依次找出最小两个节点组合起来成新节点,当组合完后,只剩一个节点时,即完成构造。
将左分支权值改为0,右分支权值改为1。