树
树(Tree)是n个结点的有限集,在任意一颗非空树种有一下特点
1)有且仅有一个根(Root)结点
2)当n > 1时,其余结点可分为m(m > 0)个互不相交的有限集,其中每一个集合本身又是一个树,称为根的子树(SubTree)
树的定义是一个递归的定义,即在树的定义种又用到树的概念,可以反应出树的固有特性
一般,分等级的分类方案都可以用层次结构来表示,即可以使用树结构
树结构中的基本术语
树的结点包含一个数据元素及若干指向其子树的分支
结点拥有的子树数称为结点的度(Degree)
度为0的结点称为叶子(Leaf)或终端结点
度不为0的结点称为非终端结点或分支节点
除根结点外,分支节点也称为内部节点
树的度是各结点度的最大值
结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent),同一个双亲的孩子之间互称兄弟(Sibling)
结点的祖先是从根到该节点所经分支上的所有结点,反之以某结点为根的子树中的任一结点都称为该结点的子孙
结点的层次(Level)从根开始定义,根为第一层,根的孩子为第二层依次类推
双亲在同一层的结点互为堂兄弟
树中结点的最大层次称为树的深度(Depth)或高度
若树中结点的各子树看成从左至右是有次序的(即不能互换)则称该树为有序树,否则称为无序树
在有序树中最左边子树的根称为第一个孩子,最右边的称为最后一个孩子
森林(Forest)是m棵互不相交的树的集合,对树的每个结点而言,其子树的集合即为森林--可以森林和树相互递归的定义来描述树
1------二叉树(BinaryTree)
1--定义
二叉树的特点是每个结点至多只有两颗子树(即二叉树中不存在度大于2的结点),且二叉树的子树有左右之分,其次序不能任意颠倒
2--性质
1)在二叉树的第i层上至多有2^(i - 1)次方个结点(i >= 1)
2)深度为k的二叉树至多有 (2^k - 1) 个结点(k >= 1)
3)对任意一颗二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 + 1
一颗深度为k且有2的k次方减一个结点的二叉树称为满二叉树--特点是每一层上的结点数都是最大结点数
对满二叉树的结点进行连续编号,约定编号从根结点起,自上而下,自左至右,从而定义完全二叉树
深度为k的,有n个结点的二叉树,当且仅当每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时,称为完全二叉树
完全二叉树的特点
(1)叶子结点只可能在层次最大的两层上出现
(2)对任一结点,若其分支下的子孙的最大层次为l,则其左分支下的子孙的最大层次必为 l 或 l + 1
4)具有n个结点的完全二叉树的深度为log2n + 1向下取整
5)如果对一颗有n个结点的完全二叉树(其深度为log2n + 1向下取整)的结点按层序编号(从第一层到第log2n + 1向下取整,每层从左到右)
则对任一结点i(1 <= i <= n)有
(1)如果i == 1,则结点i是二叉树的根,无双亲;如果i > 1,则其双亲PARENT(i)是结点i / 2向下取整
(2)如果2i > n则结点i无左孩子(结点i为叶子结点);否则其左孩子LCHILD(i)是结点2i
(3)如果2i + 1 > n则结点i无右孩子;否则其右孩子RCHILD(i)是2i + 1
二叉树的存储结构
1--顺序结构
#define MAX_TREE_SIZE 100 //二叉树的最大结点数
typedef TElemType SqBiTree[MAX_TREE_SIZE]; //0号单元存储根结点
用一维数组来存储,按照完全二叉树的编号方式依次存储每个结点的编号,如果结点不存在则在相应位置填0,可以看到,这种存储方式大多只
适用于完全二叉树,如果一个深度为k且只有k个结点的单支树却需要长度为2的k次方减一的一维数组
2--链式存储结构
1)二叉链表存储
typedef struct BiTNode {
TElemType data;
struct BiTNode *lchild, *rchild; //左右孩子指针
}BiTNode *BiTree;
NOTE 在含有n个结点的二叉链表中有n+1个空链域
遍历二叉树
问题:如何按某条搜索路径寻访树中每个结点,使得每个结点均被访问一次,而且仅被访问一次?
由于二叉树是一种非线性结构,每个结点都可能有两颗子树,需要寻找一种规律使得二叉树上的结点能排列在一个线性队列上从而便于遍历
基于二叉树的递归定义,提出3中遍历二叉树的递归算法
先序遍历{
若二叉树为空则空操作,否则
1)访问根结点
2)先序遍历左子树
3)先序遍历右子树
}
中序遍历{
若二叉树为空则空操作,否则
1)中序遍历左子树
2)访问根结点
3)中序遍历右子树
}
后序遍历{
若二叉树为空则空操作,否则
1)后序遍历左子树
2)后序遍历右子树
3)访问根结点
}
//代码实现
void PreOrderTraverse(BiTree T) //前序遍历
{
if (T == NULL) //树为空,空操作
return;
printf("%d", T->data); //显示结点数据,可换成其他操作
PreOrderTraverse(T->lchild); //先序遍历左子树
PreOrderTraverse(T->rchild); //最后先序遍历右子树
}
void InOrderTraverse(BiTree T) //中序遍历
{
if (T == NULL)
return;
InOrderTraverse(T->lchild); //中序遍历左子树
printf("%d", T->data);
InOrderTraverse(T->rchild); //中序遍历右子树
}
void PostOrderTraverse(BiTree T) //后序遍历
{
if (T == NULL)
return;
PostOrderTraverse(T->lchild); //先后序遍历左子树
PostOrderTraverse(T->rchild); //后序遍历右子树
printf("%d", T->data);
}
NOTE 已知前序和中序遍历可以唯一确定一颗二叉树;已知后序和中序遍历可以唯一确定一颗二叉树;但是已知前序和后序遍历不能确定一颗二叉树
遍历是二叉树各种操作的基础,可以在遍历过程中对结点进行各种操作,如:对于一颗已知树可求结点的双亲,求结点的孩子结点,
判定结点所在层次等,反之也可在遍历过程中生成结点,建立二叉树的存储结构
Status CreatBiTree(BiTree T)
{
//按先序次序输入二叉树中结点的值,构造二叉链表表示二叉树T
scanf("%c", &ch);
if (ch == ' ')
T = NULL;
else
{
T = (BiTNode *)malloc(sizeof(BiTNode));
if (!T)
exit(0);
T->data = ch; //生成根结点
CreatBiTree(T->lchild); //构造左子树
CreatBiTree(T->rchild); //构造右子树
}
return OK;
}
线索二叉树
遍历二叉树是以一定规则将二叉树中结点排列成一个线性序列,得到二叉树中结点的先序序列或中序或后序序列,实质就是把非线性结构线性化
使每个结点(除第一和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继
二叉链表作为存储结构时,只能找到左右孩子,不能直接得到结点在任一序列中的前驱和后继信息,这种信息只有在遍历的动态过程中才能得到
问题:如何保存这种信息?
一个方法是在每个结点上增加两个指针域fwd和bkwd,分别指示结点在任意一次遍历是得到的前驱和后继信息
显然,这样使得结构的存储密度大大降低,另外,对n个结点的二叉链表中必定存在n+1个空链域,可以使用这些空链域来存放结点的前驱和后继关系
规定,若结点有左子树,lchild指示左孩子否则指示前驱,rchild类似,为了判断指针到底指向前驱还是子树,为每个指针增加一个标志变量
如LTag = 0表示lchild指示左孩子,LTag=1指示前驱
以这种结构构成的二叉链表称为线索链表,其中指向结点前驱和后继的指针叫线索,这种树称为线索二叉树
对二叉树以某种次序遍历使其变为线索二叉树的过程称为线索化
在线索树上遍历,只要先找到序列中的第一个结点然后依次找结点后继直至其后继为空时为止
问题:如何在线索树中找结点的后继?
以中序线索树来看,树中所有叶子结点的右链是线索,则右链域直接指示了结点的后继
树中所有非终端结点的右链均为指针,则无法由此得到后继的信息。
然而根据中序遍历的规律可知,结点的后继应是遍历其右子树时访问的第一个结点,即右子树中最左下的结点
找非终端结点的后继首先沿有指针找到其右子树的根结点,然后顺其左指针往下直至左表示为1的结点即为其后继
在中序线索树中找前驱的规律是:若左标志为1则左链为线索,指示前驱,否则遍历左子树时最后访问的一个结点(左子树中最右下的结点)为其前驱
后序线索树中找结点后继则较为复杂(后续)
NOTE 在某程序中所用二叉树需经常遍历或查找结点在遍历所得线性序列中的前驱和后继,则应采用线索链表作存储结构
//二叉树的二叉线索存储表示
typedef enum PointerTag{Link,Thread}; //Link = 0:指针;Thread = 1:线索
typedef struct BiThrNode {
TElemType data;
struct BiThrNode *lchild, *rchild; //左右孩子指针
PointerTag LTag, RTag; //左右标志
}BiThrNode,*BiThrTree;
为了方便,仿照线性表的存储结构,在二叉树的线索链表上也添加一个头结点,并令其lchild域指针指向二叉树的根结点,其rchild指向中序遍历时
访问的最后一个结点;反之令二叉树中序序列中的第一个结点的lchild和最后一个结点rchild指针均指向头结点
这样既可以从第一个结点起顺序后继进行遍历,也可以从最后一个结点顺前驱遍历
//以双向链表为存储结构的算法描述
Status InOrderTraverse_Thr(BiThrTree T, Status(*Visit)(TElemType e)) //遍历
{
//T指向头结点,头结点的左链lchild指向根结点
//中序遍历二叉线索树T的非递归算法,对每个数据元素调用Visit
BiThrTree p;
p = T->lchild; //p指向根结点
while (p != T) //空树或遍历结束时p==T
{
while (p->LTag == Link) //当LTag == 0 时循环到中序序列的第一个结点
p = p->lchild;
Visit(p->data);
while (p->RTag == Thread&&p->rchild != T) //访问后继结点
{
p = p->rchild;
Visit(p->data);
}
p = p->rchild; //p进至右子树根
}
return OK;
}
如何进行线索化?
线索化的实质是将二叉链表中的空指针改为指向前驱或后继的线索,而前驱或后继的信息只要在遍历时才能得到,因此线索化的过程即为在
遍历过程中修改空指针的过程
为了记下遍历过程中访问结点的先后关系,附设一个指针pre始终指向刚刚访问过的结点,若指针p指向当前访问的结点,则pre指向它的前驱
//算法描述
Status InOrderThreading(BiThrTree &Thrt, BithrTree T)
{
//中序遍历二叉树T,并将其中序线索化,Thrt指向头结点
if (!(Thrt = (BiThrTree)malloc(sizeof(BiThrNode))))
exit(0);
Thrt->LTag = Link; //建立头结点
Thrt->RTag = Thread;
Thrt->rchild = Thrt; //右指针回旋
if (!T) //空树,左指针回旋
Thrt->lchild = Thrt;
else
{
Thrt->lchild = T;
pre = Thrt;
InThreading(T); //中序遍历线索化
pre->rchild = Thrt; //最后一个结点线索化
pre->RTag = Thread;
Thrt->rchild = pre;
}
return OK;
}
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); //右子树线索化
}
}
NOTE 在完成前驱和后继的判断后,别忘记将当前结点p赋值给pre,以便于下一次使用
树和森林
1--树的存储结构
1-双亲表示法
以一组连续空间存储树的结点,同时在每个结点中附设一个指示器指示其双亲结点在链表中的位置
#define MAX_TREE_SIZE 100
typedef struct PTNode { //结点结构
TElemType data;
int parent; //双亲位置域
}PTNode;
typedef struct { //树结构
PTNode nodes[MAX_TREE_SIZE];
int r, n; //根的位置和结点数
}PTree;
这种存储结构利用了每个结点除根外只有唯一双亲的性质,求根只要找到无双亲的结点即可,但是求结点的孩子需要变量整个结构
2-孩子表示法
typedef struct CTNode { //孩子结点
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct {
TElemType data;
ChildPtr firstchild; //孩子链表的头指针
}CTBox;
typedef struct {
CTBox nodes[MAX_TREE_SIZE];
int n, r; //结点数和根的位置
}CTree;
3-孩子兄弟表示法
即二叉链表表示法,链表中结点的两个链域分别指向该结点的第一个孩子结点和下一个兄弟结点
typedef struct CSNode {
ElemType data;
struct CSNode *firstchild, *nextsibling;
}CSNode,*CSTree;
NOTE 任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。孩子兄弟表示法正是利用了这个特性
这样查找某个结点的某个孩子很方便,通过firstchild找nestsibling的i-1次即可,如果想找双亲可以增设parent指针来实现
这个表示法可以把一颗复杂的树二叉树化
森林与二叉树的转换
1-树转换为二叉树
1)加线,在所有兄弟结点之间加一条连线
2)去线,对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间连线
3)层次调整,以树的根结点为轴心,将整颗树顺时针旋转一定的角度,使之结构层次分明。
注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子
2-森林转换成二叉树
1)把每个树转换为二叉树
2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。
当所有的二叉树连接起来后就得到了由森林转换来的二叉树
3-二叉树转换为树
1)加线,若某结点的左孩子结点存在,则将这个左孩子的右孩子结点,右孩子的右孩子结点。。。。。。左孩子的n个右孩子结点都
作为该结点的孩子,将该节点与这些右孩子结点用线连接起来
2)去线,删除原二叉树中所有结点与其右孩子结点的连线
3)层次调整,使结构层次分明
4-二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林,只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树
1)从根结点开始,若右孩子存在,则把右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除...,直到
所有右孩子连线都删除为止,得到分离的二叉树
2)再将每颗分离后的二叉树转换为树即可
树与森林的遍历
树的遍历分为两种方式
1)先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树
2)后根遍历,即先依次后根遍历每棵子树,然后再访问根结点
森林的遍历也分为两种方式
1)前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余树构成的森林
2)后序遍历:先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次用同样方式遍历除去第一棵树的剩余树构成的森林
NOTE 森林前序遍历与二叉树前序遍历结果相同,森林后序遍历和二叉树中序遍历结果相同,使用二叉链表结构时,树的先根遍历和后根遍历
可以借用二叉树的前序遍历和中序遍历
赫夫曼树及其应用
最基本的压缩编码方法--赫夫曼编码
1--赫夫曼树定义与原理
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称为路径长度
树的路径长度就是树根到每一个结点的路径长度之和
如果考虑带权的结点,结点的带权路径长度为从该结点到树根之间的路径长度与结点上权的乘积
树的带全路径长度为树中所有叶子结点的带权路径长度之和
假设有n个权值{w1,w2...,wn},构造一棵有n个叶子结点的二叉树,每个叶子结点带全wk,每个叶子的路径长度为lk,我们通常记作,
其中带权路径长度WPL最小的二叉树称赫夫曼树
构造赫夫曼树的赫夫曼算法描述
1)根据给定的n个权值{ w1,w2...,wn },构成n棵二叉树的集合F={T1,T2...Tn},其中每棵二叉树Ti中只有一个带权为wi根结点,其左右子树均为空
2)在F中选取两棵根节点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树上根结点的权值之和
3)在F中删除这两棵树,同时将新得到的二叉树加入F中
4)重复2)和3),直到F只含一棵树为止,该树即为赫夫曼树
2--赫夫曼编码
一般地,设需要编码的字符集为{d1,d2...,dn},各个字符在电文中出现的次数或频率集合为{w1,w2...wn},以d1,d2...,dn作为叶子结点,以w1,w2...,wn
作为相应叶子结点的权值来构造一棵赫夫曼树。规定赫夫曼树的左分支代表0,右分支代表1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列
便为该结点对应字符的编码,即赫夫曼编码
算法来源--大话数据结构