哈夫曼树及其应用
1.由下面例子引出哈夫曼树
哈夫曼树:带权路径长度WPL最小的二叉树
学生成绩段分布占比
比较方案一
if(a < 60)
b='不及格';
else if (a < 70)
b='及格';
else if (a < 80)
b='中等';
else if (a < 90)
b='良好';
else
b='优秀';
带权路径长度 = (叶子结点对应的权值
w
k
w_k
wk×根结点到树中叶子结点的路径长度
l
k
l_k
lk)之和
W
P
L
=
∑
k
=
1
n
w
k
l
k
WPL = \sum_{k=1}^n w_kl_k
WPL=k=1∑nwklk
带权路径长度
W
P
L
=
5
×
1
+
15
×
2
+
40
×
3
+
30
×
4
+
10
×
4
=
315
WPL=5\times 1+15\times 2+40\times 3+30\times 4+10\times 4=315
WPL=5×1+15×2+40×3+30×4+10×4=315
比较方案二
if(a < 80){
if(a < 70)
if(a < 60)
b='不及格';
else
b='及格';
else
b='中等';
}
else{
if(a < 90)
b='良好';
else
b='优秀';
}
带权路径长度
W
P
L
=
5
×
3
+
15
×
3
+
40
×
2
+
30
×
2
+
10
×
2
=
220
WPL=5\times 3+15\times 3+40\times 2+30\times 2+10\times 2=220
WPL=5×3+15×3+40×2+30×2+10×2=220
方案一的WPL=315
方案二的WPL=220
相比之下方案二的效率高一些,但这并不是最优方案,即效率不是最高(WPL不是最小)
2.哈夫曼树的存储表示
一棵有
n
n
n 个叶子结点的哈夫曼树共有
2
n
−
1
2n-1
2n−1 个结点,可以存储在一个大小为
2
n
−
1
2n-1
2n−1 的一维数组中
//哈夫曼树存储在一维数组中
typedef struct{
int weight; //结点的权值
int parent,lchild,rchild; //结点的双亲,左孩子,右孩子的下标
}HTNode,*HuffmanTree;
为方便,数组0号不用,从1号开始用,数组大小为 2 n 2n 2n ,将叶子结点集中存储在前面部分 1~n 个位置,后面的位置存储其余非叶子结点
3.构造哈夫曼树
(1)按结点对应的权值大小,从小到大顺序排列成有序序列
A
5
、
E
10
、
B
15
、
D
30
、
C
40
A5、E10、B15、D30、C40
A5、E10、B15、D30、C40
(2)取当前序列中权值最小的两个结点作为新结点
N
1
N_1
N1,并且权值小的作为左孩子
(3)现在有序序列变为:
N
1
15
、
B
15
、
D
30
、
C
40
N_115、B15、D30、C40
N115、B15、D30、C40
(4)重复步骤(2)现在有序序列变为:
N
2
30
、
D
30
、
C
40
N_230、D30、C40
N230、D30、C40
(5)现在有序序列变为:
C
40
、
N
3
60
C40、N_360
C40、N360,最后这两个结点构成一个新结点(权值小的作为新结点的左孩子)
(6)这样就完成了哈夫曼树的构造
带权路径长度
W
P
L
=
40
×
1
+
30
×
2
+
15
×
3
+
10
×
4
+
5
×
4
=
205
WPL=40\times 1+30\times 2+15\times 3+10\times 4+5\times 4=205
WPL=40×1+30×2+15×3+10×4+5×4=205
方案一的WPL=315
方案二的WPL=220
此方案的WPL=205,所以此方案的效率最高
构造哈夫曼算法实现
void CreateHuffmanTree(HuffmanTree &HT, int n){ //n个叶子结点的哈夫曼树
if(n <= 1)
return;
//初始化开始
m=2*n-1; //n个叶子结点的哈夫曼树共有 2n-1 个结点
HT = new HTNode[m+1]; //下标0号不用,动态分配m+1个单元,HT[m]表示根结点
for(i=1; i<=m; ++i){ //1~m单元存储着叶子结点,将m=2n-1个结点初始化
HT[i].parent=0;
HT[i].lchild=0;
HT[i].rchild=0;
}
for(i=1; i<=n; ++i) //前n个单元存储着叶子结点,输入叶子结点的权值
cin >> HT[i].weight;
//初始化结束
//创建哈夫曼树
for(i=n+1; i<=m; ++i){ //n~m单元内存储着非叶子结点
Select(HT,i-1,s1,s2); //从当前森林中选择双亲为0且权值最小的两个树根结点s1、s2
//在HT[k](1 <= k <= i-1)中选择两个其双亲域为0且权值最小的结点,并返回他们在HT中的序号s1和s2
HT[s1].parent=i;
HT[s2].parent=i;
//得到新结点i,从森林中删除s1和s2,将s1和s2的双亲域由0改为i
HT[i].lchild = s1; //s1作为结点i的左孩子
HT[i].rchild = s2; //s2作为结点i的有孩子
//结点i的权值为左右孩子权值之和
HT[i].weight = HT[s1].weight + HT[s2].weight;
}
}
4.哈夫曼编码
假设传输内容“BADCADFEED”,按照下表编码
则原编码二进制串:001000011010000011101100100011 每个字母都占3位,事实上在数据传输时每个字母出现频率不同。
假设六个字母频率 A27、B8、C15、D15、E30、F5
左图为构造哈夫曼树过程的权值显示
右图为将权值左分支改为0,右分支改为1
由右图得编码
原编码二进制串:001000011010000011101100100011(共30个字符)
新编码二进制串:1001010010101001000111100 (共25个字符)
数据被压缩了,在解码时,还要用到哈夫曼树,即发送方和接收方必须要约定好同样的哈夫曼编码规则
5.哈夫曼编码算法
由于每个哈夫曼编码是变长编码,因此使用一个指针数组来存放每个字符编码的首地址
哈夫曼编码表的存储表示
typedef char **HuffmanCode; //动态分配数组存储哈夫曼编码表
根据哈夫曼树求哈夫曼编码
以叶子结点为出发点,向上回溯至根结点,回溯时走左分支则生成0,走右分支则生成1
根据已知的哈夫曼树求哈夫曼编码
void CreateHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n){ //n个叶子结点的哈夫曼树
//HC的下标0不使用,从1开始,数组大小n+1
HC = new char*[n+1]; //存储动态数组cd首地址
//cd的下标0使用
cd = new char[n]; //存储叶子结点对应的哈夫曼编码
cd[n-1] = '\0'; //数组cd末尾填入结束符
for(i=1; i<=n; ++i){ //从叶子向上回溯至根结点,求n个叶子结点对应的哈夫曼编码
start = n-1; //start开始指向cd数组的最后
c = i;
f = HT[i].parent; //f指向结点c的双亲结点
while(f != 0){
--start; //回溯一次start向前指一个位置
if(HT[f].lchild == c) //双亲HT[f]的左孩子为下标c处的结点
cd[start]='0'; //左支生成0
else
cd[start]='1'; //右支生成1
//继续向上回溯
c = f;
f = HT[f].parent;
}
HC[i] = new char[n-start]; //为某个叶子结点的哈夫曼编码数组cd分配空间
strcpy(HC[i], &cd[start]); //将求得的哈夫曼编码从临时空间cd复制(数组首地址)到HC的当前行中
}
delete cd; //释放临时空间
}