数据结构:树

以下内容来源于《大话数据结构》一书
一、树

是n(n>=0)个结点的有限集。n=0时称为空树。在任意一颗非空树中:(1)有且仅有一个特定的称为(Root)的结点;(2)当n>1时,其余节点可分为m(m>0)个互不相交的有限集,其中每一个集合本身又是一颗树,并且称为根的子树(SubTree)。
下图就是一棵树:
在这里插入图片描述
B以及B所有的分支,C以及C所有的分支都是根节点A的子树,D以及D所有的分支又是节点B的子树。
注意:树的根节点是唯一的,子树的数量没有限制,但是子树间不可相交。

树的节点包含一个数据元素以及指向其子树的分支,节点拥有的子树数称为节点的。度为0的节点称为叶节点或者终端节点。度不为0的节点称为非终端节点分支节点。树的度是树内各个结点的度的最大值,例如下图的树的度最大值是结点D的度,为3:
在这里插入图片描述
树中结点的最大层次称为树的深度或者高度,例如前面上图的例子树的深度就为4。
如果将树中的结点的各子树看成左右不能互换的,则称该树为有序树,否则为无序树

二、二叉树

二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两颗互不相交的、分别称为根节点的左子树和右子树的二叉树组成。特点:每个结点最多有两颗子树;左子树和右子树是有顺序的;树中某结点只有一颗子在这里插入图片描述树,也要区分是左子树还是右子树。
下图是一个二叉树:
在这里插入图片描述
二叉树的特点:
1.每个结点最多两个子树,也就是说度最大不大于2
2.二叉树是有序树
3.即使某结点只有一个子树也要分左右
斜树:所有结点都只有左子树的二叉树叫做左斜树。所有结点都只有右子树的二叉树叫右斜树。
在这里插入图片描述
满二叉树:在一颗二叉树中,如果所有分支节点都存在左子树和右子树,并且所有叶子都在同一层上。
在这里插入图片描述
完全二叉树:对一颗具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同。
特点:
(1)叶子结点只能出现在最下两层
(2)最下层的叶子集中在左部连续位置。
(3)倒数二层,若有叶子节点,一定都在右部连续位置。
(4)如果结点度为1,则该结点只有做孩子,即不存在只有右子树的情况。
(5)同样结点数的二叉树,完全二叉树的深度最小。
在这里插入图片描述
如上图,若10、11存在,则此二叉树为完全二叉树,否则为不完全二叉树。
判断是否是完全二叉树的方法,将二叉树从上到下从左到右排序,如果中间出现空挡则为不完全二叉树,否则为完全二叉树。

二叉树的性质:
1.在二叉树的第i层上最多有2^(i-1)个结点(i≥1)。
2.深度为i的二叉树最多有2^i-1个结点(i≥1)。
3.对于任意一颗二叉树,如果其终端结点数为n,度为2的结点数为m,则n=m+1。
4.具有n个结点的完全二叉树的深度为|log₂n|+1(||表示取整数部分)。
5.如果对一颗有n个结点的完全二叉树的结点按层序编号,对任一结点i有:
5.1 如果i=1则此结点是二叉树的根,如果i>1则其双亲是结点|i/2|
5.2 如果i>n/2,则此结点无左孩,否则其左孩是结点2i
5.3 如果2i+1>n,则此结点无右孩,否则其右孩是结点2i+1

二叉树的顺序存储结构
我们将如下完全二叉树的值按序号放到数组里,可以很严格的表示顺序,当不是完全二叉树时,我们可以将空缺的序号先用null代替,一样按序号填入,如空缺的较多,如右斜树,则空缺过多,不适于使用顺序结构。
在这里插入图片描述
在这里插入图片描述
二叉树的链式结构
当顺序存储结构不适应时,可以考虑链式结构,因为二叉树每个结点最多有两个子树,所以可以设计两个指针和一个数据参数,这样的链表叫做二叉链表,再加上一个指向双亲的指针的就称为三叉链表。

typedef struct Bitnode
{
	TElemType data;
	struct Bitnode *lchild ,*rchild;
}Bitnode,*Bitree

二叉树的建立
首先由这样一个二叉树(如下图普通二叉树),为了让每个结点确认是否有左右子树,我们将他进行扩展,也就是将每个空结点的指针引出一个虚结点,值设为#,这样就可以通过遍历序列确定一颗二叉树,比如下图的前序递归遍历顺序为AB#D##C##( 前序遍历后文有提到),代码如下。
在这里插入图片描述

void CreateBiTree(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);//构造右子树
	}
}

二叉树的遍历方法
1.前序遍历
若二叉树为空,返回,否则访问根结点,然后前序遍历(根节点排最先,然后同级先左后右)左子树然后前序遍历右子树,如下图的顺序是:ABDGHCEIF。
在这里插入图片描述
2.中序遍历
若二叉树为空,返回,否则从根结点开始(不是访问根结点),中序遍历(先左后根最后右)根结点左子树,然后访问根结点,最后中序遍历右子树,如下图的顺序是:GDHBAEICF。
在这里插入图片描述
3.后序遍历
若二叉树为空,返回,否则从左到右先叶子后结点的方式遍历访问子树,最后访问根结点,如下图的顺序是:GHDBIEFCA。
在这里插入图片描述
4.层序遍历
若二叉树为空,返回,否则从树第一层开始,从上到下从左到右逐个访问结点,如下图的顺序是:ABCDEFGHI。
在这里插入图片描述
前序遍历算法
采用递归方式:

void PreOrderTraverse(BiTree T)
{
	if(T==NULL)
		return;
	printf("%c",T->data);//输出结点数据,可改为其他操作
	PreOrderTraverse(T->lchild);//先序遍历左子树
	PreOrderTraverse(T->rchild);//最后先序遍历右子树
}

中序遍历算法
采用递归方式:

void PreOrderTraverse(BiTree T)
{
	if(T==NULL)
		return;
	PreOrderTraverse(T->lchild);//中序遍历左子树
	printf("%c",T->data);//输出结点数据,可改为其他操作
	PreOrderTraverse(T->rchild);//最后中序遍历右子树
}

后序遍历算法
采用递归方式:

void PreOrderTraverse(BiTree T)
{
	if(T==NULL)
		return;
	PreOrderTraverse(T->lchild);//后序遍历左子树
	PreOrderTraverse(T->rchild);//后序遍历右子树
	printf("%c",T->data);//输出结点数据,可改为其他操作
}

线索二叉树
线索二叉树就是在二叉树的基础上增加一个指向前驱结点和一个后继结点的指针的二叉树,实现代码如下:

typedef enum {Link,Thread} PointerTag;//Link=0表示左右子树指针,Thread=1表示指向前驱或后继
typedef struct BiThrNode
{
	TElemType data;//结点数据
	struct BiThrNode *lchild,*rchild;//左右孩子指针
	PointerTag LTag;
	PointerTag RTag;//左右标志
}BiThrNode,*BiTherTree;

线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索,由于前驱和后继的信息只有在遍历该二叉树的时候才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。
中序遍历线索化的递归代码如下:

void InThreading(BiThrTee 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);//递归右子树线索化
	}
}

可以看到这段代码在第一个if中的第一行和最后一行都和中序遍历的递归代码一样,只是将打印结点的功能改成了线索化功能。
if(!p->lchild)表示如果某结点的左指针域为空,因为其前驱结点刚刚访问过,赋值给了pre,所以可以将pre赋值给p->lchild,并修改p->LTag=Thread(也就是定义为1)以完成前驱结点的线索化。
后继就会麻烦一些,因为此时p结点的后继还没有访问到,因此只能对它的前驱结点pre的右指针rchild做判断,if(!pre->rchild)表示如果为空,则p就是pre的后继,于是pre->rchild=p,并且设置pre->RTag=Thread,完成后继结点的线索化。
完成前驱和后继的判断后,将结点p赋值给pre,以便于下一次使用。
非递归方式遍历:
在这里插入图片描述

//T指向头节点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点
Status 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指向其右子树根
	}
	return OK;
}

赫夫曼树
赫夫曼树也叫最优二叉树,是带权路径最小的二叉树。举个例子,学生的成绩分为优秀、良好、中等、及格、不及格五档,五档的人数如下表:
在这里插入图片描述
将上表生成两个带权的二叉树a、b,a的路径长度为1+1+2+2+3+3+4+4=20,b的路径长度为1+2+3+3+2+1+2+2=16,a的带权路径长度为5x1+15x2+40x3+30x4+10x4=315,b的带权路径长度为5x3+15x3+40x2+30x2+10x2=220。同样的数据,当使用b树时,判断的次数将会减少1/3,那么b这样的树是怎么构造出来的呢。
在这里插入图片描述
1.先把有权值的叶子根据从小到大的顺序排列成一个有序序列,即:A5,E10,B15,D30,C40。
2.取头两个最小权值的结点作为一个新结点N₁的两个子结点,注意相对较小的是左孩子,这里A为N₁的左孩子,E为N₁的右孩子,如下图,新结点的权值为两个叶子权值的和5+10=15。
在这里插入图片描述
3.将N₁替换A与E,插入有序序列中,保持从小到大排列。即:N₁15,B15,D30,C40。
4.重复步骤2,将N₁与B作为一个新的结点N₂的两个子结点。如下图,N₂的权值为15+15=30。
在这里插入图片描述
5.将N₂替换N₁与B,插入有序序列中,保持从小到大排列。即:N₂30,D30,C40。
6.重复步骤2,将N₂和D作为一个新结点N₃的两个子结点,如下图所示,N₃的权值为30+30=60。
在这里插入图片描述
7.将N₃替换N₂与D,插入有序序列中,保持从小到大排列。即:N₃60,C40。
8.重复步骤2,将N₃和C作为一个新结点T的两个子结点,如下图所示,由于T是根结点,完成赫夫曼树构造。
在这里插入图片描述
此时二叉树的带权路径长度40x1+30x2+15x3+10x4+5x4=205,与前面的220相比,这个树还少了15,显然这才是最优二叉树。虽然这个带权路径最小,但是因为每次判断都要经过两次比较,所以论性能反而不如220的那个二叉树。

总结赫夫曼树的构造过程:
1.根据给定的n个权值{W₁,W₂,W₃…Wₙ}构成n棵二叉树的集合F={T₁,T₂,T₃…Tₙ},其中每棵二叉树T₁中只有一个带权为W₁根结点,其左右子树均为空。
2.在F中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
3.在F中删除这两棵树,同时将新得到的二叉树加入F中。
4.重复2和3步骤,直到F只含一棵树为止,此树便是赫夫曼树。

赫夫曼编码
首先来看一个例子,我们有一段文字内容为“BADCADFEED”要通过网络传输给别人,显然用二进制来表示是我们最容易想到的,我们现在可以将A-F用二进制数据表示:
在这里插入图片描述
这样,这串字符就变成了“001000011010000011101100100011”,根据这几个字母出现的频率A 27,B 8,C 15, D 15,E 30,F 5,我们也可以用赫夫曼树来重新规划:
在这里插入图片描述
将左分支用0表示,右分支用1表示,可以变成下图:
在这里插入图片描述
用上图来编码,可以得到:
在这里插入图片描述
此时的新编码是:1001010010101001000111100,此时编码的长度大约比原编码缩小了17%,当我们接收到赫夫曼编码时,需要有一个规则去解码,此时,一个同样的赫夫曼规则就很重要。以下 是赫夫曼编码规则:
一般,设需要编码的字符串为{d₁,d₂,d₃…dₙ},各个字符在电文中出现的次数或频率集合为{W₁,W₂,W₃…Wₙ},以d₁,d₂,d₃…dₙ为叶子的结点,以W₁,W₂,W₃…Wₙ作为相应叶子结点的权值来构造赫夫曼树,规定赫夫曼树的左分支代表0,右分支代表1,从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这就是赫夫曼编码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值