由遍历序列确定二叉树
从二叉树的遍历可知,任何一棵二叉树的前序遍历和中序遍历都是唯一的,那么给定结点的前序序列和中序序列能否确定一棵二叉树?
二叉树的前序序列中,第一个结点必定是根D;根节点D可将中序序列分成两部分:D之前是左子树的中序序列,D之后是右子树的中序序列,再根据左子树的中序序列,又可将前序序列除根以外分成左子树的前序序列和右子树的前序序列,以此类推,便可递归得到整棵二叉树。
结论:给定一颗二叉树的前序序列和中序序列,可以唯一的确定一棵二叉树。
二叉树的线索化
线索化是在遍历的过程中修改空指针域的过程。
对二叉树按照不同的遍历次序进行线索化,可以得到先序,中序,后序线索二叉树。
示例
中序线索化算法思路:按照中序遍历的规则,
当访问的节点root root的前驱结点pre
root无左--->prey为root的前驱
root为pre的后继<---pre无右。
void Inthread(BiTree root) // 对root所指的二叉树进行中序线索化,其中pre始终指向刚访问过的结点,其初值为NULL { if(roo!=NULL) Inthread(root->LChild); // 线索化左子树 if(root->LChild==NULL) { root->Ltag=1;LChild=pre; // 置前驱线索 if(pre!=MULL && pre->RChild==NULL) // 置后继线索 pre->RChild=root;pre->Rtag=1; pre=root; Inthread(root->RChild); // 线索化右子树 } }线索化之后如何找前驱后继,是线索化的重要作用
1)找结点的中序前驱结点
假设中序遍历序列如下:
问题:P的中序前驱x节点在树中什么位置?
答:x结点应该在p的左子树的“最右下端”。
左子树最后一个被访问的点
void InPre(BiTNode*p,BiTNode*pre) // 在中序线索二叉树中查找p的中序前驱,并用pre指针返回结果 { if(p->Ltag==1) pre=p->LChild; // 直接利用线索 else { // 在p的左子树中查找“最右下端”结点 for(q=p->LChild;q=Rtag=0;q=q->RChild); pre=q; } }2)在中序线索数中找结点后继--没有右孩子直接就是后继指针,有右孩子为右子树第一个访问节点
假设中序遍历序列如下
问题:p的中序后继结点x在树中的什么位置?
答:x节点应该在p的右子树的最左下端。
void InSucc(BiTNode*p,BiTNode*succ) // 在中序线索二叉树中查找p的后继结点,并用succ指针返回结果 { if(p->Rtag==1) succ=p->RChild; // 直接利用线索 else { // 在p的右子树中查找“最左下端”结点 for(q=p->RChild;q=Ltag=0;q=q->LChild); succ=q; } }3)在先序线索二叉树中找结点p的后继--有左子树,后继为左子树根结点。没有左子树。为右子树的根或者后继指
if(p->Ltag ==0 )succ = p->LChild;else succ = p->RChild;4)在先序线索二叉树中找结点p的前驱
假设P的父结点为F,则:
情况一:P为F的左子
结论一:P的先序前驱为F
情况二:P为F的右子,且F无左子
结论二:P的先序前驱仍为F
5)在后序线索二叉树中找结点p的前驱
if(p->RLtag ==0 )succ = p->RChild;else pre = p->LChild;线索树的插入删除结点时,可能会破坏原树的线索。所以在线索二叉树中,插入或删除结点的难度在于:插入一个结点之后,依然要维持正确的线索。
1)插入结点运算
在中序线索二叉树中插入结点可分为两种情况:
第一种,将新的节点插入到二叉树中做某结点的左孩子
第二种,将新的节点插入到二叉树中,作为某结点的右孩子,
我们仅讨论后一种情况。InsNode(BiTNode*p,BiTNode*r)表示在线索二叉树中插入r所指向的结点,做p所指结点的右孩子。
1)若结点p的右孩子为空,则插入结点r的过程很简单,原来的p的后继变为r的后继,结点p变为r的前驱,结点r成为p的右孩子,结点r的插入对p原来的后继节点没有任何的影响。
节点的右孩子为空时的插入过程为:
2)若p的右孩子不为空,则插入后,p的右孩子变为r的右孩子结点,p变为r的前驱结点,r变为p的右孩子结点,这时还需要修改原来p的右子树中“最左下端”结点的左指针域,使它由原来的指向指针结点p变为指向结点r。
插入的过程为:
void InsNode(BiTNode*p,BiTNode*r) { if(p->Rtag == 1)// p无右孩子 { r->RChild = p->RChild;// p的后继变为r的后继 r->Rtag = 1; p->RChild = r;// r成为p的右孩子 r->LRChild = p;// p变为r的前驱 r->Ltag=1; p->Rtag=0; } else// p有右孩子 { s=p->PChild; while(s->Ltag==0) s=s->LChild;// 查找p结点的右孩子的最左下端结点 r->RChild = p->RChild;// p的右孩子变为r的右孩子 r->Rtag = 0; r->RChild =p->RChild;// p变为r的前驱 r->Rtag =0; r->LRChild =p;// p变为r的前驱 r->Ltag =1; p->RChild=r;// r变为p的右孩子 s->LChild=r;// r变为p原来右子树的“最左下端”结点的前驱 } }2)删除节点运算
与插入操作一样,在线索二叉树中删除一个节点也会破坏原来的线索,所以需要在删除的过程中保持二叉树的线索化。
在程序线索二叉树中删除节点二的过程为:
1 树转换为二叉树
方法:
1)树中所有相邻兄弟之间加一条连线,
2)对树中的每个结点,只保留其与第一个孩子结点之间的连线,删去其与其他孩子结点之间的连线。
3)以树的根结点为轴心,将整个树顺时针旋转一定的角度,使之层次结构分明。
通过转换过程可以看出,数中的任意一个节点都应该对应于二叉树中的一个结点。树中某节点的第一个孩子在二叉树中是相应结点的左孩子,树中某结点的右兄弟结点在二叉树中也是相应结点的右孩子。
也就是说,在二叉树中,左分枝上的各节点在原来的树中是父子关系。而右分支上的各节点在原来的树中是兄弟关系,由于树的根结点没有兄弟,所以变换后的二叉树的根结点的右孩子必然会空。
树与二叉树的对应关系及转换---借助链表储存结构实现
树和森林遍历
若树非空,则遍历方法为
1)访问根结点。
2)从左到右,依次先根遍历根结点的每一棵子树。
先根遍历序列为:ABECFHGD。
若树非空,则遍历方法为
1)从左到右,依次后根遍历根结点的每一棵子树。
2)访问根结点。
小结:
1,树的结点没有固定的编号方式;
2,可以按照层次关系对树中的结点进行遍历;
3,通过游标的思想设计遍历成员函数;
4,遍历成员函数是互相依赖,相互配合的关系;
5,遍历算法的核心为队列的使用;
哈夫曼树
【问题1】什么样的二叉树的路径长度PL最小?
一颗树的路径长度为0结点只有1个(根),
路径长度为1结点至多只有2个(两个孩子);
路径长度为2结点至多只有4个;
以此类推:路径长度为k结点至多只有2^k个,
所以n个结点二叉树其路径长度至少等于如下序列的前n项之和。
路径长度0112222233 3333334结点数n=1n=2n=3n=4n=5n=6n=7n=7n=8 n=15……由此可知,结点n对应的路径长度为log2n
前n项之和:∑k=1->n(log2k)
完全二叉树的路径长度为:
2^0*0+2^1*1+2^2*2+……2^h*h= ∑ k=1->h(log2k)
h为树的深度,其路径长度可达到最小,所以完全二叉树具有最小路径长度的性质,但不具有唯一性。
构造哈夫曼树
哈夫曼树又叫最优二叉树,它是有n个带权叶子结点构成的所有二叉树当中带权路径长度WPL最短的二叉树。
构造步骤如下:
1)给定n个权值:{w1,w2,……wn}构造森林。F={T1,T2,……Tn},其中Ti为单根树,根的权值为wi。
2)在森林F选择两棵树根结点权值最小的二叉树,作为一个新二叉树的左右子树,标记新二叉树的根结点权值为其左右子树的根结点权值之和。
3)从F中删除被选中的那两颗二叉树,同时把新构成的二叉树加入到森林F中,
4)重复以上操作2),3),直到森林中只含有一个二叉树为止,此时得到的这颗二叉树就是哈弗曼树。
由于哈弗曼树中没有度为1的结点,则一个有n个叶子的哈夫曼树共有2xn-1个结点,可用一个大小为2xn-1的一维数组来存放各个结点。每个结点同时还包含其双亲信息和孩子节点信息,构成一个静态的三叉链表:
WeightParentLChildRChildtypedef struct { int weight; int parent,LChild,RChild; }HTNode,HuffmanTree[M+1]; Huffmantree crt_huffmantree(HuffmanTree ht,int w[],int n) { // w存放n个字符和指令的权值 m=2*n-1; for(p=ht,i=1;i<=n;i++,p++,w++)*p={*w,0,0,0}; for(;,i<=m;i++,p++)*p={0,0,0,0};// 数组初始化2n-1个点, for(i=n+1;i<=m;i++) {select(ht,i-1,&s1,&s2); // 选择parent为0且weight最小的两个结点,其序号分别赋值给所,s1,s2, 选森林中的点(双亲yu为0 ht[s1].parent=i; ht[s2].parent=i; ht[i].Lchild=s1; ht[i].Rchild=s2; ht[i].weight=ht[s1].weight+ht[s2],weight; }// 哈夫曼树建立完毕 } // 从叶子节点到根逆向求每个字符或者指令的哈夫曼编码 hc=(haffmancode)malloc(n*sizeof(char*)); // 分配n个字符编码的头指针 cd=(char*)malloc(n*sizeof(char*)); cd[n-1]=' \0 '; // 编码结束符 for(j=0;j<n;j++) { start=n-1; for(c=j;f=ht[j].parent;f!=0;c=f;f=ht[f].parent) // 从叶子到根结点求编码 if(ht[f],Lchild==c)cd[-start]='0'; else cd[-start]='1'; hc[j]=(char*)malloc((n-strat)*sizeof(char)); // 为第j个指令编码配分空间 strcpy(hc[i],&cd[start]); } free(cd); }
哈弗曼编码的概念
1)前缀码:如果在一个编码系统中,任一个编码都不是其它任何编码的前缀(最左字串),则称该编码系统中的编码是前缀码。
2)哈夫曼编码:对于一个具有n个叶子的哈夫曼树,若对树中的每一个左分支赋予0,右分支赋予1,则从根到每个叶子每个叶子上的通路上,各分支的赋值分别构成一个二进制的串,该二进制串就成为是哈夫曼编码。
哈夫曼编码的相关性质
【定理一】哈夫曼编码是前缀码。
证明:哈夫曼编码是根到叶子路径上边的编码序列,由树的特点知,若路径A是路径B的最左部分,则B经过了A,即A的终点不是叶子。而哈弗曼编码都会对应终点为叶子的路径,所以任人一哈夫曼编码都不会与其他的Huffman编码的前部重合,所以哈夫曼编码是前缀码。
【定理二】哈夫曼编码是最优前缀码。
哈夫曼编码能使各种报文(由这n种字符构成的文本)对应的二进制串的平均长度最短。
证明:由于哈弗码编码对应叶权为各字符使用的频率的哈夫曼树,因此,该树为带权长度最小的树,即∑i=1->n(wipi)最小,其中wi是第i个字符的使用频率,而pi是第i个字符的编码长度,这正是度量报文的平均长度的式子。
哈夫曼编码最典型的应用是在编码技术上的应用,利用哈夫曼树可以得到平均长度最短的编码。
为了减小程序总位数,采用变长编码。
下面的变长编码是否可行?
指令I1I2I3I4I5I6I7编码010010000001010不行,因为机器无法解码
如对编码串0010110该怎么识别呢?
第一个0可以识别为I1,也可以和第二个0组成的00一起被识别为I3
结论:变长编码必须满足的条件,任意一个编码不能成为其他任意编码的前缀,我们把满足这个条件的编码叫前缀编码。
利用哈夫曼编码,可设计出最优前缀编码
首先利用每条指令的使用频率为权值构造哈夫曼树。
若规定:向左的分支标记为1,向右的分支标记为0
从根结点开始,走到叶节点,所经过的代码序列,就构成了相应指令的哈夫曼编码。
指令I1I2I3I4I5I6I7编码01011011100111011111011111若一段程序有10000条指令,其中I1有400条,I2有300条,I3有150条,I4有50条,I5有40条,I6有30条,I7有30条,则:
定长编码:程序总位数为:3*1000=3000
哈夫曼编码:程序总位数为:
1*400+2*300+3*150+5*(50+40+30+30)=2200
哈夫曼编码的平均码长为:∑pi*Ii=0.4*1+0.3*2+0.15*3+(0.05+0.04+0.03)*5=2.20
构造满足哈夫曼编码的最短最优性质;
1)若di≠dj(字母不同)则对应的树叶不同,因此前缀码(任意字符的编码都不是另一个字符的编码)不同。一个路径不可能是其他路径的一部分,所以字母之间可以完成区别
2)将所有的字母变成二进制的哈夫曼编码,使得带权路径长度最短,相当总的道路长度最短。
构造满足哈夫曼编码的最短最优性质;
1)若di≠dj(字母不同)则对应的树叶不同,因此前缀码(任意字符的编码都不是另一个字符的编码)不同。一个路径不可能是其他路径的一部分,所以字母之间可以完成区别
2)将所有的字母变成二进制的哈夫曼编码,使得带权路径长度最短,相当总的道路长度最短。