二叉树
第六章 二叉树
6.1 定义与性质
6.1.1 二叉树的基本概念
1.二叉树
二叉树(Binary Tree)是个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个结点。
二叉树是有序的,即若将其左、右子树颠倒,就成为另一棵不同的二叉树。即使树中结点只有一棵子树,也要区分它是左子树还是右子树。因此二叉树具有五种基本形态,如图6.1所示。

2.二叉树的相关概念
(1)结点的度。结点所拥有的子树的个数称为该结点的度。
(2)叶结点。度为0的结点称为叶结点,或者称为终端结点。
(3)分枝结点。度不为0的结点称为分支结点,或者称为非终端结点。一棵树的结点除叶结点外,其余的都是分支结点。
(4)左孩子、右孩子、双亲。树中一个结点的子树的根结点称为这个结点的孩子。这个结点称为它孩子结点的双亲。具有同一个双亲的孩子结点互称为兄弟。
(5)路径、路径长度。如果一棵树的一串结点n1,n2,…,nk有如下关系:结点ni是ni+1的父结点(1≤i<k),就把n1,n2,…,nk称为一条由n1至nk的路径。这条路径的长度是k-1。
(6)祖先、子孙。在树中,如果有一条路径从结点M到结点N,那么M就称为N的祖先,而N称为M的子孙。
(7)结点的层数。规定树的根结点的层数为1,其余结点的层数等于它的双亲结点的层数加1。
(8)树的深度。树中所有结点的最大层数称为树的深度。
(9)树的度。树中各结点度的最大值称为该树的度。
(10)满二叉树。
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子结点都在同一层上,这样的一棵二叉树称作满二叉树。如图6.2所示,(a)图就是一棵满二叉树,(b)图则不是满二叉树,因为,虽然其所有结点要么是含有左右子树的分支结点,要么是叶子结点,但由于其叶子未在同一层上,故不是满二叉树。

(11)完全二叉树。
一棵深度为k的有n个结点的二叉树,对树中的结点按从上至下、从左到右的顺序进行编号,如果编号为i(1≤i≤n)的结点与满二叉树中编号为i的结点在二叉树中的位置相同,则这棵二叉树称为完全二叉树。完全二叉树的特点是:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。如图6.3所示(a)为一棵完全二叉树,(b)和图6.2(b)都不是完全二叉树。

6.1.2 二叉树的主要性质
性质1 一棵非空二叉树的第i层上最多有2i-1个结点(i≥1)。
该性质可由数学归纳法证明。证明略。
性质2 一棵深度为k的二叉树中,最多具有2k-1个结点。
性质3 对于一棵非空的二叉树,如果叶子结点数为n0,度数为2的结点数为n2,则有:
n0=n2+1。
证明 设n为二叉树的结点总数,n1为二叉树中度为1的结点数,则有:
n=n0+n1+n2 (6-1)
在二叉树中,除根结点外,其余结点都有唯一的一个进入分支。设B为二叉树中的分支数,那么有:
B=n-1 (6-2)
这些分支是由度为1和度为2的结点发出的,一个度为1的结点发出一个分支,一个度为2的结点发出两个分支,所以有:
B=n1+2n2 (6-3)
综合(6-1)、(6-2)、(6-3)式可以得到:
n0=n2+1
性质4 具有n个结点的完全二叉树的深度k为[log2n]+1。
证明 根据完全二叉树的定义和性质2可知,当一棵完全二叉树的深度为k、结点个数为n时,有
2k-1-1<n≤2k-1
即 2k-1≤n<2k
对不等式取对数,有
k-1≤log2n<k
由于k是整数,所以有k=[log2n]+1。
性质5 对于具有n个结点的完全二叉树,如果按照从上至下和从左到右的顺序对二叉树中的所有结点从1开始顺序编号,则对于任意的序号为i的结点,有:
(1)如果i>1,则序号为i的结点的双亲结点的序号为i/2(“/”表示整除);如果i=1,则序号为i的结点是根结点,无双亲结点。
(2)如果2i≤n,则序号为i的结点的左孩子结点的序号为2i;如果2i>n,则序号为i的结点无左孩子。
(3)如果2i+1≤n,则序号为i的结点的右孩子结点的序号为2i+1;如果2i+1>n,则序号为i的结点无右孩子。
此外,若对二叉树的根结点从0开始编号,则相应的i号结点的双亲结点的编号为(i-1)/2,左孩子的编号为2i+1,右孩子的编号为2i+2。
此性质可采用数学归纳法证明。证明略。
6.3 二叉树的遍历
6.3.1 二叉树的遍历方法及递归实现
二叉树的遍历是指按照某种顺序访问二叉树中的每个结点,使每个结点被访问一次且仅被访问一次。
遍历是二叉树中经常要用到的一种操作。因为在实际应用问题中,常常需要按一定顺序对二叉树中的每个结点逐个进行访问,查找具有某一特点的结点,然后对这些满足条件的结点进行处理。
通过一次完整的遍历,可使二叉树中结点信息由非线性排列变为某种意义上的线性序列。也就是说,遍历操作使非线性结构线性化。
由二叉树的定义可知,一棵由根结点、根结点的左子树和根结点的右子树三部分组成。因此,只要依次遍历这三部分,就可以遍历整个二叉树。若以D、L、R分别表示访问根结点、遍历根结点的左子树、遍历根结点的右子树,则二叉树的遍历方式有六种:DLR、LDR、LRD、DRL、RDL和RLD。如果限定先左后右,则只有前三种方式,即DLR(称为先序遍历)、LDR(称为中序遍历)和LRD(称为后序遍历)。
1.先序遍历(DLR)
先序遍历的递归过程为:若二叉树为空,遍历结束。否则,
(1)访问根结点;
(2)先序遍历根结点的左子树;
(3)先序遍历根结点的右子树。
先序遍历二叉树的递归算法如下:
void PreOrder(BiTree bt)
{/*先序遍历二叉树bt*/
if (bt==NULL) return; /*递归调用的结束条件*/
Visite(bt->data); /*访问结点的数据域*/
PreOrder(bt->lchild); /*先序递归遍历bt的左子树*/
PreOrder(bt->rchild); /*先序递归遍历bt的右子树*/
}
算法 6.5
对于图6图6.3(b)所示的二叉树,按先序遍历所得到的结点序列为:
A B D G C E F
2.中序遍历(LDR)
中序遍历的递归过程为:若二叉树为空,遍历结束。否则,
(1)中序遍历根结点的左子树;
(2)访问根结点;
(3)中序遍历根结点的右子树。
中序遍历二叉树的递归算法如下:
void InOrder(BiTree bt)
{/*中序遍历二叉树bt*/
if (bt==NULL) return; /*递归调用的结束条件*/
InOrder(bt->lchild); /*中序递归遍历bt的左子树*/
Visite(bt->data); /*访问结点的数据域*/
InOrder(bt->rchild); /*中序递归遍历bt的右子树*/
}
算法 6.6
对于图6.3(b)所示的二叉树,按中序遍历所得到的结点序列为:
D G B A E C F
3.后序遍历(LRD)
后序遍历的递归过程为:若二叉树为空,遍历结束。否则,
(1)后序遍历根结点的左子树;
(2)后序遍历根结点的右子树。
(3)访问根结点;
后序遍历二叉树的递归算法如下:
void PostOrder(BiTree bt)
{/*后序遍历二叉树bt*/
if (bt==NULL) return; /*递归调用的结束条件*/
PostOrder(bt->lchild); /*后序递归遍历bt的左子树*/
PostOrder(bt->rchild); /*后序递归遍历bt的右子树*/
Visite(bt->data); /*访问结点的数据域*/
}
算法 6.7
对于图图6.3(b)所示的二叉树,按先序遍历所得到的结点序列为:
G D B E F C A
4.层次遍历
所谓二叉树的层次遍历,是指从二叉树的第一层(根结点)开始,从上至下逐层遍历,在同一层中,则按从左到右的顺序对结点逐个访问。对于图6.3(b)所示的二叉树,按层次遍历所得到的结果序列为:
A B C D E F G
下面讨论层次遍历的算法。
由层次遍历的定义可以推知,在进行层次遍历时,对一层结点访问完后,再按照它们的访问次序对各个结点的左孩子和右孩子顺序访问,这样一层一层进行,先遇到的结点先访问,这与队列的操作原则比较吻合。因此,在进行层次遍历时,可设置一个队列结构,遍历从二叉树的根结点开始,首先将根结点指针入队列,然后从对头取出一个元素,每取一个元素,执行下面两个操作:
(1)访问该元素所指结点;
(2)若该元素所指结点的左、右孩子结点非空,则将该元素所指结点的左孩子指针和右孩子指针顺序入队。
此过程不断进行,当队列为空时,二叉树的层次遍历结束。
在下面的层次遍历算法中,二叉树以二叉链表存放,一维数组Queue[MAXNODE]用以实现队列,变量front和rear分别表示当前对首元素和队尾元素在数组中的位置。
void LevelOrder(BiTree bt)
/*层次遍历二叉树bt*/
{ BiTree Queue[MAXNODE];
int front,rear;
if (bt==NULL) return;
front=-1;
rear=0;
queue[rear]=bt;
while(front!=rear)
{front++;
Visite(queue[front]->data); /*访问队首结点的数据域*/
if (queue[front]->lchild!=NULL) /*将队首结点的左孩子结点入队列*/
{ rear++;
queue[rear]=queue[front]->lchild;
}
if (queue[front]->rchild!=NULL) /*将队首结点的右孩子结点入队列*/
{ rear++;
queue[rear]=queue[front]->rchild;
}
}
}
算法 6.8
6.3.3 由遍历序列恢复二叉树
从前面讨论的二叉树的遍历知道,任意一棵二叉树结点的先序序列和中序序列都是唯一的。反过来,若已知结点的先序序列和中序序列,能否确定这棵二叉树呢?这样确定的二叉树是否是唯一的呢?回答是肯定的。
根据定义,二叉树的先序遍历是先访问根结点,其次再按先序遍历方式遍历根结点的左子树,最后按先序遍历方式遍历根结点的右子树。这就是说,在先序序列中,第一个结点一定是二叉树的根结点。另一方面,中序遍历是先遍历左子树,然后访问根结点,最后再遍历右子树。这样,根结点在中序序列中必然将中序序列分割成两个子序列,前一个子序列是根结点的左子树的中序序列,而后一个子序列是根结点的右子树的中序序列。根据这两个子序列,在先序序列中找到对应的左子序列和右子序列。在先序序列中,左子序列的第一个结点是左子树的根结点,右子序列的第一个结点是右子树的根结点。这样,就确定了二叉树的三个结点。同时,左子树和右子树的根结点又可以分别把左子序列和右子序列划分成两个子序列,如此递归下去,当取尽先序序列中的结点时,便可以得到一棵二叉树。
同样的道理,由二叉树的后序序列和中序序列也可唯一地确定一棵二叉树。因为,依据后序遍历和中序遍历的定义,后序序列的最后一个结点,就如同先序序列的第一个结点一样,可将中序序列分成两个子序列,分别为这个结点的左子树的中序序列和右子树的中序序列,再拿出后序序列的倒数第二个结点,并继续分割中序序列,如此递归下去,当倒着取取尽后序序列中的结点时,便可以得到一棵二叉树。
下面通过一个例子,来给出右二叉树的先序序列和中序序列构造唯一的一棵二叉树的实现算法。
已知一棵二叉树的先序序列与中序序列分别为:
A B C D E F G H I
B C A E D G H F I
试恢复该二叉树。
首先,由先序序列可知,结点A是二叉树的根结点。其次,根据中序序列,在A之前的所有结点都是根结点左子树的结点,在A之后的所有结点都是根结点右子树的结点,由此得到图6.10 (a)所示的状态。然后,再对左子树进行分解,得知B是左子树的根结点,又从中序序列知道,B的左子树为空,B的右子树只有一个结点C。接着对A的右子树进行分解,得知A的右子树的根结点为D;而结点D把其余结点分成两部分,即左子树为E,右子树为F、G、H、I,如图6.10 (b)所示。接下去的工作就是按上述原则对D的右子树继续分解下去,最后得到如图6.10 (c)的整棵二叉树。
(a) (b) (c)
图6.10 一棵二叉树的恢复过程示意
上述过程是一个递归过程,其递归算法的思想是:先根据先序序列的第一个元素建立根结点;然后在中序序列中找到该元素,确定根结点的左、右子树的中序序列;再在先序序列中确定左、右子树的先序序列;最后由左子树的先序序列与中序序列建立左子树,由右子树的先序序列与中序序列建立右子树。
下面给出用C语言描述的该算法。假设二叉树的先序序列和中序序列分别存放在一维数组preod[ ]与inod[ ]中,并假设二叉树各结点的数据值均不相同。
void ReBiTree(char preod[ ],char inod[ ],int n,BiTree root)
/*n为二叉树的结点个数,root为二叉树根结点的存储地址*/
{ if (n≤0) root=NULL;
else PreInOd(preod,inod,1,n,1,n,&root);
}
算法 6.11
void PreInOd(char preod[ ],char inod[ ],int i,j,k,h,BiTree *t)
{* t=(BiTNode *)malloc(sizeof(BiTNode));
*t->data=preod[i];
m=k;
while (inod[m]!=preod[i]) m++;
if (m==k) *t->lchild=NULL
else PreInOd(preod,inod,i+1,i+m-k,k,m-1,&t->lchild);
if (m==h) *t->rchild=NULL
else PreInOd(preod,inod,i+m-k+1,j,m+1,h,&t->rchild);
}
算法 6.12
需要说明的是,数组preod和inod的元素类型可根据实际需要来设定,这里设为字符型。另外,如果只知道二叉树的先序序列和后序序列,则不能唯一地确定一棵二叉树。
6.5 二叉树的应用
6.5.1 二叉树遍历的应用
在以上讨论的遍历算法中,访问结点的数据域信息,即操作Visite(bt->data)具有更一般的意义,需根据具体问题,对bt数据进行不同的操作。下面介绍几个遍历操作的典型应用。
1.查找数据元素
Search(bt,x)在bt为二叉树的根结点指针的二叉树中查找数据元素x。查找成功时返回该结点的指针;查找失败时返回空指针。
算法实现如下,注意遍历算法中的Visite(bt->data)等同于其中的一组操作步骤。
BiTree Search(BiTree bt,elemtype x)
{/*在bt为根结点指针的二叉树中查找数据元素x*/
BiTree p;
if (bt->data==x) return bt; /*查找成功返回*/
if (bt->lchild!=NULL) return(Search(bt->lchild,x));
/*在bt->lchild为根结点指针的二叉树中查找数据元素x*/
if (bt->rchild!=NULL) return(Search(bt->rchild,x));
/*在bt->rchild为根结点指针的二叉树中查找数据元素x*/
return NULL; /*查找失败返回*/
}
算法 6.21
2.统计出给定二叉树中叶子结点的数目
(1)顺序存储结构的实现
int CountLeaf1(SqBiTree bt,int k)
{/*一维数组bt[2k-1]为二叉树存储结构,k为二叉树深度,函数值为叶子数。*/
total=0;
for(i=1;i<=2k-1;i++)
{ if (bt[i]!=0)
{ if ((bt[2i]==0 && bt[2i+1]==0) || (i>(2k-1)/2))
total++;
}
}
return(total);
}
算法 6.22
(2)二叉链表存储结构的实现
int CountLeaf2(BiTree bt)
{/*开始时,bt为根结点所在链结点的指针,返回值为bt的叶子数*/
if (bt==NULL) return(0);
if (bt->lchild==NULL && bt->rchild==NULL) return(1);
return(CountLeaf2(bt->lchild)+CountLeaf2(bt->rchild));
}
算法 6.23
3.创建二叉树二叉链表存储,并显示。
设创建时,按二叉树带空指针的先序次序输入结点值,结点值类型为字符型。输出按中序输出。
CreateBinTree(BinTree *bt)是以二叉链表为存储结构建立一棵二叉树T的存储,bt为指向二叉树T根结点指针的指针。设建立时的输入序列为:AB0D00CE00F00。
建立如图6.3 (b)所示的二叉树存储。
InOrderOut(bt)为按中序输出二叉树bt的结点。
算法实现如下,注意在创建算法中,遍历算法中的Visite(bt->data)被读入结点、申请空间存储的操作所代替;在输出算法中,遍历算法中的Visite(bt->data)被c语言中的格式输出语句所代替。
void CreateBinTree(BinTree *T)
{/*以加入结点的先序序列输入,构造二叉链表*/
char ch;
scanf("\n%c",&ch);
if (ch=='0') *T=NULL; /*读入0时,将相应结点置空*/
else {*T=(BinTNode*)malloc(sizeof(BinTNode)); /*生成结点空间*/
(*T)->data=ch;
CreateBinTree(&(*T)->lchild); /*构造二叉树的左子树*/
CreateBinTree(&(*T)->rchild); /*构造二叉树的右子树*/
}
}
void InOrderOut(BinTree T)
{/*中序遍历输出二叉树T的结点值*/
if (T)
{ InOrderOut(T->lchild); /*中序遍历二叉树的左子树*/
printf("%3c",T->data); /*访问结点的数据*/
InOrderOut(T->rchild); /*中序遍历二叉树的右子树*/
}
}
main()
{BiTree bt;
CreateBinTree(&bt);
InOrderOut(bt);
}
算法 6.24
4.表达式运算
我们可以把任意一个算数表达式用一棵二叉树表示,图6.15所示为表达式3x2+x-1/x+5的二叉树表示。在表达式二叉树中,每个叶结点都是操作数,每个非叶结点都是运算符。对于一个非叶子结点,它的左、右子树分别是它的两个操作数。
图6.15 表达式3x2+x-1/x+5的二叉树表示示意
对该二叉树分别进行先序、中序和后序遍历,可以得到表达式的三种不同表示形式。
前缀表达式 +-+*3*xxx/1x5
中缀表达式 3*x*x+x-1/x+5
后缀表达式 3xx**x+1x/-5+
中缀表达式是经常使用的算术表达式,前缀表达式和后缀表达式分别称为波兰式和逆波兰式,它们在编译程序中有着非常重要的作用。
6.5.2 最优二叉树――哈夫曼树
1.哈夫曼树的基本概念
最优二叉树,也称哈夫曼(Haffman)树,是指对于一组带有确定权值的叶结点,构造的具有最小带权路径长度的二叉树。
那么什么是二叉树的带权路径长度呢?
在前面我们介绍过路径和结点的路径长度的概念,而二叉树的路径长度则是指由根结点到所有叶结点的路径长度之和。如果二叉树中的叶结点都具有一定的权值,则可将这一概念加以推广。设二叉树具有n个带权值的叶结点,那么从根结点到各个叶结点的路径长度与相应结点权值的乘积之和叫做二叉树的带权路径长度,记为:
|
WPL= Wk·Lk
其中Wk为第k个叶结点的权值,Lk 为第k个叶结点的路径长度。如图6.16所示的二叉树,它的带权路径长度值WPL=2×2+4×2+5×2+3×2=28。
在给定一组具有确定权值的叶结点,可以构造出不同的带权二叉树。例如,给出4个叶结点,设其权值分别为1,3,5,7,我们可以构造出形状不同的多个二叉树。这些形状不同的二叉树的带权路径长度将各不相同。图6.17给出了其中5个不同形状的二叉树。
这五棵树的带权路径长度分别为:
(a)WPL=1×2+3×2+5×2+7×2=32
(b)WPL=1×3+3×3+5×2+7×1=29
(c)WPL=1×2+3×3+5×3+7×1=33
(d)WPL=7×3+5×3+3×2+1×1=43 图6.16 一个带权二叉树
(e)WPL=7×1+5×2+3×3+1×3=29
(a) (b) (c)
(d) (e)
图6.17 具有相同叶子结点和不同带权路径长度的二叉树
由此可见,由相同权值的一组叶子结点所构成的二叉树有不同的形态和不同的带权路径长度,那么如何找到带权路径长度最小的二叉树(即哈夫曼树)呢?根据哈夫曼树的定义,一棵二叉树要使其WPL值最小,必须使权值越大的叶结点越靠近根结点,而权值越小的叶结点越远离根结点。哈夫曼(Haffman)依据这一特点提出了一种方法,这种方法的基本思想是:
(1)由给定的n个权值{W1,W2,…,Wn}构造n棵只有一个叶结点的二叉树,从而得到一个二叉树的集合F={T1,T2,…,Tn};
(2)在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根结点的权值为其左、右子树根结点权值之和;
(3)在集合F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合F中;
(4)重复(2)(3)两步,当F中只剩下一棵二叉树时,这棵二叉树便是所要建立的哈夫曼树。
图6.18给出了前面提到的叶结点权值集合为W={1,3,5,7}的哈夫曼树的构造过程。可以计算出其带权路径长度为29,由此可见,对于同一组给定叶结点所构造的哈夫曼树,树的形状可能不同,但带权路径长度值是相同的,一定是最小的。
|
第一步 第二步
第三步 第四步
图6.18 哈夫曼树的建立过程
2.哈夫曼树的构造算法
在构造哈夫曼树时,可以设置一个结构数组HuffNode保存哈夫曼树中各结点的信息,根据二叉树的性质可知,具有n个叶子结点的哈夫曼树共有2n-1个结点,所以数组HuffNode的大小设置为2n-1,数组元素的结构形式如下:
|
|
|
|
其中,weight域保存结点的权值,lchild和rchild域分别保存该结点的左、右孩子结点在数组HuffNode中的序号,从而建立起结点之间的关系。为了判定一个结点是否已加入到要建立的哈夫曼树中,可通过parent域的值来确定。初始时parent的值为-1,当结点加入到树中时,该结点parent的值为其双亲结点在数组HuffNode中的序号,就不会是-1了。
构造哈夫曼树时,首先将由n个字符形成的n个叶结点存放到数组HuffNode的前n个分量中,然后根据前面介绍的哈夫曼方法的基本思想,不断将两个小子树合并为一个较大的子树,每次构成的新子树的根结点顺序放到HuffNode数组中的前n个分量的后面。
下面给出哈夫曼树的构造算法。
#define MAXVALUE 10000 /*定义最大权值*/
#define MAXLEAF 30 /*定义哈夫曼树中叶子结点个数*/
#define MAXNODE MAXLEAF*2-1
typedef struct {
int weight;
int parent;
int lchild;
int rchild;
}HNodeType;
void HaffmanTree(HNodeType HuffNode [ ])
{/*哈夫曼树的构造算法*/
int i,j,m1,m2,x1,x2,n;
scanf(“%d”,&n); /*输入叶子结点个数*/
for (i=0;i<2*n-1;i++) /*数组HuffNode[ ]初始化*/
{ HuffNode[i].weight=0;
HuffNode[i].parent=-1;
HuffNode[i].lchild=-1;
HuffNode[i].rchild=-1;
}
for (i=0;i<n;i++) scanf(“%d”,&HuffNode[i].weight); /*输入n个叶子结点的权值*/
for (i=0;i<n-1;i++) /*构造哈夫曼树*/
{ m1=m2=MAXVALUE;
x1=x2=0;
for (j=0;j<n+i;j++)
{ if (HuffNode[j].weight<m1 && HuffNode[j].parent==-1)
{ m2=m1; x2=x1;
m1=HuffNode[j].weight; x1=j;
}
else if (HuffNode[j].weight<m2 && HuffNode[j].parent==-1)
{ m2=HuffNode[j].weight;
x2=j;
}
}
/*将找出的两棵子树合并为一棵子树*/
HuffNode[x1].parent=n+i; HuffNode[x2].parent=n+i;
HuffNode[n+i].weight= HuffNode[x1].weight+HuffNode[x2].weight;
HuffNode[n+i].lchild=x1; HuffNode[n+i].rchild=x2;
}
}
算法 6.25
3.哈夫曼树在编码问题中的应用
在数据通讯中,经常需要将传送的文字转换成由二进制字符0,1组成的二进制串,我们称之为编码。例如,假设要传送的电文为ABACCDA,电文中只含有A,B,C,D四种字符,若这四种字符采用表6.2 (a)所示的编码,则电文的代码为000010000100100111 000,长度为21。在传送电文时,我们总是希望传送时间尽可能短,这就要求电文代码尽可能短,显然,这种编码方案产生的电文代码不够短。表6.2 (b)所示为另一种编码方案,用此编码对上述电文进行编码所建立的代码为00010010101100,长度为14。在这种编码方案中,四种字符的编码均为两位,是一种等长编码。如果在编码时考虑字符出现的频率,让出现频率高的字符采用尽可能短的编码,出现频率低的字符采用稍长的编码,构造一种不等长编码,则电文的代码就可能更短。如当字符A,B,C,D采用表6.2 (c)所示的编码时,上述电文的代码为0110010101110,长度仅为13。
表6.2 字符的四种不同的编码方案
字符 编码 字符 编码 字符 编码 字符 编码
A 000 A 00 A 0 A 01
B 010 B 01 B 110 B 010
C 100 C 10 C 10 C 001
D 111 D 11 D 111 D 10
哈夫曼树可用于构造使电文的编码总长最短的编码方案。具体做法如下:设需要编码的字符集合为{d1,d2,…,dn},它们在电文中出现的次数或频率集合为{w1,w2,…,wn},以d1,d2,…,dn作为叶结点,w1,w2,…,wn作为它们的权值,构造一棵哈夫曼树,规定哈夫曼树中的左分支代表0,右分支代表1,则从根结点到每个叶结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,我们称之为哈夫曼编码。
在哈夫曼编码树中,树的带权路径长度的含义是各个字符的码长与其出现次数的乘积之和,也就是电文的代码总长,所以采用哈夫曼树构造的编码是一种能使电文代码总长最短的不等长编码。
在建立不等长编码时,必须使任何一个字符的编码都不是另一个字符编码的前缀,这样才能保证译码的唯一性。例如表6.2 (d)的编码方案,字符A的编码01是字符B的编码010的前缀部分,这样对于代码串0101001,既是AAC的代码,也是ABD和BDA的代码,因此,这样的编码不能保证译码的唯一性,我们称之为具有二义性的译码。
然而,采用哈夫曼树进行编码,则不会产生上述二义性问题。因为,在哈夫曼树中,每个字符结点都是叶结点,它们不可能在根结点到其它字符结点的路径上,所以一个字符的哈夫曼编码不可能是另一个字符的哈夫曼编码的前缀,从而保证了译码的非二义性。
下面讨论实现哈夫曼编码的算法。实现哈夫曼编码的算法可分为两大部分:
(1)构造哈夫曼树;
(2)在哈夫曼树上求叶结点的编码。
求哈夫曼编码,实质上就是在已建立的哈夫曼树中,从叶结点开始,沿结点的双亲链域回退到根结点,每回退一步,就走过了哈夫曼树的一个分支,从而得到一位哈夫曼码值,由于一个字符的哈夫曼编码是从根结点到相应叶结点所经过的路径上各分支所组成的0,1序列,因此先得到的分支代码为所求编码的低位码,后得到的分支代码为所求编码的高位码。我们可以设置一结构数组HuffCode用来存放各字符的哈夫曼编码信息,数组元素的结构如下:
|
|
其中,分量bit为一维数组,用来保存字符的哈夫曼编码,start表示该编码在数组bit中的开始位置。所以,对于第i个字符,它的哈夫曼编码存放在HuffCode[i].bit中的从HuffCode[i].start到n的分量上。
哈夫曼编码算法描述如下。
#define MAXBIT 10 /*定义哈夫曼编码的最大长度*/
typedef struct {
int bit[MAXBIT];
int start;
}HCodeType;
void HaffmanCode ( )
{ /*生成哈夫曼编码*/
HNodeType HuffNode[MAXNODE];
HCodeType HuffCode[MAXLEAF],cd;
int i,j, c,p;
HuffmanTree (HuffNode ); /*建立哈夫曼树*/
for (i=0;i<n;i++) /*求每个叶子结点的哈夫曼编码*/
{ cd.start=n-1; c=i;
p=HuffNode[c].parent;
while(p!=0) /*由叶结点向上直到树根*/
{ if (HuffNode[p].lchild==c) cd.bit[cd.start]=0;
else cd.bit[cd.start]=1;
cd.start--; c=p;
p=HuffNode[c].parent;
}
for (j=cd.start+1;j<n;j++) /*保存求出的每个叶结点的哈夫曼编码和编码的起始位*/
HuffCode[i].bit[j]=cd.bit[j];
HuffCode[i].start=cd.start;
}
for (i=0;i<n;i++) /*输出每个叶子结点的哈夫曼编码*/
{ for (j=HuffCode[i].start+1;j<n;j++)
printf(“%ld”,HuffCode[i].bit[j]);
printf(“\n”);
}
}
算法 6.26
3.哈夫曼树在判定问题中的应用
例如,要编制一个将百分制转换为五级分制的程序。显然,此程序很简单,只要利用条件语句便可完成。如:
if (a<60) b=”bad”;
else if (a<70) b=”pass”
else if (a<80) b=”general”
else if (a<90) b=”good”
else b=”excellent”;
这个判定过程可以图6.19 (a)所示的判定树来表示。如果上述程序需反复使用,而且每次的输入量很大,则应考虑上述程序的质量问题,即其操作所需要的时间。因为在实际中,学生的成绩在五个等级上的分布是不均匀的,假设其分布规律如下表所示:
分数 0-59 60-69 70-79 80-89 90-100
比例数 0.05 0.15 0.40 0.30 0.10
则80%以上的数据需进行三次或三次以上的比较才能得出结果。假定以5,15,40,30和10为权构造一棵有五个叶子结点的哈夫曼树,则可得到如图6.19 (b)所示的判定过程,它可使大部分的数据经过较少的比较次数得出结果。但由于每个判定框都有两次比较,将这两次比较分开,得到如图6.19 (c)所示的判定树,按此判定树可写出相应的程序。假设有10000个输入数据,若按图6.19 (a)的判定过程进行操作,则总共需进行31500次比较;而若按图6.19 (c)的判定过程进行操作,则总共仅需进行22000次比较。
图 书6(p145)
图6.19 转换五级分制的判定过程

浙公网安备 33010602011771号