(一)树的基本概念
(二)二叉树
二叉树的定义及其主要特征;二叉树的顺序存储结构和链式存储结构
二叉树的遍历;线索二叉树的基本概念和构造
(三)树、森林
树的存储结构;森林与二叉树的转换;树和森林的遍历
(四)树与二叉树的应用
哈夫曼 (Huffinan) 树和哈夫曼编码;并查集及其应用
树是N个结点的有限集合,N=0时,称为空树。
树中一个结点的子结点个数称为该结点的度,树中结点的最大度数称为树的度。
结点的层次从树根开始定义,根结点为第1层。
结点的深度是从根结点开始自顶向下逐层累加的;结点的高度时从叶结点开始自底向上逐层累加的。
**树的高度(又称深度)**是树中结点的最大层数。
树中两个结点之间的路径是由这两个结点之间所经过的结点序列构成的,而路径长度是路径上所经过的边的个数。
森林是 (m>=0) 棵互不相交的树的集合
1)树中的结点数等于所有结点的度数加1.
2)度为m的树中第i层上至多有mi-1个结点(i≥1)。
3)高度为h的m叉树至多有(mh-1)/(m-1)个结点。
4)具有n个结点的m叉树的最小高度为logm(n(m-1)+1)向上取整。
二叉树:每个结点至多只有两棵子树(即二叉树不存在度大于2的结点),子树有左右之分,不能任意颠倒次序。
1)满二叉树:一棵高度为h,并且含有2^h-1个结点的二叉树称为满二叉树,即树中的每一层都含有最多的结点,除叶子结点之外的每个结点度数均为2.对于编号为i的结点,如果有双亲,其双亲为i/2(向下取整),如果有左孩子,则左孩子为2i,如果有右孩子,则右孩子为2i+1。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RQUBZ2dA-1651294490200)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220430115117162.png)]
2)完全二叉树:设一个高度为h,有n个结点的二叉树,当且仅当其每一个结点都与高度为h的满二叉树中编号为1~n的结点一一对应时,称为完全二叉树。
1)若i≤n/2(向下取整),则结点i为分支结点,否则为叶子结点。
2)叶子结点只可能在层次最大的两层上出现。对于最大层次中的叶子结点,都依次排列在该层最左边的位置上。
3)如果有度为1的结点,只可能有一个,且该结点只有左孩子而无右孩子。
4)若n为奇数,则每个分支结点都有左子女和右子女;若n为偶数,则编号最大的分支结点(编号为n/2)只有左子女,没有右子女,其余分支结点左右子女都有。
3)二叉排序树:左子树上所有结点的关键字均小于根结点的关键字,右子树上所有结点的关键字均大于根结点的关键字。
4)平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1.
二叉树的顺序存储结构就是用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素。
完全二叉树和满二叉树采用顺序存储比较合适。
注意:二叉树属于树,都可以用树的存储结构来存储,但是树却不都能用二叉树的存储结构来存储。
二叉树的链式存储结构:链式结构是指用一个链表来存储一棵二叉树,二叉树中每一个结点用链表的一个链结点来存储。二叉链表至少包含三个域:数据域data、左指针域lchild、右指针域rchild。
在含有n个结点的二叉链表中含有n+1个空链域。
二叉树的链式存储结构描述如下:
typedef struct BiTNode{
ElemType data; //数据域
struct BiTNode *lchild, *rchild; //左、右孩子指针
}BiTNode, *BiTree;
先序遍历PreOrder:访问根结点,先序遍历左子树,先序遍历右子树。(NLR)
void PreOrder(BiTree T){
if(T!=NULL){
visit(T); //访问根节点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
中序遍历InOrder:中序遍历左子树,访问根结点,中序遍历右子树。(LNR)
void PreOrder(BiTree T){
if(T!=NULL){
visit(T); //访问根节点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
后序遍历PostOrder:后序遍历左子树,后序遍历右子树,访问根结点。(LRN)
void PreOrder(BiTree T){
if(T!=NULL){
PreOrder(T->lchild); //递归遍历左子树
visit(T); //访问根节点
PreOrder(T->rchild); //递归遍历右子树
}
}
时间复杂度均为O(n),在递归遍历中,递归工作栈的栈深恰好为树的高度。
层次遍历:需要借助一个队列。先将二叉树根结点入队,然后出队,访问该结点,如果它有左子树,则将左子树根结点入队;如果它有右子树,则将右子树根结点入队。然后出队,对出队结点访问,如此反复,直到队列为空。
void LevelOrder(BiTree T){
InitQueue(Q); //初始化辅助队列
BiTree p;
EnQueue(Q,T); //将根节点入队
while(!IsEmpty(Q)){ //队列不空则循环
DeQueue(Q,p); //队头结点出队
visit(p); //访问出队结点
if(p->lchild!=NULL)
EnQueue(Q,p->lchild); //左子树不空,则左子树根节点入队
if(p->lchild!=NULL)
EnQueue(Q,p->rchild); //右子树不空,则右子树根节点入队
}
}
由二叉树的先序序列和中序序列可以唯一确定一棵二叉树;
由二叉树的后序序列和中序序列可以唯一确定一棵二叉树;
由二叉树的层序序列和中序序列可以唯一确定一棵二叉树。
如果只知道二叉树的先序序列和后序序列,则无法确定一棵二叉树。
线索二叉树:为了加快查找结点前驱和后继的速度。
在二叉树线索化时,通常规定:若无左子树,令lchild指向其前驱结点;若无右子树,令rchild指向其后继结点,还需要增加那个标志域表明当前指针域所指对象时指向左(右)子结点还是直接前驱(后继)。
ltag=0(lchild域指示结点的左孩子)
ltag=1(lchild域指示结点的前驱)
rtag=0(rchild域指示结点的右孩子)
rtag=1(rchild域指示结点的后继)
树的存储结构:既可以采用顺序存储结构,也可以采用链式存储结构。
双亲表示法:采用一组连续空间来存储每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置。
利用了每个结点(根结点除外)只有唯一双亲的性质,可以很快得到每个结点的双亲结点,但是求结点的孩子时需要遍历整个结构。
孩子表示法:将每个结点的孩子结点都用单链表链接起来形成一个线性结构,则N个结点就有N个孩子链表(叶子结点的孩子链表为空表)。
寻找子女的操作非常直接,而寻找双亲的操作需要遍历N个结点中孩子链表指针域所指向的N个孩子链表。
孩子兄弟表示法:又称为二叉树表示法,即以二叉树表作为树的存储结构,使每个结点包括三部分内容:结点值、指向结点第一个孩子结点的指针和指向结点下一个兄弟结点的指针(沿此域可以找到结点的所有兄弟结点)。
可以方便地实现树转换为二叉树的操作,易于查找结点的孩子等,但缺点是从当前结点查找其双亲结点比较麻烦。
给定一棵树,可以找到唯一的一棵二叉树与之对应。
树转换为二叉树的规则:每个结点左指针指向它的第一个孩子结点,右指针指向它在树中的相邻兄弟结点,可表示为“左孩子右兄弟”。由于根结点没有兄弟,所以由树转换而得的二叉树没有右子树。
森林转换为二叉树的规则与树相似。
二叉树转换为森林的规则:若二叉树非空,则二叉树根及其左子树为第一棵树的二叉树形式,二叉树根的右子树又可以看做是一个由除第一棵树外的森林转换后的二叉树,应用同样的方法,直到最后产生一棵没有右子树的二叉树为止。
二叉树转换为树和森林是唯一的。
树的遍历:
先根遍历:若树非空,则先访问根结点,再按从左到右的顺序遍历根结点的每一棵子树。其访问顺序与这棵树相应二叉树的先序遍历顺序相同。
后根遍历:若树非空,则按从左到右的顺序遍历根结点的每一棵子树,之后再访问根结点。其访问顺序与这棵树相应二叉树的中序遍历顺序相同。
层次遍历:与二叉树的层次遍历思想基本相同,即按层序依次访问各结点。
森林的遍历:
先序遍历森林:访问森林中第一棵树的根结点;先序遍历第一棵树中根结点的子树森林;先序遍历除去第一棵树之后剩余的树构成的森林。
中序遍历森林:中序遍历森林中第一棵树的根结点的子树森林;访问第一棵树的根结点;中序遍历除去第一棵树之后剩余的树构成的森林。
哈夫曼树的构造:给定N个权值分别为w1~wN的结点
1)将这N个结点分别作为N棵仅含一个结点的二叉树,构成森林F
2)构造一个新结点,并从F中选取两棵根结点权值最小的树作为新结点的左右子树,并且将新结点的权值置为左右子树上根结点的权值之和
3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中
4)重复2)和3),直至F中只剩下一棵树为止
特点:
1)每个初始结点最终都成为叶结点,并且权值越小的结点到根结点的路径长度越大
2)构造过程中共新建了N-1个结点(双分支结点),因此哈夫曼树的结点总数为2N-1
3)每次构造都选择2棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点
可变长度编码:对频率高的字符赋以短编码,而对频率较低的字符赋以较长一些的编码,从而使字符平均编码长度减短,起到压缩数据的效果。
如果没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。
由哈夫曼树构造哈弗曼编码:首先将每个出现的字符当做一个独立的结点,其权值为它出现的频度,构造出相应的哈夫曼树。显然,所有字符结点都出现在叶结点中。我们可以将字符的编码解释为从根至该字符的路径上边标记的序列,其中边标记为0表示“转向左孩子”,标记为1表示“转向右孩子”。
1)Initial(S): 将集合S中的每个元素都初始化为只有一个单元素的子集合。
1)Union(S,Root1,Root2): 把集合S中的子集合 Root2 并入子集合 Root1 。要求 Root1和Root2 互不相交,否则不执行合并。
3)Find(S,x): 查找集合S中单元素x所在的子集合,并返回该子集合的根结点。