哈夫曼树及其应用
哈夫曼树的基本概念及特点
又称最优二叉树
- 路径——从树中一个结点到另一结点之间的分支构成这两个结点间的路径。
- 结点的路径长度——两结点间路径上的分支数。
例子:
- 树的路径路径长度——从根到每一个结点的路径长度之和。记作:TL
例子:
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树(充分条件)
- 权(weight)——将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
- 结点的带权路径长度——从根结点到该结点之间路径长度与该结点的权的乘积。
- 树的带权路径长度——树中所有叶子结点的带权路径长度之和。
W P L = ∑ k = 1 n w k l k WPL = \sum_{k=1}^n w_kl_k WPL=k=1∑nwklk
w w w:权值; l l l:结点到根的路径长度
例子:
- 哈夫曼树:最优树——带权路径长度(WPL)最短的树。
注:“带权路径长度最短”是在“度相同”的树中比较而得的结果,因此有最优二叉树、最优三叉树等等。
- 哈夫曼树:最优二叉树——带权路径长度(WPL)最短的二叉树。
- 特点:满二叉树不一定是哈夫曼树,哈夫曼树中权值越大的叶子离根越近,具有相同带权结点的哈夫曼树不唯一。
构造哈夫曼树
哈夫曼树中权值越大的叶子离根越近——贪心算法:构造哈夫曼树时首先选择权值小的叶子结点。
- 哈夫曼算法(构造哈夫曼树的方法)
- (1)根据n个给定的权值 { w 1 , w 2 , … , w n } \{w_1,w_2,…,w_n\} {w1,w2,…,wn}构成n棵二叉树的森林 F = { T 1 , T 2 , . . . , T n } F=\{T_1,T_2,...,T_n\} F={T1,T2,...,Tn},其中 T i T_i Ti只有一个带权为 w i w_i wi的根结点。构造森林全是根
- (2)在 F F F中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。选用两小造新树
- (3)在F中删除这两棵树,同时将新得到的二叉树加入森林中。删除两小添新人
- (4)重复(2)和(3),直到森林中只有一棵树为止,这棵树即为哈夫曼树。重复2、3剩单根
哈夫曼树中没有度为1的结点,包含 n n n个叶子结点的哈夫曼树中共有 2 n − 1 2n-1 2n−1个结点
例子:
哈夫曼树的算法实现
采用顺序存储结构——一维结构数组
- 结点类型定义
typedef struct{
int weight;
int parent,lch,rch;
}HTNode,*HuffmanTree;
HuffmanTree H;
- 1.初始化
H
T
[
1......2
n
−
1
]
:
l
c
h
=
r
c
h
=
p
a
r
e
n
t
=
0
HT [1......2n-1]:lch=rch=parent=0
HT[1......2n−1]:lch=rch=parent=0
- 2.输入初始 n n n个叶子结点:置 H T [ 1...... n ] HT[1......n] HT[1......n]的weight值;
- 3.进行以下
n
−
1
n-1
n−1次合并,依次产生
n
−
1
n-1
n−1个结点
H
T
[
i
]
,
i
=
n
+
1......2
n
−
1
HT[i],i=n+1......2n-1
HT[i],i=n+1......2n−1:
- a)在 H T [ 1... i − 1 ] HT[1...i-1] HT[1...i−1]中选两个未被选过(从 p a r e n t : = = 0 parent: == 0 parent:==0的结点中选)的 w e i g h t weight weight最小的两个结点 H T [ s 1 ] HT[s1] HT[s1]和 H T [ s 2 ] , s 1 、 s 2 HT[s2],s1、s2 HT[s2],s1、s2为两个最小结点下标;
- b)修改 H T [ s 1 ] HT[s1] HT[s1]和 H T [ s 2 ] HT[s2] HT[s2]的 p a r e n t parent parent值: H T [ s 1 ] . p a r e n t = i ; H T [ s 2 ] . p a r e n t = i HT[s1].parent=i;HT[s2].parent=i HT[s1].parent=i;HT[s2].parent=i。
- C)修改新产生的
H
T
[
i
]
HT[i]
HT[i]:
- H T [ i ] . w e i g h t = H T [ s 1 ] . w e i g h t + H T [ s 2 ] . w e i g h t HT[i].weight=HT[s1].weight+HT[s2].weight HT[i].weight=HT[s1].weight+HT[s2].weight;
- H T [ i ] . l c h = s 1 ; H T [ i ] . r c h = s 2 HT[i].lch=s1; HT[i].rch=s2 HT[i].lch=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个元素的lch,rch,parent置为0
HT[i].lch=0; HT[i].rch=0; HT[i].parent=0;
}
for(int i=1;i<=n;i++) cin>>HT[i].weight; //输入n个元素
// 合并产生n-1个结点——构造Huffman树
for(i=n+1;i<=m;i++){
Select(HT,i-1,s1,s2); //在HT[k](1<=k<=i-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分别作为i的左右孩子
HT[i].weight=HT[s1].weight+HT[s2].weight; //i的权值为左右孩子权值之和
}
}
哈夫曼编码
缺点——浪费空间
- 若将编码设计为长度不等的二进制编码,即让待传字符串中出次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少。
-
关键——要设计长度不等的编码,则必须使任意字符的编码都不是另一个字符的编码的前缀(前缀编码)。
-
问题——什么样的前缀码能使得电文总长最短?——哈夫曼编码
-
方法:
1、统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)。
2、利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。
3、在哈夫曼树的每个分支上标上0或1:
结点的左分支标0,右分支标1
把从根到每个叶子的路径上的标号连接起来,作为该叶子代表的字符的编码。
例子:
- 两个问题:
- 哈夫曼编码算法实现:
先建立一个哈夫曼树,从叶子到根查找,然后逆序。
void CreatHuffmanCode(HuffmanTree HT,HuffmanCode &HC,int n){
//从叶子结点到根逆序求每个字符的哈夫曼编码,存储在编码表HC中
HC=new char *[n+1]; //分配n个字符编码的头指针矢量
cd=new char [n]; //分配临时存放编码的动态数组空间
cd[n-1]='\0'; //编码结束符
for(int i=1;i<=n;++i){ //逐个字符求哈夫曼编码
start=n-1;c=i;f=HT[i].parent;
while(f!=0){ //从叶子结点开始向上回溯,直到根结点
--start; //回溯一次start向前指一个位置
if(HT[f].lchild==c) cd[start]='0'; //结点c是f的左孩子,则生成代码0
else cd[start]='1'; //结点c是f的右孩子,则生成代码1
c=f;f=HT[f].parent; //继续向上回溯
} // 求出第i个字符的编码
HC[i]=new char [n-start]; //为第i个字符串编码分配空间
strcpy(HC[i],&cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd; //释放临时空间
}//CreatHuffmanCode