数据压缩原理与应用 实验三 Huffman编码与解码

一、基本原理

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.流程图

Huffman编码流程

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.结果统计
文件类型JPGDOCRGBPDFLRCAVIMPGRARPNGPPT
平均码长7.7841933.627.43287.96636.04157.7187017.9720128.0000485.730137
信源熵(bit/sym)7.7838243.5947.40177.40176.00567.6869587.9486697.99857.9945625.665643
原文件大小(kb)601619221329796176448521132
压缩后文件大小(kb)5991802132946615644952296
压缩比1.0171.7781.067111.0351.0030.9980.9991.375
3.结果分析

对于本身已经过压缩的文件而言,如.rar,.png等,信源符号接近等概分布,再进行压缩的效果不大。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值