目录
(重点内容:树和二叉树的性质、遍历、转换、存储和操作;满 完全 线索二叉树,哈夫曼树的定义性质;二叉排序树&二叉平衡树的性质操作)
5.1 树和二叉树的基本概念
5.1.1 树的基本概念
·Def:
树是n个结点的有限集。n=0时,为空树;n>0,有且仅有一个称为根的结点;n>1时其余结点可分为m个互不相交的有限集,每个有限集本身又是一棵树,称为根的子树(SubTree),子树又是由子树的根和子树根的子树组成的。
由此可见,树是一种递归的数据结构,且根结点没有前驱,根结点外的结点有且只有一个前驱;所有结点可以有零个或多个后继。
结点的度:结点拥有的子树个数/结点后继元素的个数/结点的分支数
树的度:树中各结点度的最大值
叶子:度为0 的结点,又称终端结点
分支结点:度不为0的结点,又称非终端结点,根结点外的分支结点也叫内部结点
树的深度:树中结点的最大层次
森林:m(m>=0)棵互不相交的树的集合
有序树:树中结点的各子树从左至右有次序
无序树:树中结点的各子树五次序
路径长度:路径上所经过的边的个数
·树的性质:
- 树中结点数等于所有结点地度数之和+1
- 度为n的树中第i层上最多有n^(i-1)个结点(i>=1)
- 具有n个结点的m叉树的最小高度为[logm(n(m-1)+1)]
- 深度为h的m叉树最多有(m^h -1)/(m-1)个结点
5.1.2 二叉树的基本概念Binary Tree
·Def:二叉树是n个结点的有限集,n>1时由一个根结点和两棵互不相交的根的左子树和右子树组成(左右子树的次序不能颠倒)
【注】:二叉树中不存在度大于2 的结点,即每个结点最多有两个孩子
【二叉树和树的区别】:二叉树结点的子树一定要有左右之分,即使只有一个子树也要说明是左子树还是右子树;而树无须区分左右
·二叉树的五种基本形态:
·二叉树的性质:
1)非空二叉树的第i层上最多有2^(i-1)个结点(i>=1);最少有1个结点
2)深度为h的二叉树最多有2^h -1个结点(h>=1);最少有h个结点
3)非空二叉树上的叶子结点的个数等于度为2的结点数+1,即n0=n2+1
·特殊的二叉树:
1)满二叉树:深度为h且含有2^h -1个结点的二叉树。树中的每层都是最大结点数,最底层全部都是叶子结点,叶子结点外的的每个结点的度均为2.
2)完全二叉树:深度为h,有n个结点的二叉树,当且仅当每个结点都与深度为h的满二叉树中的各编号结点一一对应时,为完全二叉树。
①具有n个结点的完全二叉树的深度为[log2n]+1 (由结点个数和树深度的不等式关系证得)
②对完全二叉树从上到下,从左到右进行编号,当结点i>1,则其双亲结点[i/2](i为偶数,为双亲左孩子,双亲=i/2,i为奇数为双亲右孩子,双亲=(i-1)/2); 如果2i<= n,则结点i的左孩子编号为2i,否则无左孩子;如果2i+ 1 <= n,则结点i右孩子编号为2i+ 1否则无右孩子。
·二叉树的存储结构:
1)顺序存储:按满二叉树的结点层次编号,从上到下,左到右以此存放二叉树中的数据元素,此方法对于完全和满二叉树比较合适;对于一般的二叉树只能添加一些不存在的空结点,让每个结点与完全二叉树上的结点相对照,再存储到一维数组的相应分量中,如下所示:
【缺点】:深度为k且只有k个结点的单支树需要长度为2^k -1的一维数组,造成极大的浪费
2)链式存储:由于顺序存储空间利用率较低,二叉树一般采用链式存储。每个结点用链表结点来储存,通常包括数据域、左指针域和右指针域,如下:
①在n个结点的二叉链表中,有n+1个空指针域(重要结论)
②三叉链表:增加一个parent域指向双亲:
5.2 二叉树的遍历
·先序遍历:
若二叉树为空,则空操作;否则
(1)访问根结点;
(2)先序遍历左子树;
(3)先序遍历右子树。
Status PreOrderTraverse(BiTree T){
if(T= =NULL) return OK; //空二叉树
else{
visit(T); //访问根结点
PreOrderTraverse(T-> Ichild); //递归遍历左子树
PreOrderTraverse(T-> rchild); //递归遍历右子树
}
}
·中序遍历
若二叉树为空,则空操作;否则
(1)中序遍历左子树;
(2)访问根结点;
(3)中序遍历右子树。
Status InOrderTraverse(BiTree T){
if(T==NULL) return OK; //空二叉树
else{
InOrderTraverse(T-> lchild); //递归遍历左子树
visit(T); //访问根结点;
InOrderTraverse(T-> rchild); //递归遍历右子树
}
}
非递归算法:借助栈让根结点进栈,再遍历左子树;根结点再出栈,输出根结点,再遍历右子树
Status InOrderTraverse (BiTree T) {
BiTree p=T; InitStack(S);
while(p || !StackEmpty(S) ) {
if(p) {
Push(S,p);
p = p-> lchild;
}
else {
Pop(S,p);
printf( "%c" , p->data);
p = p-> rchild;
}
}//while
return OK;
}
·后序遍历
若二叉树为空,则空操作;否则
(1)后序遍历左子树;
(2)后序遍历右子树;
(3)访问根结点。
Status PostOrderTraverse(BiTree T){
if(T= =NULL) return OK; //空树
else{
PostOrderTraverse(T-> Ichild); //递归遍历左子树
PostOrderTraverse(T-> rchild); //递归遍历右子树
visit(T); //访问根结点
}
}
【三种遍历】:三种遍历算法中只是访问根结点的顺序不同,从递归角度看,三种算法的访问路径是相同的,只是访问点的时机不同,不管采用哪种遍历,每个结点都仅且只访问一次,故时间复杂度均为O(n);递归的工作栈深恰好为树的深度,所以最坏情况下二叉树有n个结点且深度为n的单支树,空间复杂度为O(n).
·层次遍历:
过程:从根结点开始,从上到下,从左到右的顺序访问每一个结点,每个结点仅访问一次
思想:借助队列,先将根结点入队,队不空时循环从队中出列一个结点进行访问,若有左孩子,将左孩子进队;若有右孩子,将右孩子结点进队
算法:
void LevelOrder(BTNode *b) {
BTNode *p; SqQueue *qu;
InitQueue(qu); //初始化队列
enQueue(qu, b); //根结点指针进入队列
while (!QueueEmpty(qu)) {//队不为空,则循环
deQueue(qu, p); //出队结点p
printf("%C ", p-> data);//访问结点p
if (p->Ichild!=NULL)
enQueue(qu, p-> Ichild); //有左孩子时将其进队
if (p->rchild!=NULL)
enQueue(qu, p-> rchild); //有右孩子时将其进队
}
}
5.3 线索二叉树
·基本概念:传统二叉链表存储中不能直接得到结点在遍历中的前驱或后继,但在n个结点的二叉树中,还有n+1个空指针,利用它们可以存放指向前驱后继的指针,即为线索,这样就加快了查找结点前驱和后继的速度。
在线索二叉树中若无左子树,则令lchild指向其前驱结点;若无右子树,则令rchild指向后继结点。同时还增加两个标志域标识是指向左右孩子还是前驱后继,所以结点的结构为:
·先序线索二叉树:
·中序线索二叉树:
·后序线索二叉树:
【补充】:为方便从前往后或从后往前对线索二叉树进行遍历,可以在二叉树的线索链表上添加一个头结点,令其lchild指针指向二叉树根结点,rchild指向遍历时访问的最后一个结点;令二叉树遍历序列的第一个结点的lchild和最后一个结点的rchild均指向头结点:
5.4 树、森林
5.4.1 树的存储结构
1)双亲表示法:定义结构数组,存放树的结点,每个结点包含数据域和双亲域;数据域存放本身的信息,双亲域指示本结点的双亲结点在数组中的位置。这种方式找双亲容易但是找孩子较难。
2)孩子表示法:将每个结点的孩子结点用单链表链接起来形成一个线性结构,n个结点就有n个孩子链表,其中叶子结点的孩子链表为空表。这样找孩子容易,找双亲较难,若此时为每个结点增加一个parent域指向其父结点,则查找父结点也变得方便
3)孩子兄弟表示法:用二叉链表作树的存储结构,链表中每个结点的两个指针域分别指向其第一个孩子的结点和下一个兄弟结点
5.4.2树、森林和二叉树之间的转换
1)树与二叉树:树和二叉树都可用二叉链表作为存储结构,所以二叉链表可以作为媒介导出树与二叉树的对应关系,具体规则即树中每个结点左指针指向它的第一个孩子,右指针指向树中相邻的右兄弟
【画法】
树转换为二叉树:①在兄弟之间加一连线;②对每个结点,除了其左孩子外,去除其与其余孩子之间的关系;③以树的根结点为轴心,将整树顺时针转45°
二叉树转换为树:①若某结点是双亲结点的左孩子,则将该结点的右孩子,右孩子的右孩....沿分支找到的所有右孩子,都与该结点的双亲连起来;②抹掉原二叉树中双亲与右孩子之间的连线;③将结点按层次排列,形成树结构
2)森林与二叉树:
【画法】
森林转换为二叉树:①将各棵树分别转换成二叉树;②将每棵树的根结点用线相连;③以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转构成二叉树型结构
二叉树转换为森林:①将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二叉树;②将孤立的二叉树还原成树
5.4.3 树和森林的遍历
·树的遍历
1)先根遍历:若树不空,则先访问根结点,然后依次先根遍历各棵子树。
2)后根遍历:若树不空,则先依次后根遍历各棵子树,然后访问根结点。
3)层次遍历:若树不空,则自上而下自左至右访问树中每个结点。
·森林的遍历
1)先序遍历
若森林不空,则
①访问森林中第一棵树的根结点;
②先序遍历森林中第一棵树的子树森林;
③先序遍历森林中除第一棵树之外其余树构成的森林。
2)中序遍历
若森林不空,则
①中序遍历森林中第一棵树的子树森林;
②访问森林中第一棵树的根结点;
③中序遍历森林中除第一棵树之外其余树构成的森林。
5.5树和二叉树的应用
5.5.1 哈夫曼树
·Def:在含有n个带权叶结点的二叉树中,其中WPL最小的二叉树称为哈夫曼树
路径:从树中一-个结点到另个结点之间的分支构成这两个结点间的路径
结点路径长度:两结点间路径上的分支数
树的路径长度:从树根到每个结点的路径长度之和(结点数相同的二叉树中,完全二叉树是路径最短的二叉树,但路径最短的二叉树不一定是完全二叉树)
权:一个赋予结点某种含义的数值
结点的带权路径长度:从根结点到该结点的路径长度与该结点的权的乘积
树的带权路径长度:树中所有叶子结点的带权路径长度之和,记:
【例】:
·构造算法
思想:
1)根据n个给定的权值{W1, W2, .... Wn}构成n棵二叉树的森林{T1, T2.... Tn},其中Ti只有一个带权为Wi的根结点。(构造森林全是根)
2)在森林中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。(选用两小造新树)
3)在F中删除这两棵树,同时将新得到的二叉树加入森林中。(删除两小添新人)
4)重复2)和3) 直到森林中只有一棵树为止,这棵树即为哈夫曼树。
特点:
1)哈夫曼树的结点的度数为0或2,不存在度数为1的结点
2)包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点,且这些新结点都是具有两个孩子的分支结点,所以哈夫曼树中共有n+ n-1=2n-1个结点
3)所有初始结点最终都成为叶子结点,且权值越小到根结点的路径长度越大
实现:
1)初始化HT[1...2n-1]:lch=rch=parent=0;
2)输入初始n个叶子结点:置HT[1...n]的weight值;
3)进行以下n-1次合并,依次产生n-1个结点HT[i],i=n+1...2n-1:
①在HT[1...i- 1]中选两个未被选过(从parent == 0的结点中选)的weight最小的两个结点HT[s1]和HT[s2], s1、s2为两个最小结点下标;
②修改HT[s1]和HT[s2]的parent值: HT[s1] .parent=i; HT[s2] .parent=i;
③修改新产生的HT[i]:
HT[i].weight= HT[s1].weight + HT[s2].weight;
HT[i]. Ich=s1; HT[i]. rch=s2;
void CreatHuffmanTree (HuffmanTree HT, int n){ //构造哈夫曼树
if(n<= 1) return;
m=2*n-1; //数组共2n-1个元素
HT=new HTNode[m+1]; //0号单元未用, HT[m]表示根结点
for(i=1;i<=m;++i){ //将2n-1个元素的Ich. rch. parent置为0
HT[j]Ich=0;
HT[j]rch=0;
HT[].parent=0;
}
for(i=1;i<=n;++i) cin>>HT[j.weight]; //输入前n个元素的weight值
//初始化结束,开始建立哈夫曼树
for(i=n+1;i<=m;i++){ //合井产生n-1个结点--构造Huffman树
Select(HT, i-1,s1, s2); //在HT[k](1≤ksi-1)中选择两个其双亲域为0,且权值最小的结点并返回它们在HT中的序号s1和s2
HT[s1].parent=i; HT[s2].parent=i; //表示从F中删除s1,s2
HT[i].lch=s1; HT[i].rch=s2 ; //s1,s2分别作为的左右孩子
HT[j.weight=HT[s1].weight + HT[s2].weight; //i的权值为左右孩子权值之和
}
}
·哈夫曼编码
前缀编码:设计长度不等的编码,没有一个编码是另一个编码的前缀
哈夫曼编码:1)统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短);2)利用哈夫曼树的特点(权越大的叶子离根越近)将每个字符的概率值作为权值,构造哈夫曼树,则概率越大的结点, 路径越短;3)在哈夫曼树的每个结点的左分支标0,右分支标1,把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。
因为哈夫曼树的带权路径长度最短,这样编码能设计出总长度最短的二进制前缀编码。
【例】:设组成电文的字符集D及其概率分布W为: D={ A,B,C,D,E,F, G};W={0.40 0.30 0.15 0.05 0.04 0.03 0.03},设计哈夫曼编码
实现:文件的编码:
①输入各字符及其权值
②构造哈夫曼树HT[i]
③进行哈夫曼编码HC[i]
④查HC[i],得到各字符的哈夫曼编码
文件的解码:
①构造哈夫曼树
②依次读入二进制码
③读入0,则走向左孩子;读入1,则走向右孩子
④一旦到达某叶子时,即可译出字符
⑤然后再从根出发继续译码,指导结束