赫夫曼编码及其相关延伸
首先声明:本人菜鸟一个而已。。。。。。。。。
这次和大家一起讨论和赫夫曼编码有关的一些知识。
大家都知道赫夫曼编码是利用贪心性质构造一种最优树(不是二叉查找树)从而找出最优编码的一种算法。既然和贪心有关我就简要介绍一下贪心算法些性质。
贪心其实没有什么真正的算法,只是一种类似动态规划的思想。动态规划是根据子问题最优解来求得最优解的,但是贪心却是通过局部(贪心)最优解来求得最优解的,也就是说我们每次的选择都是因为自己贪心想“多得”的结果。
贪心算法是每次做一个当时(看起来是)最优的选择,所以这种策略并不是每次都可以产生最优解的,要判断是不是最优解必须通过一系列的证明才行。
贪心算法的两个最重要的性质:(只有符合这两个性质的问题才可以用贪心算法去解决该问题)
1、 贪心选择性质:意思就是说一个全局最优解可以通过局部最优(贪心)选择来达到。更简单的说,我们只考虑当前问题最优的选择而不考虑子问题的结果。
这一点不同于动态规划:动态规划依赖子问题,但是贪心却不用。所以解动态规划问题一般是自底向上,从小子问题处理至大子问题。在贪心算法中,我们所做的总是当前看似最佳的选择,然后在解决选择之后出现的子问题。贪心算法所做的当前选择可能要依赖于已经做出的所有选择,但不依赖于有待于做出的选择或是子问题的解。因此不像动态规划方法那样自底向上的解决子问题,贪心策略通常是自顶向下的,一个个的做贪心选择,不断的将给定的问题实例归约为更小的问题。
该步骤的证明就是:每一步所做的贪心选择最终能产生一个全局最优解。
2、 最优子结构
对一个问题来说,如果他的一个最优解包含了其子问题的最优解,则称该问题具有最优子结构。
与动态规划中使用最优子结构不同,在贪心算法中,假设我们做了一个贪心选择后得到一个子问题。我们要做的就是证明次子问题的最优解与所做的贪心选择合并后,的确可以得到原问题的一个最优解。
可能还有很多人不明白动态规划与贪心的区别,来列举一个比较经典的比较方法:
0-1背包问题与部分背包问题:
0-1背包问题:有一个贼在偷窃一家商店时发现有n件物品;第i件物品值vi元,重wi磅,此处vi和wi都是整数。他希望带走的东西越值钱越好,但他的背包中至多只能装下W磅的东西,W为一整数。应该带走几样东西(小偷不能带走某个物品的一部分或带走两次以上同一物品)。
部分背包问题:场景与上面相同,但是窃贼可以带走物品的一部分。
可以吧0-1背包问题的一件物品想象成一个金锭,而部分背包问题中的一件物品则更像金粉。
简单说了一下贪心与相关的动态规划及其不同点,现在就来分析赫夫曼编码了。
赫夫曼编码:
赫夫曼编码用于压缩文件,压缩数据非常有效。
这里的赫夫曼编码方案都是前缀编码,即没有一个编码是另一个编码的前缀。赫夫曼贪心算法使用了一张字符出现的频度表,根据它来构造一种将每个字符表示成二进制串的最优方式。
就像开头和大家说的,赫夫曼编码利用了贪心算法,我们在上文也说了,贪心算法不一定可以找到最优解,要使用贪心算法去解决问题就必须证明该问题符合贪心算法的两个性质才行。所以现在先来证明赫夫曼编码符合贪心算法的两个性质,之后我们再来讨论具体的实现。
1、 赫夫曼编码符合贪心算法的贪心选择性质
这里有一个引理:设C为一字母表, 其中每个字符cЄC具有频度f[c].设x和y为C中具有最低频度的两个字符,则存在C的一种最优前缀编码,其中x和y的编码长度相同但最后一位不同。
图:
(因为博文不支持上下标,所以大家参照看一下,标示有点区别,但是也分别代表着不同的树)
证明:证明的主要思想是是树T表示任一种最优前缀编码,然后对它进行修改,使之表示另一种最优前缀编码,是的字符x和y在新树中成为具有最大深度的叶结点。如果我们能够做到这一点,则它们的编码就具有相同的长度,而仅仅最后一位不同。
设a和b为树中具有最大深度的兄弟叶结点。不失一般性,假设f[a]<=f[b]且f[x]<=f[y]。因为f[x]和f[y]是两个最低的频度,而f[a]和f[b]是任意的频度,故有f[x] <= f[a]且f[b]<=f[y]。交换a和x在树T1中位置产生树T2,然后交换b和y在树T2中的位置产生T3。树T1和T2之间的代价上相差为(代价这么算因为:赫夫曼编码的长度就表示着该字符在树中的深度):
B(T1) – B(T2) = -
=f[x]*depT1(x) + f[a]*depT1(a) –f[x]*depT2(x) – f[a]*depT2(a)
= f[x]*depT1(x) + f[a]*depT1(a) – f[x]*depT2(a) –f[a]*depT2(x)
=(f[a] – f[x])*(depT1(a) – depT1(x)) >= 0
因为f[a] – f[x]和depT1(a) – depT1(x)都是非负的。因为x是具有最小频度的叶结点,a是具有最大深度的叶结点。同样地,B(T2)–B(T3)
也是非负的,所以B(T3) <= B(T1),又因为我假设T1是最优的一棵赫夫曼树,可以得出B(T1) <= B(T3),从而推出B(T1) == B(T3)。到这里大家应该也能够看出来了,T3就是一棵最优树,其中x和y为具有最大深度的兄弟叶结点,从而证明了上面的引理。
我们证明了上面的引理正确也就证明了赫夫曼算法符合贪心算法的第一个贪心选择的性质。即每次的选择都是局部最优的。
2、 赫夫曼编码符合贪心算法的最优子结构性质
还是有一个引理,通过引理的证明来表示出赫夫曼算法符合贪心算法。证明该算法用于寻找最优树的正确性。
引理:设C为一给定字母表,其中每个字母cЄC都定义频度f[c].设x和y是中具有最低频度的两个字母。并设C1为字母表移去x和y,再加上字符z后的字母表,亦即C1 = C – {x,y}U{z};如C一样为C1定义f,其中f[z] = f[x] + f[y]。设T2为表示字母表C1上最优前缀编码的任意一棵树,那么,将T2中的叶子结点z替换成具有x和y孩子的内部结点所得到的树T1,表示字母表C上的一个最优前缀编码。
证明:我们先来考虑将树T1的代价B(T)以树T2的代价B(T2)来表示。对每一个cЄC- {x,y},我们又depT1(c)==depT2(c)(可以自己想想为什么) 故f[c]depT1(c)== f[c]depT2(c).又因为depT1(x) == depT1(y) == depT2(z) + 1,从而我们得出:
f[x]depT1(x) + f[y]depT1(y) =(f[x]+f[y])*(depT2(z) + 1)
=f[z]depT2(z) + (f[x] +f[y])
由上式可得:B(T1) = B(T2) + f[x] + f[y]
或是这样:B(T2) = B(T) - f[x] - f[y]
利用反证法证明此引理。假设T不表示C的最优前缀编码,那么存在一棵树T3,有B(T3) < B(T1)。同样的T2也有两个兄弟结点。设T3是由T2中奖x和y的父亲结点替换为叶子结点z而得,其中频度f[z] = f[x] + f[y],那么:B(T3) = B(T2) - f[x] - f[y] < B(T)- f[x] - f[y] = B(T2)
推出一个矛盾,因为我们假设T2表示C1上的最优前缀编码,那么T必定表示字母表C上的最优前缀编码。
可能有的读者看不懂这个到底在讲什么,或是有部分懂了,没有感觉它是在证明最优子结构的性质。我给大家提示一下,大家应该就能够理解为什么了。在这里假设T1为最优子结构,经过证明通过贪心选择后证明得到的T2同样也为最优子结构,T2是将T1中的x,y结点替换之后得到的。这下大家应该能够明白了吧。
我们已经证明了赫夫曼算法即贪心算法,确实是可以得到一种最优树的,现在就来给出代码吧。
赫夫曼算法代码:
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
using namespace std;
#define max 50000
typedef struct
{
int lson, rson, parent;
int weight;
}hfnode, *huffmantree;
typedef char **huffmancode;
//选择函数
void select(huffmantree ht, int n, int &s1, int &s2)
{
int min1, min2;
min1=min2=max;
for(int i=n; i>=1; i--)//记住这种获得位置的顺序
{
if(ht[i].parent == 0) {
if(ht[i].weight < min1)
{
min2=min1;
s2=s1;
min1=ht[i].weight;
s1=i;
}
else if(ht[i].weight <min2)
{
min2=ht[i].weight;
s2=i;
}
}
}
}
//转置函数
void reverse(char *a, int n)
{
int i;
for(i=0; i<n/2; i++)
{
char t;
t=a[i];
a[i]=a[strlen(a)-1-i];
a[strlen(a)-1-i]=t;
}
}
//赫夫曼编码
void huffmancoding(int n, int *w, huffmantree &ht, huffmancode&hc)
{
int i, j;
int m=2*n-1; //存储的数目
ht=(huffmantree )malloc((m+1)*sizeof(hfnode));//从一开始
for(i=1; i<=n; i++)
{
ht[i].weight=w[i];
ht[i].parent=0;
ht[i].lson=ht[i].rson=0;
}
for(; i<=m; i++)
ht[i].weight=ht[i].parent=ht[i].lson=ht[i].rson=0;
int s1, s2;
for(i=n+1; i<=m; i++)
{
select(ht, i-1, s1, s2);//引用
ht[i].lson=s1;
ht[i].rson=s2;
ht[s1].parent=i;
ht[s2].parent=i;
ht[i].weight=ht[s1].weight+ht[s2].weight;
}
//编码
hc=(huffmancode)malloc((n+1)*sizeof(char *));//表示n个字符
char *cd;
cd=(char *)malloc(n*sizeof(char));
for(i=1; i<=n; i++)
{
int t=0;
memset(cd, '\0', sizeof(cd));
for(int c=i, f=ht[i].parent; f!=0; c=f, f=ht[f].parent)
if(ht[f].lson == c)cd[t++]='0';
else cd[t++]='1';
hc[i] = (char *)malloc( n * sizeof(char));
reverse(cd, strlen(cd)); //将cd转置
strcpy(hc[i], cd);
printf("%s\n", hc[i]);
}
free(cd);
}
//主函数
int main()
{
int n;
while(scanf("%d", &n)!=EOF){
int i, j;
int w[max];
huffmantree ht;
huffmancode hc;
for(i=1; i<=n; i++)
scanf("%d",&w[i]);//出现的频率
huffmancoding(n, w, ht, hc);
}
return 0;
}