哈夫曼编码思想
-
问题描述: 在远程通讯中,要将待传送的字符先编码再解码,编码过程就是将字符转换成由二进制组成的字符串。假设传送的字符为:ABACCDA,如果我们采用等长的编码方式,若每个字符的编码为 A : 00 B : 01 C : 10 D : 11 A:00\\B:01\\C:10\\D:11 A:00B:01C:10D:11则待传送字符的编码为00010010101100。这种编码方式的一个很明显的缺点就是浪费空间。如果这里要传送的字符很多,如果用ASCII码表示,每一个ASCII码占8位,那么就会很浪费存储空间。
所以,若将编码设计成长度不等的二进制编码,即让待传送字符串中出现次数较多的字符采用尽可能短的编码,则转换的二进制字符串便可能减少。
假设要传送的字符为:ABACCDA,不等长的编码为: A : 0 B : 00 C : 1 D : 01 A:0\\B:00\\C:1\\D:01 A:0B:00C:1D:01,则编码为000011010。但这种编码很容易产生重码问题,如上述的编码在解码的时候,前四位"0000"可以解为AAAA,也可以解为ABA或BB。
在设计不等长编码的时候,关键在于必须使任一字符的编码都不是另一个字符编码的前缀。——在这种编码叫前缀编码。 -
什么样的前缀码能使得电文总长最短?——哈夫曼编码
方法如下:- 统计字符集中每个字符在电文中出现的平均概率(概率越大,要求编码越短)。
- 利用哈夫曼树的特点:权值越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。
- 在哈夫曼树的每个分支上标上0或1:
- 结点的左分支标0,右分支标1
- 把从根到每个叶子结点的路径上的标号连接起来,作为该叶子代表的字符的编码。
-
例子: 要传输的字符集 D = { C , A , S , T , ; } D=\{C,A,S,T,;\} D={C,A,S,T,;},字符出现的频率 w = { 2 , 4 , 2 , 3 , 3 } w=\{2,4,2,3,3\} w={2,4,2,3,3}。
首先,构造哈夫曼树,
接着,进行01标记(左0右1),
从根结点到每个叶结点的路径的01序列,就是每个字符的哈夫曼编码,即, T : 00 ; : 01 A : 10 C : 110 S : 111 T:00\\;:01\\A:10\\C:110\\S:111 T:00;:01A:10C:110S:111 -
哈夫曼编码的性质:
- 哈夫曼编码是前缀码。
因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀。 - 哈夫曼编码是最优前缀码。
因为哈夫曼树的带权路径长度最短,故字符编码的总长度最短。
- 哈夫曼编码是前缀码。
-
例子: 设组成电文的字符集D及其概率分布W为: D = { A , B , C , D , E , F , G } W = { 0.40 , 0.30 , 0.15 , 0.05 , 0.04 , 0.03 , 0.03 } D=\{A,B,C,D,E,F,G\}\\W=\{0.40,0.30,0.15,0.05,0.04,0.03,0.03\} D={A,B,C,D,E,F,G}W={0.40,0.30,0.15,0.05,0.04,0.03,0.03}设计哈夫曼编码。
哈夫曼编码具体实现
-
例子: 以上例为例,说明哈夫曼编码的实现过程。在上一篇博客中,已经说明用一维结构数组存储每个结点,即如下图,
以找结点G的编码为例,首先找G的双亲结点,为结点8,在看结点8的左右孩子域,发现结点G所在的结点7为其左孩子,所以,结点G的最后一位是0;接着再看结点8的双亲,是结点10,结点8在结点10的左孩子域,所以倒数第二位编码是0;接着再看结点10的双亲,是结点11,结点10在结点11的左孩子域,所以倒数第三位编码是0;接着再看结点11的双亲,是结点12,结点11在结点12的左孩子域,所以倒数第四位编码是0;接着再看结点12的双亲,是结点13,结点12在结点13的右孩子域。所以倒数第五位编码是1;最后,当看到结点13的双亲结点为0时(结点13为根结点),结束;得出结点G的编码是10000。 -
具体实现:
定义一个字符串数组,用于存放每个结点的哈夫曼编码,
再用一个字符数组存放每个结点回溯时暂时得到的哈夫曼编码,数组长度实际上n-1个就够了,但在这里定义数组长度为n,最后一位存"\0"。
-
算法描述:
void CreateHuffmanCode(HuffmanTree HT, HuffmanCode &HC, int n)
{
HC = new char *[n + 1]; //分配n个字符编码的头指针矢量
cd = new char[n]; //分配临时存放编码的动态数组空间
cd[n - 1] = '\0'; //编码结束符
for (i = 1; i <= n; ++i) //逐个字符求哈夫曼编码
{
start = n - 1;
c = i;
f = HT[i].parent;
while (f != 0) //从叶子结点开始向上回溯,直至根结点
{
--start; //回溯一次start向前指一个位置
if (HT[f].lchild == c) //结点c是f的左孩子,则生成编码0
cd[start] = '0';
else //结点c是f的右孩子,则生成编码1
cd[start] = '1';
c = f;
f = HT[f].parent; //继续向上回溯
} //求出第i个字符的编码
HC[i] = new char[n - start]; //为第i个字符串编码分配空间
strcpy(HC[i], &cd[start]); //将求得的编码从临时空间cd复制到HC的当前行中
}
delete cd; //释放临时空间
}