树是大话数据结构中篇幅较多的四章之一,也是十分常见的数据结构。需要好好掌握
目录
定义
树是n个结点的有限集。n=0时称为空树。在任意一颗非空树中:
- 有且仅有一个特定的称为根的结点
- 当n>1时,其余结点可分为m(m>0)个互不相交的树,称为树的子树
结点分类
- 结点拥有的子树数称为结点的度(Degree)
- 度为0的结点称为叶节点;度不为0的结点称为分支节点(内部节点)
- 树的度是树内部结点中度的最大值
结点间关系
子树的根称为该结点的孩子,该结点称为孩子的双亲。同双亲的孩子之间互称兄弟,结点的祖先是从根结点到此结点所经分支上所有结点,反之某结点子树中任意结点都是该结点子孙。
其他概念
结点的层次:根是第一层,其孩子是第二层,以此类推;树中结点的最大层次为树的深度或者高度。
如果孩子结点有次序,不能互换的树是有序树,否则是无序树。
森林是m课互不相交的树的集合。对于根节点的子树集合,就可以是森林。
树的存储结构
双亲表示法
数组表示,由于双亲可以唯一的表示,所以可以自定义结构体,用双亲结点指针指出双亲在哪个下标(只有双亲结点的话查找孩子结点就需要遍历,这时候就可以在结构体中再定义比如左子节点、右子节点之类的,设计可以很灵活,但是如果设置太多子节点,可能造成很多资源浪费,当然可以设置一个degree度来表明有几个子结点,但是这样也有了需要维护度和不同数据结构的麻烦与消耗)
孩子表示法
为了克服上面不同数据结构造成的麻烦,我们把每个结点的孩子用单链表作为存储,如下图所示
所有结点再一个数组里,firstchild指向第一个子结点,子结点的数据结构child表示子结点在数组中的下标,然后指向下一个子结点,如果没有则为NULL(当然这样的表示方法查找子结点容易了,但是查找双亲结点有点麻烦,可以在构造数组的时候加上parent的空间)
孩子兄弟表示法
一个结点的长子结点和右兄弟结点是确定的,所以结点的数据结构就使用数据、长子结点指针、右兄弟结点指针构建
如右图
从此我们就把一颗复杂的树变成了一颗二叉树。
二叉树
就是最多只有两个子结点的有序树,特殊的二叉树有
- 斜树:左斜树——所有结点只有左子树;右斜树——所有结点只有右子树;可以看做线性表(线性表就是树的一种特殊形式)
- 满二叉树:所有结点的都存在左右子树,并且所有叶子节点都在同一层
- 完全二叉树:满二叉树按照结点编号1234排下去,完全二叉树需要满足结点包含的编号必须是连续的比如1 2 3 4 5 6 7 8 9 10,如图
二叉树的性质
- 第i层至多2^(i-1)个结点
- 深度为k的二叉树最多2^k -1个结点
- 对任何一颗二叉树T,终端结点数为n0,度为2的结点数为n2 则n0=n2+1
- 具有n个结点的完全二叉树的深度为[
]+1([x]表示不大于x的最大整数)
- n个结点的完全二叉树的结点如果有双亲(根结点除外),结点编号为i,双亲编号为[i/2];如果2i>n,则结点i无左子结点,否则左子结点是2i;如果2i+1>n,则结点i无右子结点,否则右孩子是结点2i+1
- 二叉树可以用顺序存储,下标即编号,顺序存储一般只用于完全二叉树
- 二叉链表用数据结构【【lchild】【data】【rchild】】表示,左孩子,数据,右孩子
遍历二叉树
遍历二叉树是二叉树中的重点,遍历的时候讲究访问和次序。
所谓访问就是要根据实际的需求来确定需要具体做什么,比如对每个结点进行计算啊,打印啊,是一个抽象的操作。
而次序,分为前序遍历,中序遍历,后序遍历
前序遍历:根,左,右
二叉树的遍历算法也采用递归,而且每种次序只是其中的代码顺序不同
//二叉树的前序遍历
void PreOrderTraverse(BiTree T)
{
if(T==NULL)
return;
printf("%c",T->data);//显示结点数据,单然这里是先输出根的数据
PreOrderTraverse(T->lchild);//再 先序遍历 左子树
PreOrderTraverse(T->rchild);//最后 后序遍历 右子树
}
中序遍历:左,根,右
//二叉树的中序遍历
void InOrderTraverse(BiTree T)
{
if(T==NULL)
return;
InOrderTraverse(T->lchild);// 中序遍历 左子树
printf("%c",T->data);//显示结点数据, 输出根的数据,也可以改为对结点的其他操作
InOrderTraverse(T->rchild);//最后 中序遍历 右子树
}
后序遍历:左,右,根
//二叉树的后序遍历
void PostOrderTraverse(BiTree T)
{
if(T==NULL)
return;
PostOrderTraverse(T->lchild);// 中序遍历 左子树
PostOrderTraverse(T->rchild);//然后 中序遍历 右子树
printf("%c",T->data);//显示结点数据, 输出根的数据,也可以改为对结点的其他操作
}
层序遍历:从上而下逐层遍历
二叉树的建立
二叉树的建立依照前中后层序依次输入,如果为空则输入#,程序较为简单不再赘述
线索二叉树
线索二叉树将二叉链树的NULL左右子叶指针利用起来,指向该结点的前驱(左孩子指针)和后继(右孩子指针),实际上加了前驱后继就等于把一颗二叉树转换成了一个双向链表,也称线索化。
- 为了区分是前驱后继还是左右孩子,我们用ltag和rtag的布尔变量来标明,0表示左右孩子,1表示前驱后继
最后我们用头指针指向一个头结点,头结点的左孩子指向根结点,后继指向最后一个结点,如下图所示
如果所用到的二叉树经常遍历或者查找结点时候需要以某种遍历顺序查找其中的前驱和后继,那么采用线索二叉树十分合适。
树、森林和二叉树的转换
树可以用孩子兄弟法表示成二叉链表存储,所以说,我们的树和二叉树之间可以相互转换(只要设定一定的规定,就可以用二叉树表示树,甚至是森林)
树转换为二叉树
- 加线:在所有兄弟节点之间加一条连线
- 去线:去掉节点中与其他非长子结点的连线
- 层次调整。以树的根节点作为轴心,顺时针旋转一定角度,使得第一个孩子是左孩子,兄弟是右孩子(实则在孩子兄弟表示法中是兄弟)
这样物理意义依然明确,只是之前的树的表示是以双亲结点与子结点直接连接表示的,现在的二叉树表示树,就是用兄弟表示是哪一个双亲结点的孩子。
森林转换成二叉树
- 将每一个树转换为二叉树
- 第一颗二叉树不懂,依次将后一颗二叉树的根节点作为前一个二叉树的根结点的右孩子(循环至最后一个二叉树)
就得到了森林的二叉树表示
二叉树转换成树
- 加线:如果某节点的左孩子存在,那么将左孩子的兄弟,兄弟的兄弟,兄弟的兄弟的兄弟……反正就是这个双亲的孩子都连接到双亲结点上
- 去线:去掉原来二叉树中所有连接到兄弟的线
- 层次调整
二叉树转换森林
- 从根节点开始,若右孩子存在,则删除连线,分离后的二叉树如此循环,或者说递归,直到所有右孩子连线都没有了
- 然后将每颗分离后的二叉树转换成树即可
树和森林的遍历都有两种,先根后子树与先子树后根,也可以借用二叉树的前序和中序遍历实现。
霍夫曼树
我记得在图像处理的时候有讲过霍夫曼编码,总的来说就是通过统计频率根据频率编码,使得平均码长最短,是一种最优编码
这里的话如法炮制,就是将频率进行排序融合排序融合直至根节点比较完毕,最后构成的哈夫曼树必须左孩子数值(概率)小,右孩子数值大。
BST AVLT
分别是二叉查找树和平衡二叉树的英文缩写,这里只概念上大概介绍,以便需要用到的时候知道有这个东西,再去详细学习。
二叉查找树的定义
- 二叉查找树要么是一个空树
- 要么由根结点、左子树、右子树构成(有时候没有左右子树,那么就是叶子结点),必须满足左子树上所有的结点均小于等于根结点的数值,右子树上所有的结点均大于根结点的值。
二叉查找树的查找效率、插入效率、删除效率都很高,是O(h), h是二叉查找树的高度
平衡二叉树
AVLT就是再BST的基础上,防止插入顺序造成树的结构变得h很大,在每次插入、删除的时候如果需要,就要进行结构上的一些小变化,使得h始终是log(n)的一种树。主要差别还在于引入了平衡因子的概念,是BST plus.
好了,树的部分,就这么简要的介绍完毕了,如果有疑惑的可以在评论区留言,或者看看《大话数据结构》的原著。