摘要 当一个问题具有最优子结构性质时,可用动态规划法求解,但有时会有更简单,更有效的算法。本文通过对哈夫曼编码问题的引入,探讨并研究了贪心算法的基本思想及其特点,并在这一典型的贪心算法的基础上推广到三元码的情形,证明该算法是否可产生最优三元码。
关键词 贪心算法,贪心选择性质,最优子结构性质,哈夫曼算法,三元码
1问题简述
哈夫曼编码是广泛地用于数据文件压缩的十分有效的编码方法。其压缩率通常在20%~90%之间。哈夫曼编码算法用字符在文件中出现的频率来建立一个用0,1串表示各字符的最优表示方式。现将哈夫曼算法推广到三元码的情形(即用0,1,2进行编码),证明该算法可产生最优三元码。
2贪心选择策略的基本要素
贪心算法通过一系列的选择得到问题的解。它所做的每一个选择都是当前状态下局部最好选择,即贪心选择。希望通过每次所做的贪心选择导致最终结果是一个最优解。这种启发式的策略并不是总能奏效,然而在许多情况下却能达到预期的目的。
2.1贪心选择性质
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部优先的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素。在贪心算法中,仅在当前状态下作出最好选择,即局部最优选择,后再去解作出这个选择后产生的相应的子问题。贪心算法所做的贪心选择可以依赖于以往所作过的选择,但绝不依赖于将来所做的选择,也不依赖于问题的解。
对于一个具体问题,确定它是否具有贪心选择性质,必须证明每一步所做的贪心选择最终导致问题的一个整体最优解。首先考察问题的一个整体最优解,并证明可修改这个最优解,使其以贪心选择开始。作出贪心选择后,原问题简化为规模更小的类似子问题。然后用数字归纳法证明,通过每一步做贪心选择,最终得到问题的整体最优解。其中,证明贪心选择后的问题简化为规模更小的类似子问题的关键在于利用该问题的最优子结构性质。
2.2最优子结构性质
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心算法求解的关键特征。
3算法设计
3.1前缀码
对每一个字符规定一个0,1,2串作为其代码,并要求任一字符的代码都不是其他字符代码的前缀。这种编码称为前缀码。译码过程需要方便的取出编码的前缀,因此需要表示前缀码的合适的数据结构。为此,可以用三叉树作为前缀码的数据结构:树叶表示给定字符;从树根到树叶的路径当作该字符的前缀码;代码中每一位的0,1,2则作为指示某节点到儿子的“路标”。结构如图3-1所示。
图3-1
给定字符集编码C及其频率分布f,即C中任一字符c以频率f(c)在数据文件中出现。C的一个前缀码编码方案对应一棵三叉树。字符c在树T中的深度记作dT(c),dT(c)也是字符c的前缀码长。
该编码方案的平均码长定义为 使平均码长达到最小的前缀码编码方案称为C的最优前缀码。
3.2构造三元哈夫曼编码
哈夫曼提出构造最优前缀码的贪心算法,由此产生的编码方案称为哈夫曼编码。现将哈夫曼算法推广到三元码,其构造步骤如下:
(1)构造只有根结点的三叉树,构成一个森林F。
(2)在森林F中选取三棵根结点的频率最小的树作为左子树,右子树,中子树构造一棵新的三叉树,且置新的三叉树的根结点的频率为其左子树,右子树,中子树上根结点的频率之和。
(3)在森林F中删除这三棵树,同时将新得到的三叉树加入F中。
(4)重复步骤(2)(3),直到F中只含有一棵树为止。这棵树便是三元哈夫曼树。
3.3证明三元哈夫曼算法中的最优前缀码具有贪心选择性质
设C是编码字符集,C中字符c的频率为f(c)。设x,y,z是C中具有最小频率的三个字符,存在C 的最优前缀码使x,y,z具有相同码长且最后一位编码不同。
证明:设三叉树T表示C的任意一个最优前缀码。下面证明可以对T作适当修改后得到一棵新树T''',使得在新树中x,y,z是最深叶子且为兄弟。同时新树T''表示的前缀码也是C的最优前缀码。如果能做到这一点,则x,y,z在T'''表示的最优前缀码中就具有相同的码长且仅最后一位编码不同。
设b,c,d是树T的最深叶子且为兄弟。不失一般性可设f(b)≤f(c)≤f(d), f(x)≤f(y)≤f(z)。由于x,y,z是C中具有最小频率的三个字符,故f(x)≤f(b),f(y)≤f(c), f(z)≤f(d)。
首先在树T中交换叶子b和x的位置得到树T',然后在树T'中再交换叶子c和y的位置得到树T'',最后在树T''中交换叶子d和z 的位置,得到树T''',如图3-2所示。
图3-2
由此可知,树T和T''表示的最优前缀码的平均码长之差为:
B(T)-B(T'')=∑c∈C f(c)dT(c)- ∑c∈C f(c)dT'(c)
= f(x)dT(x)+ f(b)dT(b)- ∑c∈C f(x)dT'(x)- ∑c∈C f(b)dT'(b)
= f(x)dT(x)+ f(b)dT(b)- ∑c∈C f(x)dT(b)- ∑c∈C f(b)dT(x)
=(f(b)-f(x))( dT(b)- dT(x)) ≥0
类似地,可以证明在T'中交换y与c的位置也不增加平均码长,即B(T')-B(T'')也是非负的,在T''中交换z与d的位置与此同理。由此可知B(T''')≤B(T'') ≤ B(T') ≤ B(T)。另一方面,由于T所表示的前缀码是最优的,故B(T)≤B(T''')。因此,B(T)=B(T'''),即T'''表示的前缀码也是最优前缀码,且x,y,z具有最长的码长,同时仅最后一位编码不同。
3.3证明三元哈夫曼算法中的最优前缀码具有优子结构性质
设T表示字符集C的一个最优前缀码的三叉树。在C中字符c的出现频率为f(c)。设x,y,z是三叉树T中的三个叶子且为兄弟,w是它们的父亲。若将w看作是具有频率f(w)=f(x)+f(y)+f(z)的字符,则三叉树T'=T-{x,y,z}表示字符集C'=C-{x,y,z}∪{w}的一个最优前缀码。
证明:首先证明T的平均码长B(T)可用T'的平均码长B(T')表示。
对任意c∈C-{x,y,z}有dT(c)=dT' (c),故f(c)dT(c)=f(c)dT' (c)。
另一方面,dT(x)=dT(y)=dT(z)=dT' (w)+1
故f(x)dT(x)+f(y)dT(y)+ f(z)dT(z)=(f(x)+f(y)+f(z))( dT' (w)+1)
=f(x)+f(y)+f(z)+f(w) dT' (w)
由此即知,B(T)=B(T')+f(x)+f(y)+f(z)。
若T'所表示的字符集C'的前缀码不是最优前缀码,则有T''表示的C'的前缀码使得B(T'')<B(T')。由于w被看作是C'中的一个字符,故w在T''中是一树叶。若将x,y,z加入树T''作为w的儿子,则得到表示字符集C的最优前缀码的三叉树T''',且有
B(T''')=B(T'')+ f(x)+f(y)+f(z)<B(T')+ f(x)+f(y)+f(z)=B(T)
这与T的最优性矛盾,故T'所表示的C'的前缀码是最优的。
由贪心选择性质和最优子结构性质可以推出三元哈夫曼算法是正确的,即该算法可产生最优三元码。
4详细算法
#define n 100
#define m 2 * n -1
typedef struct { //存储结构
char ch;
char bits[9];
int len;
}CodeNode;
typedef CodeNodeHuffmanCode[n + 1];
typedef struct{ //树节点的存储结构
int weight;
int child1, child2, child3,parent;
}HTNode;
typedef HTNode HuffmanTree[m+ 1];
int num,num0;
void select(HuffmanTree HT,int k, int&s1, int&s2, int&s3){
int i = 0, j = 0;
int minl = 32767;
for(i = 1; i <= k; i++){
if(HT[i].weight < minl&& HT[i].parent == 0){
j = i;
minl = HT[i].weight;
}
}
s1 = j; minl=32767;
for(i = 1; i <= k; i++){
if(HT[i].weight < minl&& HT[i].parent == 0 && i != s1){
j = i;
minl = HT[i].weight;
}
}
s2 = j;
minl = 32767;
for(i = 1; i <= k; i++){
if(HT[i].weight < minl&& HT[i].parent == 0 && i != s1 && i != s2){
j = i;
minl = HT[i].weight;
}
}
s3 = j;
}
int jsq(char *s, int cnt[],charstr[]){ //统计输入字符和串
char *p;
int i, j , k = 0;
int temp[257];
for(i = 0; i < 257; i++){
temp [i] = 0;
}
for(p = s; *p != '\0'; p++){
temp[*p]++;
}
for(i = 0, j = 0;i <=256;i++){
if(temp[i] != 0){
j++;
str[j] = i;
cnt[j] = temp[i];
}
}
num0 = j;
while((j - 3) % 2 != 0)
j++;
num = j;
return num0;
}
voidchuffmanTree(HuffmanTree &HT,HuffmanCode &HC,int cnt[],char str[]){ //建立哈夫曼树
int i,s1,s2,s3;
for(i = 1; i <= 2 * num -1; i++){
HT[i].child1 = 0;HT[i].child2 = 0; HT[i].child3 = 0;
HT[i].parent = 0;
HT[i].weight = 0;
}
for(i = 1;i <= num; i++){
HT[i].weight = cnt[i];
}
for(i = num + 1; i <= num+ (num - 3) / 2 + 1; i++){
select(HT, i - 1, s1, s2,s3);
HT[s1].parent = i;HT[s2].parent = i; HT[s3].parent = i;
HT[i].child1 = s1;HT[i].child2 = s2; HT[i].child3 = s3;
HT[i].weight = HT[s1].weight+ HT[s2].weight + HT[s3].weight;
}
for(i = 1; i <= num;i++){
HC[i].ch = str[i];
}
}
voidHuffmanEncoding(HuffmanTree HT, HuffmanCode HC){ //給每个叶子节点分配二进制码
int c,p,I,star;
char cd[n];
cd[num] = '\0';
for(i = 1; i <= num;i++){ //1到num为叶子节点,每个叶子节点都有一个二进制编码串
start = num;
c = i;
while((p = HT[c].parent)> 0){
start--;
if(HT[p].child1 == c)cd[start] = '0';
if(HT[p].child2 == c)cd[start] = '1';
if(HT[p].child3 == c)cd[start] = '2';
c = p;
}
strcpy(HC[i].bits,&cd[start]);
HC[i].len = num - start;
}
}
void coding(HuffmanCode HC,char str[],char get[]){ //输出字符串的二进制编码
int i, j = 0;
while(str[j] != '\0'){
for(i = 1; i <= num; i++){
if(HC[i].ch == str[j]){
strcat(get,HC[i].bits);
break;
}
}
j++;
}
strcat(get,"\0");
}
5.分析与总结
该算法的时间复杂度为:O(nlog3n)
测试结果如图5-1所示。
图5-1
贪心算法是计算机算法策略中常用的一个,往往在需要解决一些最优性问题时,都可以应用贪心算法。贪心选择策略虽然不能保证求得的最后解一定是最佳的,但是可以为某些问题确定一个可行性范围。而除了本文所介绍到的算法外,在查阅资料时能够发现在关于哈夫曼多元码的推广和哈弗曼树的算法优化等问题的相关文献还有很多,也侧面反映了此问题的典型性,同时也十分具有研究价值。