霍夫曼编码是一种被广泛应用而且非常有效的数据压缩技术,根据待压缩数据的特征,一个可压缩掉20%~90%。这里考虑的数据指的是字符串序列。要理解霍夫曼编码,先要理解霍夫曼树,即最优二叉树,是一类带权路径长度最短的树。
路径是指从树中一个结点到另一个结点之间的通路,路径上的分支数目称为路径长度。
树的路径长度是从树根到每一个叶子之间的路径长度之和。结点的带权路径长度为从该结点到树根之间的路径长度与该结点权的乘积,树的带权路径长度为树中所有叶子结点的带权路径长度之和.
霍夫曼树是指所有叶子结点的二叉树中带权路径长度最小的二叉树.
当给定了n个叶子结点的权值后,构造出的最优二叉树的结点数目m就确定了,即m=2n-1,所以可用一维结构数组来存储最优二叉树
#define MAXLEAFNUM 50 /*最优二叉树中最大叶子树目*/
struct node{
char ch; /*当前结点表示的字符,对于非叶子结点,此域不用*/
int weight; /*当前结点的权值*/
int parent; /*当前结点的父结点的下标,为0时表示无父结点*/
int lchild,rchild; /*当前结点的左,右孩子结点的下标,为0时表示无孩子结点*/
}HuffmanTree[2 * MAXLEAFNUM];
typedef char *HuffmanCode[MAXLEAFNUM + 1];
/*创建最优二叉树*/
void createHTree(HuffmanTree HT, char *c, int *w, int n)
{
/*数组c[0..n-1]和w[0..n-1]存放了n个字符及其概率,构造霍夫树HT*/
int i, s1, s2;
if (n <= 1)
return;
/*根据n个权值构造n棵只有根结点的二叉树*/
for (i=1; i<=n; i++)
{
HT[i].ch = c[i-1];
HT[i].weight = w[i-1];
HT[i].parent = HT[i].lchild = HT[i].rchild = 0;
}
for (; i<2*n; ++i)
{
HT[i].parent = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
}
/*构造霍夫曼树*/
for (i=n+1; i<2*n; i++)
{
/*从HT[1..i-1]中选择parent为0且weight最小的两棵树,其序号为s1和s2*/
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)设给定的一组权值为{W1,W2,W3,……Wn},据此生成森林F={T1,T2,T3,……Tn},F 中的每棵二叉树只有一个带权为W1的根节点(i=1,2,……n)。
(2)在F中选取两棵根节点的权值最小和次小的二叉树作为左右构造一棵新的二叉树,新二叉树根节点的权值为其左、右子树根节点的权值之和。
(3)在F中删除这两棵最小和次小的二叉树,同时将新生成的二叉树并入森林中。
(4)重复(2)(3)过程直到F中只有一棵二叉树为止。
霍夫曼树的应用非常广,在不同的应用中叶子节点的权值可以作不同的解释。霍夫曼树应用于信息编码中,权值可以看成某个符号出现的频率;应用到判定过程中,权值可以看成某类数据出现的频率;应用到排序过程中,权值可以看成是已排好次序而等待合并的序列长度等。
应用霍夫曼编码
假设有一个包含100 000个字符的数据文件要压缩存储。各字符在该文件中的出现频度见表1。仅有6种不同字符出现过,字符a出现了45000次。
a b c d e f
频度(千字) 45 13 12 16 9 5
固定代码字 000 001 010 011 100 101
变长代码字 0 101 100 111 1101 1100
表1 一个字符编码问题。大小为100 000个字符的一个数据文件仅包含字符a~f,每个字符出现的频度如表中所示。如果对每个字符赋予一个三位的编码,则该文件可被编码为300000位。如果利用表中的可变长度编码,该文件可被编码为224000位。
可以用很多种方式来表示这样一个文件。采用固定长度编码,则需要三位二进制数字来表示六个字符:a=000,b=001,…,f=101。这种方法需要300 000来对整个原文件编码。