《大话数据结构》读书笔记3
文章目录
第六章 树
树的存储结构
1、双亲表示法
2、孩子表示法
3、孩子兄弟表示法
二叉树的性质
性质1:在二叉树的第i层上至多有2i-1个结点(i≥1)。(数学归纳法可证)
性质2:深度为k的二叉树最多有2k-1个结点(k≥1)。(由性质1,通过等比数列求和可证)
性质3:一棵二叉树的叶子结点数为n0,度为2的结点数为n2,则n0 = n2 + 1。
证:结点总数n = n0 + n1 + n2。设B为分支总数,因为除根节点外,其余结点都有一个分支进入,所以n = B + 1。又因为分支是由度为1或2的结点射出,所以B = n1 + 2n2。综上:n = n0 + n1 + n2 = B + 1 = n1 + 2n2 + 1,得出:n0 = n2 + 1。
性质4:具有n个结点的完全二叉树的深度为floor(log2n) + 1 。
性质5:如果对一棵有n个结点的完全二叉树(其深度为floor(log2n) + 1 )的结点按层序编号,则对任一结点i(1≤i≤n)有:
(1) 如果i = 1,则结点i是二叉树的根,无双亲;如果i > 1,则其双亲PARENT(i)是结点 floor((i)/2)。
(2)如果2i > n,则结点i无左孩子;否则其左孩子LCHILD(i)是结点2i。
(3)如果2i + 1 > n,则结点i无右孩子;否则其右孩子RCHILD(i)是结点2i + 1
遍历二叉树
已知前序遍历和后序遍历,是不能确定一棵二叉树的
前序遍历(M-L-R)
/* 初始条件: 二叉树T存在 */
/* 操作结果: 前序递归遍历T */
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
PreOrderTraverse(T->lchild); /* 再先序遍历左子树 */
PreOrderTraverse(T->rchild); /* 最后先序遍历右子树 */
}
中序遍历(L-M-R)
/* 初始条件: 二叉树T存在 */
/* 操作结果: 中序递归遍历T */
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return;
InOrderTraverse(T->lchild); /* 中序遍历左子树 */
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
InOrderTraverse(T->rchild); /* 最后中序遍历右子树 */
}
后序遍历(L-R-M)
/* 初始条件: 二叉树T存在 */
/* 操作结果: 后序递归遍历T */
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild); /* 先后序遍历左子树 */
PostOrderTraverse(T->rchild); /* 再后序遍历右子树 */
printf("%c",T->data);/* 显示结点数据,可以更改为其它对结点操作 */
}
层序遍历
按照层的高低进行排序,从上到下,从左到右
二叉树的建立
按前序输入二叉树中结点的值,建立二叉树
当然可以选择中序和后续,只需调整一下算法即可
/* 按前序输入二叉树中结点的值(一个字符) */
/* #表示空树,构造二叉链表表示二叉树T。 */
void CreateBiTree(BiTree *T)
{
TElemType ch;
/* scanf("%c",&ch); */
ch=str[treeIndex++];
if(ch=='#')
*T=NULL;
else
{
*T=(BiTree)malloc(sizeof(BiTNode));
if(!*T)
exit(OVERFLOW);
(*T)->data=ch; /* 生成根结点 */
CreateBiTree(&(*T)->lchild); /* 构造左子树 */
CreateBiTree(&(*T)->rchild); /* 构造右子树 */
}
}
线索二叉树
我们把这种指向前驱和后继的指针称为线索,加上线索的二叉树链表称为线索链表,相应的二叉树就称为线索二叉树
由于在二叉链表中,如果结点无左右孩子,那么指针就为∧,这样的空间就不存储任何事务。线索二叉树的提出就是可以解决这种浪费。
其实线索二叉树,等于把一颗二叉树转变成一个双向链表,这样对我们的插入删除结点、查找某个结点都带来了方便。所以我们对二叉树以某种次序遍历使其变成线索二叉树的过程称做事线索化。
我们如何知道某一个结点的lchild是指向它的左孩子还是指向前驱,……。所以我们在每个结点在增设两个标志域ltag和rtag,而这均为布尔类型,0(false)代表为孩子,1(true)代表为前驱或后继
线索化的过程就是在遍历的过程中修改指针的过程
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); /* 递归右子树线索化 */
}
}
有了线索二叉树后,我们对它进行遍历时发现,其实就等于是操作一个双向链表结构
和双向链表结构一样,在二叉树线索链表上添加一个头结点。令其lchild指向二叉树的根结点,其rchild指向中序遍历访问的最后一个结点;令中序遍历的第一个结点的lchild和最后一个结点的rchild均指向头结点,
这样的好处是我们既可以从第一个结点顺后继进行遍历,也可以从最后一个结点顺前驱进行遍历
/* 中序遍历二叉线索树T(头结点)的非递归算法 */
Status InOrderTraverse_Thr(BiThrTree T)
{
BiThrTree p;
p=T->lchild; /* p指向根结点 */
while(p!=T)
{ /* 空树或遍历结束时,p==T */
while(p->LTag==Link)
p=p->lchild;
if(!visit(p->data)) /* 访问其左子树为空的结点 */
return ERROR;
while(p->RTag==Thread&&p->rchild!=T)
{
p=p->rchild;
visit(p->data); /* 访问后继结点 */
}
p=p->rchild;
}
return OK;
}
树、森林和二叉树的转换
树转换为二叉树
1、加线——在所有兄弟结点之间加一条连线
2、去线——对树中的每个结点,只保留它与第一个孩子的连线
3、层次调整——以树的根节点为轴心顺时针旋转一定角度。第一个孩子为结点的左孩子,兄弟转换过来的孩子是节点的右孩子
森林转换为二叉树
1、把每个树转换为二叉树
2、第一个二叉树不动,从第二棵二叉树开始,依次把最后一棵二叉树的根结点作为前一棵二叉树的根结点右孩子,用线连接起来
二叉树转换为树和森林
把前面转换的过程反过来即可
树和森林的遍历
树
1、先根遍历——先访问数的根节点,然后依次先根遍历每一棵子树
2、后根遍历——先依次后根遍历每一棵子树,然后再访问根结点
森林
1、前序遍历
先根遍历森林的第一棵树,再依次用同样的方式遍历除去第一棵树的剩余森林
2、后续遍历
后根遍历森林的第一棵树,再依次用同样的方式遍历除去第一棵树的剩余森林
森林的前序遍历和二叉树的前序遍历结果相同,森林的后续遍历和二叉树的中序遍历结果相同
赫夫曼树及其应用
构造赫夫曼树的赫夫曼算法描述
1、n个权值构成n棵二叉树的集合F,其中每个二叉树只有一个带权值的根结点
2、选取权值最小的两个作为左右孩子构造一棵二叉树,该树权值为左右孩子权值之和
3、在F中删除这两棵树,新二叉树加入集合
4、重复2、3步骤,知道F只含一个树为止
赫夫曼编码
同样的方式遍历除去第一棵树的剩余森林
森林的前序遍历和二叉树的前序遍历结果相同,森林的后续遍历和二叉树的中序遍历结果相同
赫夫曼树及其应用
构造赫夫曼树的赫夫曼算法描述
1、n个权值构成n棵二叉树的集合F,其中每个二叉树只有一个带权值的根结点
2、选取权值最小的两个作为左右孩子构造一棵二叉树,该树权值为左右孩子权值之和
3、在F中删除这两棵树,新二叉树加入集合
4、重复2、3步骤,知道F只含一个树为止
赫夫曼编码
以编码的字符集和电文出现的频率构造赫夫曼树,规定赫夫曼树的左分支代表0,右分支为1.根节点到叶子结点所经过的路径组成的0和1的序列便是对应字符的编码,这就是赫夫曼编码。