树的一些总结
1.树的度
结点拥有的子树数称为结点的度。度为0的结点称为叶结点(leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。
2.分支结点
分支结点也称为内部结点。
3.树的层次
结点的层次从根开始定义起,根为第一层,根的孩子为第二层。
4.树的存储结构
利用顺序存储和链式存储的特点,完全可以实现对数的存储结构的表示。我们介绍三种不同的表示法:
双亲表示法、孩子表示法、孩子兄弟表示法
1)双亲表示法
/** 树的双亲表示法结点结构定义 **/
#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.
存储结构的设计是一个非常灵活的过程。一个存储结构设计的是否合理,取决于基本该存储结构的运算是否适合、是否方便,时间复杂度好不好等等。
2)孩子表示法
方案一:
一种是指针域的个数等于树的度,树的度是树哥哥结点度的最大值。
评判:这种方法对于树中各结点的度相差很大时,显然是很浪费时间,因为有很多个结点,它的指针域是空的。不过如果树的各结点度相差很小时,
那就意味着开辟的空间被充分利用了,这时存储结构的缺点反而变成了优点。
方案二:
每个结点指针域的个数等于该结点的度,我们专门取一个位置来存储结点指针域的个数。
评判:这种方法客服了浪费空间的缺点,对空间利用率是很高了,但是由于各个结点的链表是不相同的结构,加上要维护结点度的数值,在运算上
会带来时间上的损耗。
再优化:
定义孩子表示法的结构定义代码
/** 树的结点表示法结构定义 **/
#define MAX_TREE_SIZE 100
typedef struct CTNode /** 孩子结点 **/
{
int child;
struct CTNode *next;
}*ChildPtr;
typedef struct /** 表头结构 **/
{
TElemType data;
ChildPtr firstchild;
}CTBox;
typedef struct /** 树结构 **/
{
CTBox nodes[MAX_TREE_SIZE];/** 结点数组 **/
int r,n;/** 根的位置和结点数 **/
}CTree;
评判:这样的结构对于我们要查找某个结点的某个孩子,或者找某个结点的兄弟,只需要查找这个结点的孩子单链表即可。对于遍历整棵树也很方便,但是
存在一个问题,如何知道结点的双亲是谁?在表头结构中加一项parent的位置。
3)孩子兄弟表示法(未完待续)
任意一棵树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。
结构定义代码:
/** 树的孩子兄弟表示法结构定义 **/
typedef struct CSNode
{
TElemType data;
struct CSNode *firstchild,*rightsib;
}CSNode,*CSTree;
查找孩子很方便,查找双亲不是很方便,所以在有可能的条件下,可以添加父母结点项。
5.二叉树基本性质
性质1:在二叉树的第i层上至多有2的(i-1)次方个结点(i>=1)
性质2:深度为k的二叉树至多有(2的k次)-1 个结点(意思就是这棵树上有多少个结点,哈哈)
性质3:对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 + 1;(不理解)
终端结点数其实就是叶子结点数,而一棵二叉树,除了叶子结点外,剩下的就是度为1或2的结点数了,我们设n1为度是1的结点数。则数T结点总数为n = n0 + n1 + n2
性质4: 具有n个结点的完全二叉树的深度为log2(N) + 1
性质5:如果对一棵有n个结点的完全二叉树的结点按层序编号(从第一层到第log2 N + 1层,每层从左到右),对任一结点(1<=i<=n)
1)如果i = 1,则结点i是二叉树的根,无双亲;如果,i>1,则双亲是结点的[i/2];
2)如果2i > n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i.
3)如果 2i + 1 > n,则结点i无右孩子;否则其右孩子是结点2i + 1;
6.二叉树顺序存储结构
使用于完全二叉树。
7.二叉链表
二叉树每个结点最多有两个孩子,所以它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。
二叉链表的结点结构定义代码:
/** 二叉树的二叉链表的结点结构定义 **/
typedef struct BiTNode /*结点结构*/
{
TElemType data;/** 结点结构 **/
struct BitNode *lchild, *rchild;/** 左右孩子指针 **/
}BiTNode,*BitTree;
注意:如果有需要就定义一个指向父的指针。
8.遍历二叉树
1)前序遍历
规则:若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
根-》左-》右
代码实现:
/* 二叉树的前序遍历递归算法 */
void PreOrderTraverse(BiTree T)
{
if(T == NULL)
return ;
printf("%c",T->data);/* 显示结点数据,可以更改为其他队结点操作 */
PreOrderTraverse(T->lchild);/* 再先序遍历左子树 */
PreOrderTraverse(T->rchild);/* 最后先序遍历右子树 */
}
2)中序遍历
规则:若树为空,则空操作返回,否则从根结点返回(注意,并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
左-》根-》右
代码实现: /* 二叉树的中序遍历递归算法 */
void InOrderTraverse(BiTree T)
{
if(T == NULL)
return ;
InOrderTraverse(T->lchild);/*中序遍历左子树*/
printf("%c",T->data);/*显示结点数据,可以更改为其他对结点操作*/
InOrderTraverse(T->rchild); /*最后中序遍历右子树*/
}
3)后序遍历
规则:若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。
左-》右-》根
代码实现:
/* 二叉树的后序遍历递归算法 */
void PostOrderTraverse(BiTree T)
{
if(T == NULL)
return ;
PostOrderTraverse(T->lchild);/* 后序先遍历左子树 */
PostOrderTraverse(T->rchild);/* 后序再遍历右子树 */
printf("%c",T->data);/** 后序,显示结点数据,可以更改为其他对结点的操作 **/
}
4)层序遍历
规则:若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
9.二叉树的建立
创建前序的二叉树
代码实现: /** 按前序输入二叉树中的结点的值(一个字符)**/
/** #表示空树,构造二叉链表表示二叉树T **/
void CreateBitTree(BiTree *T)
{
TElemType ch;
scanf("%c",&ch);
if(ch == '#')
{
*T = NULL;
}
else
{
*T = (BiTree)malloc(sizeof(BitNode));
if(!*T)
{
exit(OVERFLOW);
}
(*T)->data = ch;
CreateBiTree(&(*T)->lchild);/* 构建左子树 */
CreateBiTree(&(*T)->rchild);/* 构建右子树 */
}
}
10.线索二叉树原理
解决问题的方向:
当二叉树的左右孩子指针为空的时候,这样将浪费了内存中的空间。想找一个二叉树的前驱和后继,必须先对二叉树进行遍历,可以考虑在创建的时候记住这些前驱和后继,那
将是多大的时间上的节省。
定义:
指向前驱和后继的指针称为线索,加上线索的二叉链表成为线索链表,相应的二叉树成为线索二叉树(Threaded Binary Tree).
线索化:
对二叉树以某种次序遍历使其变为线索二叉树的过程称为线索化。
判断lchild是指向它的前驱还是左孩子,rchild是指向它的后继还是右孩子:
我们在每个结点再增设两个标志域Itag和rtag,注意Itag和rtag只是存放0或1数字的布尔型变量。
结构:
lchild Itag data rtag rchild
其中: Itag为0时指向该结点的左孩子,为1时指向该结点的前驱。
rtag为0时指向该结点的右孩子,为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;
实质:
将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是遍历的过程中修改空指针的过程。
中序遍历线索化的递归代码实现:
BiThrTree pre; /* 全局变量,始终指向刚刚访问过的结点 */
/* 中序遍历进行中序线索化 */
void InThreaing(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; /* 前驱右孩子指针指向后继 */
}
pre = p;/* 保持pre指向p的前驱 */
InThreading(p->rchild);/* 递归右子树线索化 */
}
}
有了线索二叉树后,我们对它进行遍历时发现,其实就等于是操作一个双向链表结构。
遍历的代码如下:
/* T指向头结点,头结点左链lchild指向根结点,头结点右键rchild指向中序遍历的*/
/* 最后一个结点,中序遍历二叉线索链表表示的二叉树T */
static InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p = T->lchild; /* p指向根结点 */
while(p != T) /* 空树或遍历结束时, p == T*/
{
while(p->LTag == Link)/* 当LTag == 0 时循环到中序序列第一个结点 */
p = p->lchild;
printf("%c",p->data); /*显示结点数据,可以更改为其他队结点操作 */
while(p->RTag == Thread && p->rchild != T)
{
p = p->rchild;
printf("%c",p->data);
}
p = p->rchild;/* p 进至右子树根 */
}
reutrn OK;
}
注意:和双向链表结构一样,在二叉树线索链表上添加一个头结点,并令其lchild域的指针指向二叉树的根结点,其rchild域的
指针指向中序遍历时访问的最后一个结点。反而,令二叉树的中序序列中的一个结点中,lchild域指针和最后一个结点的rchild
域指针均指向头结点。这样的好处就是我们即可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
小结:
由于它充分利用了空指针域的空间(这等于节省了空间),又保证了创建时的一次遍历就可以终生受用前驱后继的信息。所以在实际
问题中,如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉表的存储结构就是非常不错的选择。
11. 树、森林与二叉树的转换
1)树转换为二叉树
1.加线,在所有兄弟之间加一条连线
2.去线,对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线
3.层次调整,以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换
过来的孩子是结点的右孩子。
2)森林转换为二叉树
森林是由若干棵树组成的。
1.把每个树转换为二叉树
2.第一棵二叉树不动,从第二棵二叉树开始,一次把后一棵二叉树的根结点作为前一棵二叉树的根节点的右孩子,用线连接起来。当所有的二叉树连接
后就得到右森林转换来的二叉树。
3)二叉树转换为树
1.加线,若某结点的左孩子结点存在,则将这个左孩子的右孩子结点、右孩子的右孩子结点、右孩子的右孩子的右孩子结点。。。哈,反正就是左孩子
的n个右孩子结点都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
2.去线。删除原二叉树中所有结点与其右孩子结点的连线
3.层次调整,使之结构层次分明。
4)二叉树转换为森林
判断一棵二叉树能够转换成一棵树还是森林,标准很简单,那就是只要看这棵二叉树的根结点有没有右孩子,有就是森林,没有就是一棵树,
转换森林的方法:
1.从根结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除。。。,直到右孩子连线都删除
为止,得到分离的二叉树。
2.再将每棵分离后的二叉树转换为树即可
5)树与森林的遍历
树的遍历分为两种方式
1.一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树。
2。另一种是后根遍历,即先依次后根遍历每棵树,然后再访问根节点。
森林的遍历也为两种方式
1.前序遍历:先访问深林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余树构成的森林。
2.后序遍历:是先访问森林中第一棵树,后根遍历方式遍历每一棵树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林。
12.赫夫曼树
1)路径长度:
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称作路径长度。
树的路径长度就是从树根到每一结点的路径长度之和。
2)带权路径:
结点带权的路径长度为从该结点到树根之间的路径长度与结点上权的乘积。
树的带权路径长度为树中所有叶子结点的带权路径长度之和。
*带权路径长度WPL最小的二叉树称做赫夫曼树
3)构造最优二叉树
没啥好说的,一看就懂。