数据结构与算法系列研究六——哈夫曼编码与译码
哈夫曼编码与译码
一、哈夫曼编码定义
1.1、基本术语
路径: 从一结点到另一结点上的分支构成这两个结点的路径。
路径长度: 路径上的分支数目。
树的路径长度: 从根到所有结点的路径长度之和。
结点的带权路径长度: 从该结点到树根之间的路径长度与结点上权值的乘积。
树的带权路径长度: 树中所有叶子结点的带权路径长度之和。
1.2、哈夫曼树定义: 设有n 个权值 {w1,w2,......wn},试构造具有 n 个叶结点的二叉树,每个叶结点权值为 wi ,则其中带权路径长度WPL最小的二叉树称为哈夫曼树(最优二叉树)。特点:权值越大的叶子离根越近。若叶结点上的权值均相同,则完全二叉树一定是最优二叉树,否则完全二叉树不一定是最优二叉树。
WPL=2*(7+5+2+4)=36
WPL=3*(7+5)+2*4+2=46
WPL=3*(2+4)+2*5+7=35
1.3、哈夫曼树构造:
(1) 根据给定的n个权值 {w1,w2,......wn}, 生成 n 棵二叉树的集合F= {T1,T2,.......Tm};其中每棵二叉树Ti只有一个带权为Wi的根结点,左右子树为空。
(2) 在 F 中选择两棵根结点值最小的树 Ti ,Tj 作为左右子树,构成一棵新二叉树Tk , Tk根结点值为Ti ,Tj根结点权值之和;
(3) 在 F 中删除Ti ,Tj ,并把 Tk 加到 F中;
(4) 重复 (2) (3),直到 F中只含一棵树。
例:w={7,5,2,4}
1.4、哈夫曼编码
数据的压缩过程称为编码,解压过程称为解码。
编码:将文件中的字符转换为唯一的一个二进制串。
解码:将一个二进制串转换为对应的字符。
定长编码:设编码字符个数为n,码长为k,则k= 上界(log2^(n+1))。
不等长编码:使出现频率最多的字符采用尽可能短的编码。
前缀编码:对不等长编码,要求任一字符的编码都不是另一个字符的编码的前缀。
采用二叉树设计前缀编码:用二叉树的叶结点表示待编码的字符,并约定左分支表示字符‘0’,右分支表示字符‘1’,则从根结点到叶子结点的路径上分支字符组成的字符串作为该叶子结点的编码。由此得到的编码必为二进制的前缀编码。
方法:以n种字符出现的频率作权,设计一棵哈夫曼树,由此得到字符的二进制前缀编码为总长最短的二进制前缀编码,这种编码即为哈夫曼编码。
二、实验实现
2.1、实验内容
哈夫曼编码生成与译码。
输入符号(序号用英文字母A, B, C, …表示)以及各符号出现概率,以字符串形式输出各符号对应的二进制哈夫曼编码。
以字符串形式输入接收到的比特序列,输出译码后的符号序列。建议用菜单形式提供功能。
2.2、输入与输出
输入:编码的个数,编码的权重,编码的符号,接收到的比特序列。
输出:编码结果和译码结果。
2.3.关键数据结构与算法描述
关键数据结构:霍夫曼树的节点构造以及权重表,符号表,编码表,实际符号数。由于采用数组的形式来存储所有节点,则要提供左右子树和双亲节点指针。
1 typedef char ElemType;
2 typedef struct {
3 int parent; //双亲下标
4 int lchild; //左儿子下标
5 int rchild; //右儿子下标
6 double w; //结点权重
7 }HF_BTNode; //码树结点类型
8 typedef struct
9 {
10 int n; //实际符号数, n<=N
11 ElemType s[N]; //符号表
12 double weight[N]; //符号权重表
13 char code[N][N+1]; //编码表
14 HF_BTNode hf[2*N-1]; //码树
15 } HFT; //Huffman码树及码表
算法描述:
首先是哈夫曼树的生成需要根据相应的数据结构采用相应的算法。因为采用的是数组存储树的节点,属于顺序存储结构。首先根据算法应该先找到所有根节点中最小的两个组成一棵新树的左右子树,删除这两个节点(此处用parent为-1来说明为根节点,如不是-1,则为删除),添加新生成的节点即让新树的根节点的parent为-1即可。因此根据思路可以遍历要生成节点前的所有节点来不断生成新树,最后形成只有二度节点和零度节点的二叉树。具体算法如下:
int m=2*a.n-1; //所有节点数
for(int i=0; i<m; i++)//对所有节点
{
a.hf[i].parent=a.hf[i].lchild=a.hf[i].rchild=-1;//-1代表着为根节点
}
for(i=0; i<a.n; i++)
{
a.hf[i].w=a.weight[i]; //权重赋值
}
/**********************生成霍夫曼树***********************************/
int j1,j2,j;
for(i=a.n; i<m; i++) //从原有节点之后增添数值
{
for(j1=0; j1<i; j1++) //遍历带增添节点之前所有节点找到根节点
{
if(a.hf[j1].parent==-1)
{
break;
}
}
for(j=j1+1; j<i; j++)//找到最小的根节点作为新增节点的左子树
{
if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j1].w)
{
j1=j;
}
}//j1为最小节点
a.hf[j1].parent=i;
a.hf[i].lchild=j1;
for(j2=0; j2<i; j2++) //同理找到次小节点,作为新增节点右子树
{
if(a.hf[j2].parent==-1)
{
break;
}
}
for(j=j2+1; j<i; j++)
{
if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j2].w)
{
j2=j; //j2为次小节点
}
}
a.hf[j2].parent=i;
a.hf[i].rchild=j2;
a.hf[i].w=a.hf[j1].w+a.hf[j2].w;//节点生成完成,权重赋值
}
然后,就要进行编码了,对生成的霍夫曼树约定左边编1,右边编0,只需从符号表亦即最原始节点开始追溯到根,即可的反序的编码,然后将其翻转即可。具体代码如下:
/*********************生成code字符串数组*******************/
for(i=0; i<a.n; i++)
{
int j=i;
char *p=a.code[i],*q; //j从第i个叶子出发上溯至根结点,故编码表一定和字符表对应
while(a.hf[j].parent!=-1)//未到根节点时持续编号
{
int child=j;
j=a.hf[j].parent;
if(a.hf[j].lchild==child)
{
*p++='1';//左边是1
}
else
{
*p++='0';//右边是0
}
}
*p='\0';
q=a.code[i];
p--; //因是从叶子到根故要字符串逆序
char ch;
while(q<p)
{
ch=*q;
*q=*p;
*p=ch;
q++;
p--;
}
}
最后,就是译码过程了,根据二进制码的字符串开始遍历,根据霍夫曼树从根出发按照1向左走,0向右走的规则,直至不能再走,则得到了想要的节点,该节点对应与符号表中一个数,故此唯一确定。如此循环往复,直至编码被译完。具体代码如下:
while(receive[i]!='\0')
{
k=2*a.n-2; //k指向根结点,注意是从0开始
while(k>=a.n&&(receive[i]!='\0'))
{
if(receive[i++]=='1')
{
k=a.hf[k].lchild; //向左走
}
else
{
k=a.hf[k].rchild; //向右走
}
}
if(k<a.n)
{
decoded[j++]=a.s[k]; //根据对应关系输出一个符号
}
}
decoded[j]='\0';
2.4、测试与理论
理论:按理说输入相应的编码符号表,编码数,编码权重就会生成相应的霍夫曼树,例如输入编码符号表为“abcde”,编码数自然为5,编码权重分别为0.1,0.2,0.3,0.3,0.1则应该生成相应的形如下面的霍夫曼树:
则输入序列”1110110001010001”对应与“baeccdc”,此处要注意权重相同时的编码处理方法,谁先被发现,谁就是左子树。
测试结果为:
选择1之后:
选择1后,再选择2,输入数据得到:
可见结果是正确的。
2.5、附录(源代码)
1 #include "stdio.h"
2 #include "stdlib.h"
3 #include "iostream"
4 using namespace std;
5 #define N 300 //最大允许符号数
6 typedef char ElemType;
7 typedef struct {
8 int parent; //双亲下标
9 int lchild; //左儿子下标
10 int rchild; //右儿子下标
11 double w; //结点权重
12 }HF_BTNode; //码树结点类型
13 typedef struct
14 {
15 int n; //实际符号数, n<=N
16 ElemType s[N]; //符号表
17 double weight[N]; //符号权重表
18 char code[N][N+1]; //编码表
19 HF_BTNode hf[2*N-1]; //码树
20 } HFT; //Huffman码树及码表
21
22 void createHF(HFT &a)
23 {
24 int m=2*a.n-1; //所有节点数
25 for(int i=0; i<m; i++)//对所有节点
26 {
27 a.hf[i].parent=a.hf[i].lchild=a.hf[i].rchild=-1;//-1代表着为根节点
28 }
29 for(i=0; i<a.n; i++)
30 {
31 a.hf[i].w=a.weight[i]; //权重赋值
32 }
33 /**********************生成霍夫曼树***********************************/
34 int j1,j2,j;
35 for(i=a.n; i<m; i++) //从原有节点之后增添数值
36 {
37 for(j1=0; j1<i; j1++) //遍历带增添节点之前所有节点找到根节点
38 {
39 if(a.hf[j1].parent==-1)
40 {
41 break;
42 }
43 }
44 for(j=j1+1; j<i; j++)//找到最小的根节点作为新增节点的左子树
45 {
46 if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j1].w)
47 {
48 j1=j;
49 }
50 }//j1为最小节点
51 a.hf[j1].parent=i;
52 a.hf[i].lchild=j1;
53 for(j2=0; j2<i; j2++) //同理找到次小节点,作为新增节点右子树
54 {
55 if(a.hf[j2].parent==-1)
56 {
57 break;
58 }
59 }
60 for(j=j2+1; j<i; j++)
61 {
62 if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j2].w)
63 {
64 j2=j; //j2为次小节点
65 }
66 }
67 a.hf[j2].parent=i;
68 a.hf[i].rchild=j2;
69 a.hf[i].w=a.hf[j1].w+a.hf[j2].w;//节点生成完成,权重赋值
70 }
71 /***********************树生成完毕*********************/
72 /*********************生成code字符串数组*******************/
73 for(i=0; i<a.n; i++)
74 {
75 int j=i;
76 char *p=a.code[i],*q; //j从第i个叶子出发上溯至根结点,故编码表一定和字符表对应
77 while(a.hf[j].parent!=-1)//未到根节点时持续编号
78 {
79 int child=j;
80 j=a.hf[j].parent;
81 if(a.hf[j].lchild==child)
82 {
83 *p++='1';//左边是1
84 }
85 else
86 {
87 *p++='0';//右边是0
88 }
89 }
90 *p='\0';
91 q=a.code[i];
92 p--; //因是从叶子到根故要字符串逆序
93 char ch;
94 while(q<p)
95 {
96 ch=*q;
97 *q=*p;
98 *p=ch;
99 q++;
100 p--;
101 }
102
103 }
104 //根据对应关系输出编码对应表
105 cout<<"the code corresponding's string:"<<endl;
106 for(i=0;i<a.n;i++)
107 {
108 cout<<a.s[i]<<":"<<a.code[i]<<endl;
109 }
110 }
111 /**************译码算法********************/
112 char * dec(HFT &a, char receive[])
113 {
114 int i,j,k;
115 i=j=0; //一定要初始化为0
116 char decoded[N];
117
118 while(receive[i]!='\0')
119 {
120 k=2*a.n-2; //k指向根结点,注意是从0开始
121 while(k>=a.n&&(receive[i]!='\0'))
122 {
123 if(receive[i++]=='1')
124 {
125 k=a.hf[k].lchild; //向左走
126 }
127 else
128 {
129 k=a.hf[k].rchild; //向右走
130 }
131 }
132 if(k<a.n)
133 {
134 decoded[j++]=a.s[k]; //根据对应关系输出一个符号
135 }
136 }
137 decoded[j]='\0';
138 return decoded;
139 }
140 void MainMenu( )
141 {
142 HFT tree;
143 int i;
144 char choice,ch;
145 int s=0;
146 char InputCode[N],decode[N];
147 start:
148 system("cls");
149 cout<<"-------------------welcome----------------------"<<endl;
150 cout<<" 1.编码 "<<endl;
151 cout<<" 2.译码 "<<endl;
152 cout<<" 3.退出 "<<endl;
153 cout<<"-------------------end--------------------------"<<endl;
154 cin>>choice;
155 switch(choice)
156 {
157 case '1':
158 system("cls");
159 cout<<"plese input the code num:"<<endl;
160 cin>>tree.n;
161 cout<<"please input the code weight:"<<endl;
162 for(i=0;i<tree.n;i++)
163 {
164 cin>>tree.weight[i];
165
166 }
167 for(i=0;i<tree.n;i++)
168 {
169 s+=tree.weight[i];
170 }
171 b: cout<<"please input the needed string:"<<endl;
172 cin>>tree.s;
173 if(strlen(tree.s)!=tree.n)
174 {
175 goto b;
176 }
177 createHF(tree);
178 cout<<"|--------------------1.返回--------------------|"<<endl;
179 cout<<"|--------------------2.退出--------------------|"<<endl;
180 cin>>ch;
181 if(ch=='1')
182 goto start;
183 else
184 exit(0);
185
186 case '2':
187 system("cls");
188 cout<<"input the code:"<<endl;
189 cin>>InputCode;
190 strcpy(decode,dec(tree,InputCode));
191 cout<<decode<<endl;
192 cout<<"|--------------------1.返回--------------------|"<<endl;
193 cout<<"|--------------------2.退出--------------------|"<<endl;
194 cin>>ch;
195 if(ch=='1')
196 goto start;
197 else
198 exit(0);
199 case '3': exit(0);
200 default:
201 cout<<"error";
202 goto start;
203 }
204
205 }
206 int main()
207 {
208 MainMenu( );
209 return 0;
210 }
关于本系列已经是第六个了,看了一下有回馈的却很少,可能是刚刚开始写博客吧,很多地方还是有点生疏的,再加上临近毕业答辩,抽出一点时间学点东西也是忙里偷闲的,写的有不好之处请各位多多担待,毕竟营造良好的网络学习环境和氛围是我们新一代人应该做的事情,这个时代讲究的是信息的交流和共享,在这一点我一直在努力着,希望大家能错我的文章中受到启发,这样也是非常好的一件事了,独乐乐不如众乐乐,就是这个道理了!