在计算机和互联网技术中,文本压缩就是一个非常重要的技术。玩电脑的人几乎都会应用压缩和解压缩软件来处理文档。因为它除了可以减少文档在磁盘上的空间外,还有重要的一点,
就是我们可以在网络上以压缩的形式传输大量数据,使得保存和传递都更加高效。
那么压缩而不出错是如何做到的呢?简单说,就是把我们要压缩的文本进行重新编码,以减少不必要的空间。尽管现在最新技术在编码上已经很好很强大,但这一切都来自于曾经的技术积累,我们今天就来介绍一下最基本的压缩编码方法-赫夫曼编码。
在介绍赫夫曼编码前,我们必须得介绍赫夫曼树,而介绍赫夫曼树,我们不得不提这样一个人,美国数学家赫夫曼(David
Huffman),
也有的翻译为哈夫曼。他在1952年发明了赫夫曼编码,为了纪念他的成就,于是就把他在编码中用到的特殊的C叉树称之为赫夫曼树,他的编码方法称为赫夫曼编码。也就是说,我们现在介绍的知识全都来自于近60年前这位伟大科学家的研究成果,而我们平时所用的压缩和解压缩技术也都是基于赫夫曼的研究之上发展而来,我们应该要记住他。
部分显示问题,请参考
哈夫曼树的相关术语
路径
- 从树中一个结点到另一个结点之间的分支构成这两个结点间的路径
路径长度
带权路径长度
树的带权路径长度(weighted path length)
定义
设有n个权值{w1,w2,…,wn},构造一棵有n个叶子结点的二叉树,每个叶子的权值为wi,则wpl最小的二叉树叫哈夫曼树。
哈夫曼树的构造过程
(1).
初始化:根据给定的n个权值{w1,w2,…,wn},构成n棵二叉树的集合F={T1,T2,…,Tn},其中每棵二叉树Ti只有一个带权为wi的根结点,其左右子树均空。
(2). 选取与合并:在 F
中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树上根结点的权值之和。
(3). 删除与加入:在 F 中删除这两棵树,同时将新得到的二叉树加入 F
中。
(4). 重复(2)和(3),直到 F
只含一棵树为止。这棵树便是所求的哈夫曼树。
例:
w={5, 29, 7, 8, 14, 23, 3, 11}的构造过程
哈夫曼编码
一般地, 设需要编码的字符集为{ d1,d2,···,dn
},各个字符在电文中出现的次数或频率集合为{
W1,W2,···,Wn},以d1,d2,···,dn作为叶子结点,以W1,W,···,Wn作为相应叶子结点的权值来构造一棵哈夫曼树。规定哈夫曼树的左分支代表0,右分支代表1,则从根结点到叶子结点所经过的路径分支组成的0和1的序列便为该结点对应字符的编码,这就是哈夫曼编码。
哈夫曼树及编码存储结构
1 2 3 4 5 6 7
| typedef struct { char data; int weight; int parent, lchild, rchild; }HTNode, * HuffmanTree; typedef char** HuffmanCode;
|
哈夫曼树建立
返回两个双亲域为0且权值最小的点的下标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| void Select(HuffmanTree HT, int n, int& s1, int& s2) { for (int i = 1; i <= n; i++) { if (HT[i].parent == 0) { s1 = i; break; } } for (int i = 1; i <= n; i++) { if (HT[i].weight < HT[s1].weight && HT[i].parent == 0) s1 = i; } for (int i = 1; i <= n; i++) { if (HT[i].parent == 0 && i != s1) { s2 = i; break; } } for (int i = 1; i <= n; i++) { if (HT[i].weight < HT[s2].weight && HT[i].parent == 0 && i != s1) s2 = i; } }
|
建树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| void CreateHuffmanTree(HuffmanTree& HT, int n) { if (n <= 1) return; int m = 2 * n - 1; HT = new HTNode[m + 1]; for (int i = 1; i <= m; i++) { HT[i].data = '\0'; HT[i].parent = 0; HT[i].lchild = 0; HT[i].rchild = 0; } cout << "输入结点信息和权值:"; for (int i = 1; i <= n; i++) { cin >> HT[i].data >> HT[i].weight; } int s1, s2; for (int i = n + 1; i <= m; i++) { Select(HT, i - 1, s1, s2); HT[s1].parent = i; HT[s2].parent = i; HT[i].lchild = s1; HT[i].rchild = s2; HT[i].weight = HT[s1].weight + HT[s2].weight; } }
|
哈夫曼编码
根据树对每个元素编码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| void CreateHuffmanCode(HuffmanTree HT, HuffmanCode& HC, int n) { HC = new char* [n + 1]; char* cd = new char[n]; cd[n - 1] = '\0'; for (int i = 1; i <= n; i++) { int start = n - 1; int c = i; int f = HT[c].parent; while (f != 0) { --start; if (HT[f].lchild == c) cd[start] = '0'; else cd[start] = '1'; c = f; f = HT[c].parent; } HC[i] = new char[n - start]; strcpy(HC[i], &cd[start]); } delete[] cd; }
|
哈夫曼解码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void DeCode(HuffmanTree HT, string a, string& b, int n) { b = ""; int q = 2 * n - 1; for (int i = 0; i < a.length(); i++) { if (a[i] == '0') { q = HT[q].lchild; } else if (a[i] == '1') { q = HT[q].rchild; } if (HT[q].lchild == 0 && HT[q].rchild == 0) { b += HT[q].data; q = 2 * n - 1; } } }
|
哈夫曼译码
1 2 3 4 5 6 7 8 9 10 11 12
| void Code(HuffmanTree HT, HuffmanCode HC, string c, string& d, int n) { d = ""; for (int i = 0; i < c.length(); i++) { for (int j = 1; j <= n; j++) { if (c[i] == HT[j].data) d.append(HC[j]); } } }
|
总结
学习哈夫曼树和哈夫曼编码有助于初步理解数据压缩原理。
部分显示问题,请参考