树与二叉树
- 本节大纲内容
- 1 树的有关概念
- 树
- 树的基本术语
- 2 二叉树的定义与性质
- 二叉树
- 二叉树的性质
- 特殊二叉树
- 3 二叉树的存储结构
- 1、顺序存储结构
- 2、链式存储结构
- 4 二叉树的遍历
- 遍历
- 表达值二叉树
- 先序序列创建二叉树
- 由先序序列和中序序列得出二叉树和由后序序列和中序序列得出二叉树
- 线索二叉树:
- 二叉树的线索化:
- 中序线索化算法(不带头结点)
- 中序线索化算法(带头结点)
- 5二叉树遍历的应用
- 复制二叉树
- 二叉树的深度
- 二叉树结点个数
- 6 树的存储结构
- 双亲表示法(数组表示法、顺序表示法)
- 孩子表示法(链接表表示法)
- 孩子兄弟法(二叉树表示法、二叉链表)
- 孩子链表示法
- 带双亲的孩子链表示法
- 7 树与二叉树的相互转换
- 森林转化为二叉树
- 二叉树转化为树
- 二叉树转化为森林
- 8 树与森林的遍历
- 树的遍历
- 森林的遍历
- 9 哈夫曼树
- 基本概念:
- 哈弗曼树
- 10 哈夫曼算法
- 哈弗曼树的构造过程
- 哈弗曼树举例
- 哈弗曼树的实现
- 构造哈夫曼树算法
- 哈夫曼码
- 哈夫曼编码
- 哈夫曼解码:
本节大纲内容
1 树的有关概念
树
树的特性就是分叉或分支。既然这种数据结构被称为树,那么数据元素之间的逻辑关系就应该有树的特性。树的分叉点被称为结点。到此可以分析到树的结点。而把这些结点之间的关系在以图示表示,就可以得到树形示意图。 而这便是“树”名字的由来。
树是n(n>=0)个结点的有限集,它或为空树(n=0);或为非空树,对于非空树T:
(1)有且仅有一个称之为根的结点;
(2)除根节点以外的其余结点可分为m(m>0)个互不相交的有限集T1,T2,Tm,其中每一个集合本身又是一棵树,并且称为根的子树。
树的基本术语
例如一棵树,用来探讨树的基本术语。
(1)结点
树中的一个独立单元。包含一个数据元素及若干指向其子树的分支。如图中的A、B、C、D。
(2)结点的度
结点拥有的子树数称为结点的度。例如,A的度为3,C的度为1,F的度为0。
(3)树的度
树的度是树内各结点度的最大值。上图所示的树的度为3.
(4)叶子
度为0的结点称为叶子或终端结点。结点K,L,F,G,M,I,J都是树的叶子。
(5)非终端端点
度不为0的结点称为非终端结点或分支结点。
(6)双亲和孩子
结点的子树的根称为该结点的孩子,相应地,该结点称为孩子的双亲。例如,B的双亲为A,B的孩子有E和F。
(7)兄弟
同一个双亲的孩子之间互称兄弟。例如,H,I和J互为兄弟。
(8)祖先
从根到该结点所经分支上的所有结点。例如,M的祖先为A,D,H。
(9)子孙
以某结点为根的子树中的任一结点都称为该结点的子孙。如B的子孙为E,K,L和F。
(10)层次
结点的层次从根开始定义,根为第一层,根的孩子为第二层。树中任一结点的层次等于其双亲结点的层次加1。
(11)堂兄弟
双亲在同一层的结点互为堂兄弟。例如,结点G与E、F、H、I、J互为堂兄弟。
(12)树的深度
树中结点的最大层次称为树的深度或高度。例如上图所示的树的深度为4。
(13)有序树和无序树
如果将树中结点的各子树看成是从左至右是有次序的(即不能互换)则称该树为有序树,否则称为无序树。在有序树中最左边的子树的根称为第一个孩子,最右边的子树为最后一个孩子。
(14)森林
是m(m>=0)棵互不相交的树的集合。
2 二叉树的定义与性质
二叉树
二叉树是n个结点所构成的集合,它或为空树或为非空树,对于非空树T:
(1)有且仅有一个称之为根的结点。
(2)除根结点外的其余结点分为两个互不相交的子集T1和T2,分别称为树T的左子树和右子树,且T1和T2本身又都是二叉树。
二叉树和树一样具有递归的性质,二叉树与树的区别主要有以下两点:
(1)二叉树每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点)。
(2)二叉树的子树有左右之分,其次序不能颠倒。
二叉树有5种基本形态。
(1)空树
(2)仅有根结点的二叉树
(3)右子树为空的二叉树
(4)左右子树都非空的二叉树
(5)左子树为空的二叉树
二叉树的性质
性质1
在二叉树的第i层上至多有
性质2
深度为k的二叉树至多有
推导:
性质3
对任何一棵二叉树T,如果其终端结点数为 ,度为2的结点数为 ,则
推理过程
度为1的结点会射出一个分支指向一个孩子结点,度为2的结点会射出两个分支指向它的两个孩子结点。
除了根节点外,每个结点均对应一个分支指向它。那么结点数
换个角度,二叉树的结点度树为0,1,2。那么结点数
上述两个角度分析得出的总结点数n相同。那么即可得
特殊二叉树
满二叉树:深度为k且含有
满二叉树的特点:每一层上的结点数都是最大结点数。可以对满二叉树进行连续编号,约定编号从根结点起,自上而下,自左而有。由此可引出完全二叉树的定义。
完全二叉树:深度为k的,有n个结点的二叉树,当且仅当其每一个结点都与深度为k的满二叉树中的编号从1至n的结点一一对应时,称之为完全二叉树。
完全二叉树的特点:
(1)叶子结点只可能在层次最大的两层上出现;
(2)对任一结点,若其右分支下的子孙的最大层次为 l,则其左分支下的子孙最大层次必为l或l+1。
性质4
具有n个结点的完全二叉树的深度为不大于
推导过程:
假设深度为k,则k-1层的结点必须装满,则k-1层共有 个结点。第k层可不必装满。
则结点数n的范围为,k是整数,解出k即可。
性质5
如果对一棵有n个结点的完全二叉树的结点按层序编号,每层自左而右,则队任一结点i,有
(1)若i==1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点i/2取整。
(2)若2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i;
(3)若2i+1>n,则结点i无右孩子;否则其右孩子是结点2i+1。
3 二叉树的存储结构
1、顺序存储结构
//- - - - -二叉树的顺序存储表示- - - - -//
#define MaxSize 100 //二叉树的最大结点数
typedef TElemType SqBiTree[MaxSize]; //0号单元存储根结点
SqBiTree bt;
必要说明:
顺序存储结构使用一组地址连续的存储单元来存储数据元素,为了能够在存储结构中反映出结点之间的逻辑关系,必须将二叉树的结点按照一定的规律安排在这组单元中。
对于完全二叉树,只要从根起按层序存储即可,依次自上而下、自左而右存储结点元素,即将完全二叉树上编号为i的结点元素存储在如上定义的一维数组下标为i-1的分量中。例如:
完全二叉树如图:
顺序存储结构图:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
对于一般二叉树,则应将其每个结点与完全二叉树上的结点相对照,存储在一维数组的相应分量重,以0代表不存在此结点。
非完全二叉树如图:
顺序存储结构图:
1 | 2 | 3 | 4 | 5 | 0 | 0 | 0 | 0 | 6 | 7 |
由此可见,这种顺序存储结构仅适用于完全二叉树。若有一个右单只树,用顺序存储时将会有大量的空间被浪费。所以对于一般二叉树,更适合采用链式存储结构。
2、链式存储结构
(1)二叉链表
结点结构:
lchild | data | rchild |
结点定义:
//- - - -二叉树的二叉链表存储表示- - - - //
typedef struct BiTNode{
TElemType data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;
说明:我们知道二叉链表会产生很多的空指针域,那么有多少个空指针域呢?
分析:二叉树中有三种结点。的结点有两棵子树,因此不产生空指针。那么只有
产生空指针。
空指针域=
我们知道在二叉树中:
那么,空指针域=
得出空指针域为:个,其中为二叉树的结点总数。
(2)三叉链表
结点结构:
lchild | data | parent | rchild |
结点定义:
//- - - -二叉树的二叉链表存储表示- - - - //
typedef struct BiTNode{
TElemType data;
struct BiTNode *parent,*lchild,*rchild;
}BiTNode,*BiTree;
4 二叉树的遍历
遍历
遍历二叉树:是指按某条搜索路径寻访树中的每个结点,使每个结点均被访问一次,而且仅被访问一次。
访问的含义:可以对结点做各种处理,包括输出结点的信息,对结点进行运算和修改。
遍历二叉树是二叉树最基本的操作,也是二叉树其他各种操作的基础,遍历的实质是对二叉树进行线性化的过程,即遍历的结果是将非线性结构的树中结点排成一个线性序列。
先序遍历:
若二叉树为空,则操作结束,否则
(1)访问根结点
(2)先序遍历左子树
(3)先序遍历右子树
中序遍历:
若二叉树为空,则操作结束,否则
(1)中序遍历左子树
(2)访问根结点
(3)中序遍历右子树
后序遍历:
若二叉树为空,则操作结束,否则
(1)后序遍历左子树
(2)后序遍历右子树
(3)访问根结点
中序遍历递归算法
void InOrderTraverse(BiTree T){
if(T){
InOrderTraverse(T->lchild);
cout<<T->data;
InOrderTraverse(T->rchild);
}
}
中序非遍历递归算法
算法步骤:
1、初始化一个空栈S,指针p指向根结点。
2、申请一个结点空间q,用来存放栈顶弹出的元素。
3、当p非空时或者站S非空时,循环执行以下操作:
如果p非空,则将p进栈,p指向该节点的左孩子。
算法描述:
void InOrderTraverse(Bitree T){
InitStack(S);
p=T;
q=new BiTNode;
while(p||IStackEmpty(S)){
if(p){
Push(S,p);
p=p->lchild;
}
else{
Pop(S,q);
cout<<q->data;
p=q->rchild;
}
}
}
对于算法的理解,一定要画出示意图,按步骤进行演示一遍。以此来掌握其中的精髓。
表达值二叉树
表达式二叉树T = (第一操作数) (运算符) (第二操作数)
其中:第一操作数、第二操作数也是表达式二叉树,分别为表达式二叉树T的左子树和右子树。
表达式二叉树图:
上图所示表达式为
若先序遍历此二叉树,按访问结点的先后次序将结点排列起来,可得到二叉树的先序序列为:-+a*b-cd/ef
若先序遍历此二叉树,按访问结点的先后次序将结点排列起来,可得到二叉树的先序序列为:a+b*c-d-e/f
若先序遍历此二叉树,按访问结点的先后次序将结点排列起来,可得到二叉树的先序序列为:abcd-*+ef/-
从表达式来看以上三个序列为前缀表示(波兰式)、中缀表示和后缀表示(逆波兰式)。
先序序列创建二叉树
如果仅有先序序列或后序序列不能唯一确定一棵二叉树。
二叉树T1:
T1先序序列为:ADEFGBC
二叉树T2:
T2先序序列为:ADEFGBC
由此可知二叉树的先序序列不能唯一确定一个二叉树,那么如何改进呢?
把T1的子树补全:
带空叉的T1先序序列为:(空用#代替)
AD#EF##G##B#C##
把T2的子树补全:
带空叉的T2先序序列为:(空用#代替)
ADEF##G###BC###
补全后发现先序序列就可以唯一确定一棵二叉树了。
用先序序列创建二叉树:
算法步骤:
1、扫描字符序列,读入字符ch
2、如果ch是一个“#”字符,则表明该二叉树为空树,即T为NULL;否则执行以下操作:
申请一个结点空间T;
将ch赋给T->data;
递归创建T的左子树;
算法描述:
void CreateBiTree(BiTree &T){
cin>>ch;
if(ch=='#') T=NULL;
else{
T=new BiTNode;
T->data=ch;
CreateBiTree(T->lchild);
CreateBitree(T->rchild);
}
}
画出算法执行步骤示意图,体会该算法的思想。
由先序序列和中序序列得出二叉树和由后序序列和中序序列得出二叉树
1、由先序和后序确定每一棵子二叉树的根结点
2、由中序序列确定子二叉树的左右子树
3、循环上述步骤得出所有子树的左右子树
线索二叉树:
线索二叉树的引出:
遍历二叉树是以一定规则将二叉树中的结点排列成一个线性序列,得到二叉树中结点的先序、中序和后序序列。
这实质上是对一个非线性结构进行线性化的操作,使每个结点(除第一个和最后一个结点)在这些线性序列中有且仅有一个直接前驱和直接后继。
这样就可以建立非线性结构和线性序列一一对应,可以由非线性结构确定一个线性序列,也可以由一个线性序列确定一个非线性结构。
那么,问题来了,这个线性序列在此仅仅是一个序列而已,并不是真正的线性序列,现在它还不具有线性结构的特性。即不能体现线性结构的前驱和后继关系。
那么,如何让它具有前驱和后继的特性呢?于是就引入了线索二叉树。
再想,既然要体现前驱和后继关系,那是不是应该添加两个指针域用来保存前驱和后继的信息呢?我们考虑到二叉树本身就会有很多空闲域,例如子树的左孩子或右孩子为空。如果再添加两个指针域,那么将会有更多的空间浪费,因此想到,可以充分利用二叉树本身的空闲域即可来存放前驱和后继的信息。
试做如下规定:
若结点有左子树,则其lchild域指向其左孩子,否则令lchild指向该结点的前驱;
若结点有右子树,则其rchild域指向其右孩子,否则令rchild指向该结点的后继。
为了避免混淆还要做出两个标志域。
结点形式如图:
lchild | LTag | data | RTag | rchild |
其中,
LTag为0时,lchild指向结点的左孩子
LTag为1时,lchild指向结点的前驱
同理:
RTag为0时,rchild指向结点的右孩子
RTag为1时,rchild指向结点的后继
二叉树的二叉线索类型定义:
typedef struct BiThrNode{
TElemtype data;
struct BiThrNode *lchild,*rchild;
int LTag,RTag;
}BiThrNode,*BiThrTree;
以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表,其中指向结点前驱和后继的指针,叫做线索。加上线索的二叉树称之为线索二叉树。对二叉树以某种次序遍历使其成为线索二叉树的过程叫做二叉树的线索化。
解释:
把非线性结构转化为线性序列时,线性序列还不是线性结构。于是引入线索化,把线性序列转为线性结构。要明确,在没有线索化时,只有在遍历时的那一个短暂的时间内才有前驱和后继的体现。但是,一旦生成了线性序列,这个前驱和后继的关系就消失了。而二叉树的线索化,它所做的就是不让这个前驱和后继的关系消失。
线索就是指能保存前驱和后继关系的那个指针的指向。通过这个线索我们可以得出前驱和后继的关系。
如何线索化的呢?
它是在遍历过程中,每遍历一个结点,就把线索给填上,这样得出线性序列的同时也生成了线索。这就是线索化,换句话说,就是把前驱和后继记录下来。
那么,问题来了,我应该怎样遍历呢?
这和遍历的方式无关。
如果先序遍历,则得出先序线索二叉树。
如果中序遍历,则得出中序线索二叉树。
如果后序遍历,则得出后序线索二叉树。
二叉树的线索化:
如何快速画出线索化的二叉树表呢?
例如一棵二叉树,对其中序线索化:
第一步:
写出中序序列:BDCEAFG
第二步:
根据二叉树图把中序序列线索化:
第三步:
根据第二步填入到二叉链表中:
填法:
(1)依次找到每个结点
(2)找到从该结点出发的箭头(即线索)填入二叉链表中
如果不熟练,请务必按照此法,避免出错。这可以快速又准确的画出线索二叉表。
如果熟练的同学,可以直接填入二叉树链表。
如果带头结点呢?
带头结点是这样规定的:
(1)令头结点的lchild指向二叉树的根结点
(2)令头结点的rchild指向遍历后的最后一个结点
(3)令遍历的第一个结点的lchild指向头结点
(4)令遍历的最后一个结点的rchild指向头结点
只需要把这四个指向填入即可。
中序线索化算法(不带头结点)
算法步骤:
1、如果p非空,左子树递归线索化
2、如果p的左孩子为空,则给p加上左线索,将LTag置为1,将p的左孩子指针指向pre(前驱)否则,将p的LTag置为0
3、如果pre的右孩子为空,则给pre加上右线索,将RTag置为1,让pre的右孩子指针指向p(后继);否则,将pre的RTag置为0
4、将pre指向刚访问过的结点p,即pre=p
5、右子树递归线索化
算法描述:
void InThreading(BiThrTree p){
//pre是一个全局变量,初始化时其右孩子为空,便于在树的最左边开始建线索
if(p){
InThreading(p->lchild);
if(!p->lchild){
p->LTag=1;
p->lchild=pre;
}
else p->LTag=0;
if(!pre->rchild){
pre->RTag=1;
pre->rchild=p;
}
else p->RTag=0;
pre=p;
InThreading(p->rchild);
}
}
中序线索化算法(带头结点)
void InOrderThreading(BiThrTree &Thrt,BiThrTree T){
Thrt=new BiThrNode;
Thrt->LTag=0; //头结点有左孩子,若树非空则指向树根
Thrt->RTag=1; //头结点的右孩子指针为线索
Thrt->rchild=Thrt; //初始化时指向自己
if(!T){
Thrt->lchild=T; //头结点的左孩子指向树根,第一条线
pre=Thrt; //pre初始指向头结点,第二条线,第一个序列指向前驱指向头结点
InThreading(T); //调用不带头结点的中序线索化
pre->rchild=Thrt; //上个算法结束后,pre指向最右结点,pre的右线索指向头结点,第三条线,最后一个序列后继指向头结点
pre->RTag=1;
Thrt->rchild=pre; //头结点的右线索指向pre,第四条线,头结点的右线索指向最后一个序列
}
}
5二叉树遍历的应用
复制二叉树
算法步骤:
如果是空树,递归结束,否则执行如下:
(1)申请一个新结点空间,复制根结点
(2)递归复制左子树
(3)递归复制右子树
算法描述:
void Copy(BiTree T,BiTree &NewT){
if(T==NULL){
NewT=NULL;
return;
}
else {
NewT=new BiTreeNode;
NewT->data=T->data;
Copy(T->lchild,NewT->lchild);
Copy(T->rchild,NewT->rchild);
}
二叉树的深度
二叉树的深度为树中结点的最大层次,二叉树的深度为左右子树的深度的较大者加1。
算法步骤:
如果是空树,递归结束,深度为零,否则执行如下:
(1)递归计算左子树的深度记为m
(2)递归计算右子树的深度记为n
(3)如果m>n,深度为m+1,否则为n+1
算法描述:
int Depth(BiTree T){
if(T==NULL) return 0;
else {
m=Depth(T->lchild);
n=Depth(T->rchild);
if(m>n) return (m+1);
else return (n+1);
}
}
二叉树结点个数
算法描述:
int NodeCount(BiTree T){
if(T=NULL) return 0;
else
return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
6 树的存储结构
双亲表示法(数组表示法、顺序表示法)
这种表示法中,以一组连续的存储单元存储树的结点,每个结点除了数据域data外,还附设一个parent域用以指示其双亲结点的位置。
结点定义:
例如一棵树:
表示如下:
这种存储结构利用了每个结点(除根结点外)只有唯一的双亲的性质。在这种存储结构下,求结点的双亲十分方便,也很容易求树的根,但求结点的孩子时需要遍历整个结构。
孩子表示法(链接表表示法)
(1)固定大小的结点格式。
这种格式中,多重链表的结点都是同构的,大小相同。假设树的度为d,那么每个结点都要留出d个指针域用来指向其所有孩子。我们知道树的度是所以结点中的最大度数,那么将会有很多结点的指针域为空,从而浪费大量空间。
结点格式图为:
例如一棵树
表示为:
由图可知B、D、E、F、G中有大量空间浪费。
(2)非固定大小的结点格式
为了不浪费太多空间,就引入了这种结构,不过要给出每个结点的度。此时节约了空间,但是操作不方便。
结点格式图:
例如一棵树:
表示为:
孩子兄弟法(二叉树表示法、二叉链表)
即以二叉链表做树的存储结构。链表中结点的两个链域分别指向该结点的第一个孩子和下一个兄弟结点。
格式图:
例如一棵树:
表示为:
孩子链表示法
结点格式:
例如一棵树
表示为:
带双亲的孩子链表示法
结点格式:
例如一棵树
表示为:
7 树与二叉树的相互转换
对于每个二叉树根而言
(1)每个根为二叉根
(2)第一个孩子为此根的左孩子
(3)第一个兄弟结点为此根的右孩子
例如一棵树:
转化为二叉树:
森林转化为二叉树
(1)先把每棵树转化为二叉树
(2)每棵树之间为兄弟关系
其他转化规则不变。
例如一个森林:
转化为:
二叉树转化为树
对每一个结点利用逆规则即可
(1)左孩子为下一层
(2)右孩子为同一层
二叉树转化为森林
(1)左孩子为下一层
(2)右孩子为同一层,当和根为同一层时,为新的一棵树。
8 树与森林的遍历
树的遍历
类比二叉树的遍历,二叉树有先根、中根、后根遍历。而树是不是也是如此呢?
分析:
二叉树最多有两个子树,可以分得清楚根是否在中间。但普通的树就不一样了,它可以有好多子树,因此无法分清根是否在中间。所以树只有先根和后根遍历。
同层之间按从左到右遍历。
例如一棵树:
森林的遍历
依次对各树进行遍历
例如一个森林
9 哈夫曼树
哈夫曼树又称最优树,是一类带权路径长度最短的树,在实际应用中有广泛的用处。
基本概念:
路径: 从树中一个结点到另一个结点之间的分支构成这两个结点之间的路径。
路径长度: 路径上的分支数目。
树的路径长度: 从树根到每一结点的路径长度之和。
权: 赋予某个实体的一个量,是对实体的某个或某些属性的数值化描述。在数据结构中,实体有结点和边两大关系,所以应对应有结点权和边权。结点权或边权具体代表的含义应视情况而定。如果在一棵树的结点上有权值,则对应就有带权树的概念。
结点的带权路径长度: 从该结点到树根之间的路径长度与结点上的权值的乘积。
树的带权路径长度: 树中所有叶子结点的带权路径长度之和,通常记作
哈弗曼树
哈弗曼树: 在具有n个相同权值叶子结点的所有二叉树中,WPL最小的二叉树。
例如:
10 哈夫曼算法
哈弗曼树的构造过程
(1)根据给定的n个权值,构造n棵只有根结点的二叉树,这棵二叉树构成一个森林F。
(2)在森林中选取两棵根结点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左右子树结点权值之和。
(3)在森林F中删除选中的两棵树,同时将得到的新树加入到森林F中。
(4)重复(2)和(3)直到F中只含一棵树为止。这棵树便是哈弗曼树。
哈弗曼树举例
给出一个森林:
哈弗曼树的实现
哈夫曼树是一种二叉树,当然可以用前面介绍的通用存储方法,而由于哈弗曼树没有度为1的结点,则一棵有n个叶子结点的哈弗曼树共有2n-1个结点,可以存储在一个大小为2n-1的一维数组中。树中每个结点还要包含其双亲信息和孩子结点的信息。
存储结构格式图:
weight | parent | lchild | rchild |
存储表示:
typedef struct{
int weifht;
int parent,lchild,rchild;
}HTNode,*HuffmanTree;
哈弗曼树各结点存储在由HuffmanTree定义的动态分配的数组中,为了实现方便,数组的0号单元不存储使用,所以数组的大小应为2n。将叶子结点集中存在前面1到n个位置,而后面n-1个位置存储其余非叶子结点。
构造哈夫曼树算法
算法步骤:
1、初始化:
首先动态申请2n个单元;
然后循环2n-1次,从1号单元开始,依次将1到2n-1所有单元中的双亲、左孩子、右孩子的下标初始化为0;
最后再循环n次,输入前n个单元中叶子结点的权值。
2、创建树:
循环n-1次,通过n-1次的选择、删除与合并来创建哈弗曼树。
选择是从当前森林中选择双亲为0且权值最小的两个树根结点s1和s2;
删除是指将两个结点s1和s2的双亲改为非0;
合并是将s1和s2的权值和作为一个新结点的权值依次存入到数组的第n+1之后的单元中,同时记录这个新结点左孩子的下标为s1,右孩子的下标为s2。
算法描述
void CreateHuffmanTree(HuffmanTree &HT,int n){
if(n<=1) return;
m=2*n-1;
HT=new HTNode[m+1];
for(i=1;i<=m;i++){ //初始化数组中所有单元
HT[i].parent=0;
HT[i].lchild=0;
HT[i].rchild=0;
}
for(i=1;i<=n;++i){
cin>>HT[i].weight;
}
for(i=n+1;i<=m;++i){
//创建树
Select(HT,i-1,s1,s2); //在HT[k](1<=k<=i-1)中选取其双亲为0且权值最小的两个结点,并返回他们的序号s1和s2
HT[s1].parent=i;
HT[s2].parent=i; //得到新结点i,从森林中删除s1和s2,将两者双亲改为i
HT[i].lchild=s1;
HT[i].rchild=s2; //s1和s2作为结点i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight; //i的权值等于s1和s2之和
}
}
哈夫曼码
哈夫曼编码
例如:
给定一个由18个字符组成的文本
哈夫曼解码:
从上述例题进行解码:
编码为:0100110101111111110111010101001100
解码为:after red data
哈夫曼树:
根据哈弗曼树读取编码,读到叶子结点为文本。
即可解出文本内容。