一、基本原理
1.Huffman 编码
(1) Huffman Coding (霍夫曼编码)是一种无失真编码的编码方式,Huffman 编码是可变字长编码(VLC)的一种。
(2) Huffman 编码基于信源的概率统计模型,它的基本思路是,出现概率大的信源符号编长码,出现概率小的信源符号编短码,从而使平均码长最小。
(3) 在程序实现中常使用一种叫做树的数据结构实现 Huffman 编码, 由它编出的码是即时码。
2.Huffman 编码的方法
(1)将文件以ASCII字符流的形式读入,统计每个符号的发生频率;
(2)将所有文件中出现过的字符按照频率从小到大的顺序排列;
(3)每一次选出最小的两个值,作为二叉树的两个叶子节点,将和作为它们的根节点,这两个叶子节点不再参与比较,新的根节点参与比较;
(4)重复3,直到最后得到和为1的根节点;
(5)将形成的二叉树的左节点标0,右节点标1,把从最上面的根节点到最下面的叶子节点途中遇到的0、1序列串起来,得到了各个字符的编码表示。
3.Huffman的数据结构设计
在程序实现中使用一种叫做二叉树的数据结构实现Huffman编码。
(1)节点结构
/* Huffman节点结构体 */
typedef struct huffman_node_tag
{
unsigned char isLeaf; //是否为叶节点,1表示是,0不是
unsigned long count; //节点代表的符号加权和
struct huffman_node_tag *parent;//父节点指针
union
{
struct //若不是叶节点,则为左右子节点指针
{
struct huffman_node_tag *zero, *one; //子节点指针,分别代表0,1子节点指针
};
unsigned char symbol; //节点代表的符号
};
} huffman_node;
(2)码结构
/* Huffman码字结构体 */
typedef struct huffman_code_tag
{
unsigned long numbits; //该码所用的比特数
unsigned char *bits; //指向该码比特串的指针
} huffman_code;
二、实验流程及代码分析
1.流程图
2.代码分析
(1)读入待编码的源文件
huffcode.c
int
main(int argc, char** argv)
{
char memory = 0; //内存操作标识符,1-内存数据,0-外部输入数据
char compress = 1; //编解码标识符,1-编码,0-解码
int opt; //命令行参数
const char *file_in = NULL, *file_out = NULL; //输入输出文件名
const char *file_out_table = NULL; //输出信息表文件名
FILE *in = stdin; //输入信息流
FILE *out = stdout; //输出信息流
FILE * outTable = NULL; //输出信息表
/* 读取命令行参数 */
while((opt = getopt(argc, argv, "i:o:cdhvmt:")) != -1)
{
switch(opt)
{
case 'i': //i 表示输入文件
file_in = optarg;
break;
case 'o': //0 表示输出文件
file_out = optarg;
break;
case 'c': //c 表示进行编码,compress=1
compress = 1;
break;
case 'd': //d 表示进行解码,compress=0
compress = 0;
break;
case 'h': //h 表示帮助,输出参数用法
usage(stdout);
return 0;
case 'v': //v 表示输出版本号
version(stdout);
return 0;
case 'm': //m 表示对内存数据进行操作,memory=1
memory = 1;
break;
case 't': //t 表示输出信息表文件
file_out_table = optarg;
break;
default:
usage(stderr);
return 1;
}
}
/* 打开输入文件 */
if(file_in)
{
in = fopen(file_in, "rb");
if(!in)
{
fprintf(stderr,
"Can't open input file '%s': %s\n",
file_in, strerror(errno));
return 1;
}
}
/* 生成输出文件 */
if(file_out)
{
out = fopen(file_out, "wb");
if(!out)
{
fprintf(stderr,
"Can't open output file '%s': %s\n",
file_out, strerror(errno));
return 1;
}
}
/* 生成输出信息表文件 */
if(file_out_table)
{
outTable = fopen(file_out_table, "w");
if(!outTable)
{
fprintf(stderr,
"Can't open output file '%s': %s\n",
file_out_table, strerror(errno));
return 1;
}
}
/* 对内存数据进行操作 */
if(memory)
{
return compress ?
memory_encode_file(in, out) : memory_decode_file(in, out);
}
/* 判断进行编码或解码操作 */
if(compress)
huffman_encode_file(in, out,outTable);
else
huffman_decode_file(in, out);
/* 收尾工作 */
if(in)
fclose(in);
if(out)
fclose(out);
if(outTable)
fclose(outTable);
return 0;
}
getopt.c
int
getopt(int nargc, char * const *nargv, const char* ostr)
{
static char *place = EMSG; /* option letter processing */
char *oli; /* option letter list index */
if (optreset || !*place) { /* update scanning pointer */
optreset = 0;
if (optind >= nargc || *(place = nargv[optind]) != '-') {
place = EMSG;
return (EOF);
}
if (place[1] && *++place == '-') { /* found "--" */
++optind;
place = EMSG;
return (EOF);
}
} /* option letter okay? */
if ((optopt = (int)*place++) == (int)':' ||
!(oli = strchr(ostr, optopt))) {
/*
* if the user didn't specify '-' as an option,
* assume it means EOF.
*/
if (optopt == (int)'-')
return (EOF);
if (!*place)
++optind;
if (opterr && *ostr != ':')
(void)fprintf(stderr,
"%s: illegal option -- %c\n", __FILE__, optopt);
return (BADCH);
}
if (*++oli != ':') { /* don't need argument */
optarg = NULL;
if (!*place)
++optind;
}
else { /* need an argument */
if (*place) /* no white space */
optarg = place;
else if (nargc <= ++optind) { /* no arg */
place = EMSG;
if (*ostr == ':')
return (BADARG);
if (opterr)
(void)fprintf(stderr,
"%s: option requires an argument -- %c\n",
__FILE__, optopt);
return (BADCH);
}
else /* white space */
optarg = nargv[optind];
place = EMSG;
++optind;
}
return (optopt); /* dump back option letter */
}
(2)第一次扫描:统计文件中各个字符出现概率
huffman.c
/* get_symbol_frequencies函数功能:统计文件中各个字符出现概率 */
static unsigned int
get_symbol_frequencies(SymbolFrequencies *pSF, FILE *in)
{
int c;
unsigned int total_count = 0;//总数
/* 先将所有字符的频率设为0 */
init_frequencies(pSF);
/* 统计输入文件中每个字符出现的频率 */
while((c = fgetc(in)) != EOF) //逐个读取输入文件中的字符
{
unsigned char uc = c; //将读取的字符赋值给uc
if(!(*pSF)[uc]) //若此字符是第一次读取,分配空间
(*pSF)[uc] = new_leaf_node(uc);
++(*pSF)[uc]->count; //若不是,加一
++total_count; //总数加一
}
return total_count;
}
/* new_leaf_node函数功能:建立一个新的叶节点 */
static huffman_node*
new_leaf_node(unsigned char symbol)
{
huffman_node *p = (huffman_node*)malloc(sizeof(huffman_node));//分配一个叶节点的空间
p->isLeaf = 1; //标志当前节点为叶节点
p->symbol = symbol; //当前信源符号
p->count = 0; //新叶节点在文件中出现次数初始化为0
p->parent = 0; //父节点指针初始化为0
return p;
}
(3)建立Huffman树,生成码字
huffman.c
a.建立Huffman树:
/* calculate_huffman_codes函数功能:生成huffman码树 */
static SymbolEncoder*
calculate_huffman_codes(SymbolFrequencies * pSF)
{
unsigned int i = 0;
unsigned int n = 0;
huffman_node *m1 = NULL, *m2 = NULL;//排序用到的中间变量
SymbolEncoder *pSE = NULL; //中间变量
//排序前,按序输出每片树叶代表的信源符号和出现次数
#if 1
printf("BEFORE SORT\n");
print_freqs(pSF);
#endif
//按符号出现的次数排列节点结构体指针数组
qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp);
//排序后,按序输出每片树叶代表的信源符号和出现次数
#if 1
printf("AFTER SORT\n");
print_freqs(pSF);
#endif
//统计实际节点种类数
for(n = 0; n < MAX_SYMBOLS && (*pSF)[n]; ++n);
for(i = 0; i < n - 1; ++i)
{
//把出现次数最少的两个信源符号节点设为 m1,m2
m1 = (*pSF)[0];
m2 = (*pSF)[1];
// 合并这两个符号,把合并后的新节点设为这两个节点的父节点
(*pSF)[0] = m1->parent = m2->parent =
new_nonleaf_node(m1->count + m2->count, m1, m2);
(*pSF)[1] = NULL;//合并后,第二个节点为空
//处理完毕后再次排序
qsort((*pSF), n, sizeof((*pSF)[0]), SFComp);
}
//构造完成后,为码字数组分配空间
pSE = (SymbolEncoder*)malloc(sizeof(SymbolEncoder));
memset(pSE, 0, sizeof(SymbolEncoder));
build_symbol_encoder((*pSF)[0], pSE);
return pSE;
}
/* print_freqs函数功能:输出256个信源符号及其出现次数*/
#if 1
static void
print_freqs(SymbolFrequencies * pSF)
{
size_t i;
for(i = 0; i < MAX_SYMBOLS; ++i)
{
if((*pSF)[i])
printf("%d, %ld\n", (*pSF)[i]->symbol, (*pSF)[i]->count);
else
printf("NULL\n");
}
}
#endif
/* new_nonleaf_node函数功能:建立一个新的非叶节点 */
static huffman_node*
new_nonleaf_node(unsigned long count, huffman_node *zero, huffman_node *one)
{
huffman_node *p = (huffman_node*)malloc(sizeof(huffman_node));//分配一个非叶节点的空间
p->isLeaf = 0; //标志当前节点不是叶节点
p->count = count; //计数
p->zero = zero; //左子树
p->one = one; //右子树
p->parent = 0;
return p;
}
b.生成码字:
/* SFComp函数功能:按符号出现次数生序排列256个码字结构体指针 */
static int
SFComp(const void *p1, const void *p2)
{
const huffman_node *hn1 = *(const huffman_node**)p1;
const huffman_node *hn2 = *(const huffman_node**)p2;
//把空节点往后排
if(hn1 == NULL && hn2 == NULL)
return 0;
if(hn1 == NULL)
return 1;
if(hn2 == NULL)
return -1;
//p1>p2,count=1;p1<p2,count=-1
if(hn1->count > hn2->count)
return 1;
else if(hn1->count < hn2->count)
return -1;
return 0;
}
/* build_symbol_encoder函数功能:为每一个树叶编码,输入树根遍历码树找到树叶进行编码 */
static void
build_symbol_encoder(huffman_node *subtree, SymbolEncoder *pSF)
{
//若是空树,返回
if(subtree == NULL)
return;
//若是叶节点,进行编码
if(subtree->isLeaf)
(*pSF)[subtree->symbol] = new_code(subtree);
//若都不是,先访问左节点,再访问右节点
else
{
build_symbol_encoder(subtree->zero, pSF);
build_symbol_encoder(subtree->one, pSF);
}
}
/* new_code函数功能:生成码字*/
static huffman_code*
new_code(const huffman_node* leaf)
{
unsigned long numbits = 0;//码长初始化
unsigned char* bits = NULL;
huffman_code *p;
while(leaf && leaf->parent)//当树叶和其父节点都不为空时,执行循环
{
huffman_node *parent = leaf->parent;
unsigned char cur_bit = (unsigned char)(numbits % 8); //所编位在当前byte中的位置
unsigned long cur_byte = numbits / 8; //当前是第几个byte
if(cur_bit == 0) //下一个byte
{
size_t newSize = cur_byte + 1;
bits = (char*)realloc(bits, newSize);
bits[newSize - 1] = 0;
}
if(leaf == parent->one)
bits[cur_byte] |= 1 << cur_bit;
++numbits; //码长加一
leaf = parent;
}
//编码完对码字进行倒序
if(bits)
reverse_bits(bits, numbits);
//输出
p = (huffman_code*)malloc(sizeof(huffman_code));
p->numbits = numbits;
p->bits = bits;
return p;
}
/* reverse_bits函数功能:将生成的码字进行倒序 */
static void
reverse_bits(unsigned char* bits, unsigned long numbits)
{
unsigned long numbytes = numbytes_from_numbits(numbits);//判断码字字节数
unsigned char *tmp =(unsigned char*)alloca(numbytes); //分配空间
unsigned long curbit; //当前比特数
long curbyte = 0; //当前字节数初始化
memset(tmp, 0, numbytes);
for(curbit = 0; curbit < numbits; ++curbit)
{
unsigned int bitpos = curbit % 8;//判断当前是字节中哪一位
if(curbit > 0 && curbit % 8 == 0)//若位数达到8,字节数加一
++curbyte;
tmp[curbyte] |= (get_bit(bits, numbits - curbit - 1) << bitpos);//从后往前取码字中的每一位,移至正确位置
}
memcpy(bits, tmp, numbytes);
}
/* numbytes_from_numbits函数功能:由比特位长度求字节数 */
static unsigned long
numbytes_from_numbits(unsigned long numbits)
{
return numbits / 8 + (numbits % 8 ? 1 : 0);
}
/* get_bit函数功能:取出码字中某一位*/
static unsigned char
get_bit(unsigned char* bits, unsigned long i)
{
return (bits[i / 8] >> i % 8) & 1;
}
(4)将码表及其他必要信息写入输出文件
/* write_code_table函数功能:将码表写到输出文件中*/
static int
write_code_table(FILE* out, SymbolEncoder *se, unsigned int symbol_count)
{
unsigned long i, count = 0;
//再次确认统计实际码字种类
for(i = 0; i < MAX_SYMBOLS; ++i)
{
if((*se)[i])
++count;
}
//把字节种类数和字节总数变成大端保存的形式,写入文件中
i = htonl(count);
if(fwrite(&i, sizeof(i), 1, out) != 1)
return 1;
//写入输入文件字节数
symbol_count = htonl(symbol_count);
if(fwrite(&symbol_count, sizeof(symbol_count), 1, out) != 1)
return 1;
//写入码表
for(i = 0; i < MAX_SYMBOLS; ++i)
{
huffman_code *p = (*se)[i];
if(p)
{
unsigned int numbytes;
fputc((unsigned char)i, out); //写字节符号
fputc(p->numbits, out); //写码长
numbytes = numbytes_from_numbits(p->numbits);//写码字
if(fwrite(p->bits, 1, numbytes, out) != numbytes)
return 1;
}
}
return 0;
}
(5)对源文件进行编码
/* do_file_encode函数功能:对文件符号进行编码 */
static int
do_file_encode(FILE* in, FILE* out, SymbolEncoder *se)
{
unsigned char curbyte = 0; //当前字节码字
unsigned char curbit = 0; //当前位
int c;
//逐个读取文件字符
while((c = fgetc(in)) != EOF)
{
unsigned char uc = (unsigned char)c;
huffman_code *code = (*se)[uc];//把当前符号uc作为数组的下标
unsigned long i;
for(i = 0; i < code->numbits; ++i)
{
curbyte |= get_bit(code->bits, i) << curbit;//把码字中的比特位放到编码字节相应位置
if(++curbit == 8) //若当前字节已经写满,输出,再写一个新的字节
{
fputc(curbyte, out);//输出当前字节
curbyte = 0;
curbit = 0;
}
}
}
//若剩余的码字不够一字节,输出
if(curbit > 0)
fputc(curbyte, out);
return 0;
}
(6)总的编码函数,调用以上各个模块
/* huffman_encode_file函数:编码过程总函数,将每个小部分组织在一起 */
int
huffman_encode_file(FILE *in, FILE *out, FILE *out_Table)
{
SymbolFrequencies sf;//
SymbolEncoder *se;
huffman_node *root = NULL;
int rc;
unsigned int symbol_count;
huffman_stat hs;
//第一遍扫描,获取输入文件每个符号的出现频率
symbol_count = get_symbol_frequencies(&sf, in);
huffST_getSymFrequencies(&sf,&hs,symbol_count);//将信源的概率写入输出信息
//建立Huffman树和码表
se = calculate_huffman_codes(&sf);
root = sf[0];
//输出信息
huffST_getcodeword(se, &hs);
output_huffman_statistics(&hs,out_Table);
//第二次扫描,利用之前建立的码表进行编码,写入输出文件
rewind(in); //回到文件头
rc = write_code_table(out, se, symbol_count); //在输出文件中写入码表
if(rc == 0) //编码
rc = do_file_encode(in, out, se);
//释放码树
free_huffman_tree(root);
free_encoder(se);
return rc;
}
三、实验结果
1.样本文件的概率分布图
2.结果统计
文件类型 | JPG | DOC | RGB | LRC | AVI | MPG | RAR | PNG | PPT | |
---|---|---|---|---|---|---|---|---|---|---|
平均码长 | 7.784193 | 3.62 | 7.4328 | 7.9663 | 6.0415 | 7.718701 | 7.972012 | 8.00004 | 8 | 5.730137 |
信源熵(bit/sym) | 7.783824 | 3.594 | 7.4017 | 7.4017 | 6.0056 | 7.686958 | 7.948669 | 7.9985 | 7.994562 | 5.665643 |
原文件大小(kb) | 60 | 16 | 192 | 213 | 2 | 979 | 6176 | 448 | 521 | 132 |
压缩后文件大小(kb) | 59 | 9 | 180 | 213 | 2 | 946 | 6156 | 449 | 522 | 96 |
压缩比 | 1.017 | 1.778 | 1.067 | 1 | 1 | 1.035 | 1.003 | 0.998 | 0.999 | 1.375 |
3.结果分析
对于本身已经过压缩的文件而言,如.rar,.png等,信源符号接近等概分布,再进行压缩的效果不大。