Huffman编码及实现

一、基本原理

1. Huffman编码原理

以信源空间的概率分布为基准,用概率匹配方法进行信源编码,可以二叉树的形式进行构造。

(1)编码步骤

  1. 在信源空间中,统计所有信源符号出现次数,求出其概率
  2. 按概率从小到大的顺序排列信源符号,作为树叶节点
  3. 将最小的两个概率节点合并为一个代表其概率和的父节点
  4. 在新生成的父节点与未合并的树叶节点中再选出概率最小的两个节点,重复上一步骤,直到合成概率和为1的节点,即根节点
  5. 从根节点开始,依次为每个节点的左右孩子分配码字(为编程方便,统一左0右1)
  6. 从树根开始向每一个树叶读码字,即为该树叶代表符号的编码

过程如图所示:

这里写图片描述
(2)Huffman编码特点

  • 是从树叶到树根的码字生成过程
  • Huffman码是即时码、紧致码
  • 编码后符号概率(0和1的概率)接近等概分布
  • 码字非唯一,因为概率相同时,合并情况不唯一,每个符号对应的码长不唯一,但平均码长唯一

二、实验流程分析

1. 流程框图

 这里写图片描述

 为了能够分析信源结构和编码效率,输出文件除了编码文件,还有一个信息表用来输出信源符号的统计概率、码长和码字。

2. 关键代码分析

实验中将实际完成编码工作的代码(工程Huff_code)封装成一个静态链接库,由工程huff_run来调用,huff_run完成的工作包括解析命令行参数,打开、读取、关闭输入文件,打开关闭输出文件,调用Huff_code完成编码。

(1)huff_run工程

huffcode.c文件

static void
version(FILE *out)
{
    fputs("huffcode 0.3\n"
          "Copyright (C) 2003 Douglas Ryan Richardson"
          "; Gauss Interprise, Inc\n",
          out);
}

static void
usage(FILE* out)
{
    fputs("Usage: huffcode [-i<input file>] [-o<output file>] [-d|-c]\n"
          "-i - input file (default is standard input)\n"
          "-o - output file (default is standard output)\n"
          "-d - decompress\n"
          "-c - compress (default)\n"
          "-m - read file into memory, compress, then write to file (not default)\n"
          // step1: by yzhang, for huffman statistics
          "-t - output huffman statistics\n",
          //step1:end by yzhang
          out);
}

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;//输入输出文件名
    //step1:add by yzhang for huffman statistics
    const char *file_out_table = NULL;//输出信息表文件名
    //end by yzhang
    FILE *in = stdin;//输入流
    FILE *out = stdout;//输出流
    //step1:add by yzhang for huffman statistics
    FILE * outTable = NULL;//输出信息表
    //end by yzhang

    /* Get the command line arguments. */
    while((opt = getopt(argc, argv, "i:o:cdhvmt:")) != -1) //演示如何跳出循环,及查找括号对
    {
        switch(opt)//判断命令行参数
        {
        case 'i'://-i表示输入文件,为输入文件名赋值
            file_in = optarg;
            break;
        case 'o'://-o表示输出文件,为输出文件名赋值
            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;
        // by yzhang for huffman statistics
        case 't'://-t表示输出信息表文件,为输出信息表文件名赋值
            file_out_table = optarg;            
            break;
        //end by yzhang
        default:
            usage(stderr);
            return 1;
        }
    }

    /* If an input file is given then open it. */
    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 an output file is given then create it. */
    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;
        }
    }

    //by yzhang for huffman statistics
    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;
        }
    }
    //end by yzhang

    if(memory)//对内存数据进行操作
    {
        return compress ?
            memory_encode_file(in, out) : memory_decode_file(in, out);
    }

    if(compress)  //change by yzhang判断编解码标识
        huffman_encode_file(in, out,outTable);//step1:changed by yzhang from huffman_encode_file(in, out) to huffman_encode_file(in, out,outTable)
    else
    huffman_decode_file(in, out);

    if(in)//关闭输入文件
        fclose(in);
    if(out)//关闭输出文件
        fclose(out);
    if(outTable)//关闭输出信息表文件
        fclose(outTable);
    system("pause");//输出不会一闪而过
    return 0;
}

getopt.c文件

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* declarations to provide consistent linkage */
extern char *optarg;
extern int optind;
extern int opterr;

int opterr = 1,     /* if error message should be printed */
    optind = 1,     /* index into parent argv vector */
    optopt,         /* character checked for validity */
    optreset;       /* reset getopt */
char    *optarg;        /* argument associated with option */

#define BADCH   (int)'?'
#define BADARG  (int)':'
#define EMSG    ""

/*
 * getopt --
 * Parse argc/argv argument vector.
 */
int getopt(int nargc, char * const *nargv, const char* ostr)
{//nargc和nargv对应main函数里的argc和argv,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)Huff_code

huffman_encode_file函数
首先分析将实现一个个小功能的函数组织在一起完成整个编码过程的函数

int huffman_encode_file(int argc, char** argv)  //step1:changed by yzhang for huffman statistics from (FILE *in, FILE *out) to (FILE *in, FILE *out, FILE *out_Table)
{
    SymbolFrequencies sf;//含有256个节点的数组
    SymbolEncoder *se;//指向256个编码的指针
    huffman_node *root = NULL;//根节点
    int rc;//标识位
    unsigned int symbol_count;//文件中总ASCII码数
    //step2:add by yzhang for huffman statistics
    huffman_stat hs;//输出信息表 包括符号频率 码长 码字
    //end by yzhang

    /* Get the frequency of each symbol in the input file. */
    symbol_count = get_symbol_frequencies(&sf, in); 
    //演示扫描完一遍文件后,SF指针数组的每个元素的构成,sf中每个节点所代表的信源符号出现的次数count已经被赋值 
    //step3:add by yzhang for huffman statistics,...  get the frequency of each symbol 
    huffST_getSymFrequencies(&sf,&hs,symbol_count);//将信源概率写入输出信息中
    //end by yzhang

    /* Build an optimal table from the symbolCount. */
    se = calculate_huffman_codes(&sf);//编码(核心):256个节点传入得到256个码字
    root = sf[0];//根节点

    //step3:add by yzhang for huffman statistics... output the statistics to file
    huffST_getcodeword(se, &hs);//为输出信息赋值
    output_huffman_statistics(&hs,out_Table);//输出信息
    //end by yzhang

    /* Scan the file again and, using the table
       previously built, encode it into the output file. */
    rewind(in);//将输入文件的内部指针重新指向文件开头(因为开始统计概率时将文件扫描了一遍,内部指针指到了文件最后)
    rc = write_code_table(out, se, symbol_count);//写码表
    if(rc == 0)//成功写入码表后,rc就被赋值为0
        rc = do_file_encode(in, out, se);//写编码后的文件,返回值为0

    /* Free the Huffman tree. */
    free_huffman_tree(root);//释放码树
    free_encoder(se);//释放码字结构体
    return rc;//返回0
}

下面根据huffman_encode_file的调用顺序,分析具体完成每一步操作的代码
step1 结构体和数组的定义
通过节点结构体中指向父节点和左右子节点的指针能够组织一个码树。

/* 定义节点结构体 */
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;//若是中间节点 则包含指向左、右子节点结构体的指针
        };
        unsigned char symbol;//若是树叶 则包含表示的符号
    };
} huffman_node;

/* 定义码字结构体 */
typedef struct huffman_code_tag
{
    /* The length of this code in bits. */
    unsigned long numbits;//码长

    /* The bits that make up this code. The first
       bit is at position 0 in bits[0]. The second
       bit is at position 1 in bits[0]. The eighth
       bit is at position 7 in bits[0]. The ninth
       bit is at position 0 in bits[1]. */
    unsigned char *bits;//指向存码字的数组,每一个符号有一个码字数组
} huffman_code;

/* 定义输出信息结构体 */
typedef struct huffman_statistics_result
{
    float freq[256];//256个ASCII码各自出现的频率
    unsigned long numbits[256];//码长
    unsigned char bits[256][100];//假设了256个码长不超100的码字
}huffman_stat;

#define MAX_SYMBOLS 256
typedef huffman_node* SymbolFrequencies[MAX_SYMBOLS];//定义指向256个节点的指针数组
typedef huffman_code* SymbolEncoder[MAX_SYMBOLS];//定义指向256个码字结构体的指针数组

step2 统计信源符号出现次数,并建立节点

get_symbol_frequencies函数

/* 获得输入文件中每个信源符号的出现次数 */
static unsigned int get_symbol_frequencies(SymbolFrequencies *pSF, FILE *in)
{
    int c;
    unsigned int total_count = 0;//文件大小初始化为0

    /* Set all frequencies to 0. */
    init_frequencies(pSF);//初始化每个节点指针为0(具体函数见下文)

    /* Count the frequency of each symbol in the input file. */
    while((c = fgetc(in)) != EOF)//以字节为单位读取文件
    {
        unsigned char uc = c;
        if(!(*pSF)[uc])//如果该信源符号第一次出现 就建立树叶节点 把符号的ASCII码作为数组的下标
            (*pSF)[uc] = new_leaf_node(uc);//建立叶结点的函数见下文
        ++(*pSF)[uc]->count;//该信源符号数加一
        ++total_count;//计算原始输入文件字节数,即文件大小
    }
    return total_count;//返回文件字节数
}

init_frequencies函数

/* 初始化一个含有256节点指针的数组为0 */
static void init_frequencies(SymbolFrequencies *pSF)
{
    //void *memset(void *s, int ch, size_t n)函数解释:将s中前n个字节替换为ch并返回s;
    memset(*pSF, 0, sizeof(SymbolFrequencies));//全部初始化为0
}

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;//该树叶代表的信源符号在文件中出现的次数
    p->parent = 0;//父节点指针初始化为0
    return p;//返回一个已初始化的叶结点
}

step3 往存储统计信息的结构体写数据,分两次,这是第一次,遍历文件一次统计出符号出现次数,就可以把符号概率写进去

huffST_getSymFrequencies函数

/* 写存储统计信息的结构体 */
//1、写频率和符号数(生成树叶节点后即可完成)
int huffST_getSymFrequencies(SymbolFrequencies *SF, huffman_stat *st,int total_count)
{
    int i,count =0;
    for(i = 0; i < MAX_SYMBOLS; ++i)
    {   
        if((*SF)[i])
        {
            st->freq[i]=(float)(*SF)[i]->count/total_count;//计算每个符号的频率并赋值到结果表中
            count+=(*SF)[i]->count;//计算出现的信源符号总数
        }
        else 
        {
            st->freq[i]= 0;//没有出现过的信源符号频率为0
        }
    }
    if(count==total_count)
        return 1;
    else
        return 0;
}

step4 本实验的关键部分,将节点指针数组传入就能得到码字指针数组。生成码树的过程是自树叶到树根,用到排序函数,生成中间节点的函数,从树叶开始生成码树,再将码树的根节点传入编码的函数找到码树里的叶,调用为一片树叶进行编码的函数,从叶开始向根编码,因此靠近树叶的位在低字节,但是真正的huffman码字应该是从根向叶读,所以最后还要逆序。
calculate_huffman_codes函数

/* 生成码树并编码 */
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);//SFComp函数见下文
    //排列完后节点结构体指针数组下标不再是信源符号,空的结构体对应下标大,出现次数多的符号结构体对应下标大

#if 1 //排序后,按数组顺序打印每片树叶代表的信源符号和出现的次数
    printf("AFTER SORT\n");
    print_freqs(pSF);//排序后,打印信源符号及出现次数
#endif

    //计算非空节点数n
    for(n = 0; n < MAX_SYMBOLS && (*pSF)[n]; ++n)

    //循环n-1次,生成中间节点并建立节点间的相互关系
    for(i = 0; i < n - 1; ++i)//因为二叉树中度为0的节点比度为2的节点多1个,霍夫曼码树的非树叶节点度均为2,因此生成非树叶节点的个数为n-1
    {
        /* Set m1 and m2 to the two subsets of least probability. */
        m1 = (*pSF)[0];
        m2 = (*pSF)[1];

        /* Replace m1 and m2 with a set {m1, m2} whose probability
         * is the sum of that of m1 and m2. */
        (*pSF)[0] = m1->parent = m2->parent =new_nonleaf_node(m1->count + m2->count, m1, m2);
        //将出现序列中出现次数最少的两个符号的次数和、左右孩子作为参数建立新的非树叶节点(建立非叶节点的函数见下文)赋给这两个节点的父节点指针,并将此节点作为节点指针数组新的0号元素
        (*pSF)[1] = NULL;//已经加过的节点置空

        /* Put newSet into the correct count position in pSF. */
        qsort((*pSF), n, sizeof((*pSF)[0]), SFComp);//加入新的节点,再次排序生成新节点
    }

    /* Build the SymbolEncoder array from the tree. */
    pSE = (SymbolEncoder*)malloc(sizeof(SymbolEncoder));//为存放256个码字结构体开辟空间
    memset(pSE, 0, sizeof(SymbolEncoder));//将pse先全部置零

    //断点在调试程序里的作用~断点在此:(*pSF)[0]是根节点 pse是一个全为零的码字指针数组[256] 通过根节点能找到整棵树 并从树叶开始向上编码 这是关键的函数 有递归遍历 和编码 如果编码出错 可以进入里面看具体哪一步出错
    build_symbol_encoder((*pSF)[0], pSE);

    return pSE;//返回码字结构体指针数组
}

print_freqs函数

/* 打印256个信源符号及其出现次数 */
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");
    }
}

SFComp函数

/* 定义排序的标准:按符号出现次数升序排列256个码字结构体指针 */
static int SFComp(const void *p1, const void *p2)
{//void qsort ( void * base, size_t num, size_t size, int ( * comparator ) ( const void *, const void * ) )
    //将p1 p2强制类型转换为霍夫曼节点的2维指针 并将其第一个行指针赋给hn1 hn2
    const huffman_node *hn1 = *(const huffman_node**)p1;
    const huffman_node *hn2 = *(const huffman_node**)p2;

    /* Sort all NULLs to the end. */
    if(hn1 == NULL && hn2 == NULL)
        return 0;
    if(hn1 == NULL)
        return 1;
    if(hn2 == NULL)
        return -1;//空节点 没有出现过的字符都是数组下标大的元素

    if(hn1->count > hn2->count)
        return 1;
    else if(hn1->count < hn2->count)
        return -1;//因为要升序排列 所以对于出现次数count p1>p2返回正值 p1<p2返回负值

    return 0;
}

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;//父节点初始化为0
    return p;//返回一个已初始化的中间节点
}

build_symbol_encoder函数:关键部分的关键函数,调用了new_code函数,为一片树叶编码

/* 为每一个树叶编码,输入树根遍历码树找到树叶进行编码 */
static void build_symbol_encoder(huffman_node *subtree, SymbolEncoder *pSF)
{ 
    if(subtree == NULL)//如果节点为空 返回(为什么是根节点)
        return;

    if(subtree->isLeaf)//如果该节点是树叶
        //为该树叶编码,返回一个码字结构体
        (*pSF)[subtree->symbol] = new_code(subtree);//256个huffman_code的指针,下标是信源符号,此时信源符号将节点结构体和码字结构体联系起来
    else//为不是树叶的节点遍历左右孩子 递归调用 直到找到树叶 对其信源符号编码 所以此程序为中序遍历
    {
        build_symbol_encoder(subtree->zero, pSF);
        build_symbol_encoder(subtree->one, pSF);
    }
}

new_code函数:用到逆序函数

/* 为树叶代表的信源编码(编一个符号) */
static huffman_code* new_code(const huffman_node* leaf)//将树叶节点作为参数输入
{
    /* Build the huffman code by walking up to
     * the root node and then reversing the bits,
     * since the Huffman code is calculated by
     * walking down the tree. */
    unsigned long numbits = 0;//码长初始化为0
    unsigned char* bits = NULL;//初始化指向码字的指针
    huffman_code *p;//定义指向码字结构体的指针
    //leaf!=0表示当前字符存在 leaf->parent!=0表示当前在字符未编码完成 还没有寻到根节点(根节点没有父节点)
    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 we need another byte to hold the code,
           then allocate it. */
        if(cur_bit == 0)
        {
            size_t newSize = cur_byte + 1;
            //realloc在保持bits中原有数据不变的情况下,为其开辟newSize的空间,原来的数据存放在新空间的前面部分,地址可能发生变化
            bits = (unsigned char*)realloc(bits, newSize);
            bits[newSize - 1] = 0; /* Initialize the new byte.初始化新分配的 8bit 为 0*/
        }

        /* If a one must be added then or it in. If a zero
         * must be added then do nothing, since the byte
         * was initialized to zero. */
        if(leaf == parent->one)//如果是右孩子(若是左孩子因为初始化bits是0所以不用编)
            bits[cur_byte] |= 1 << cur_bit;//左移 1至当前byte的当前位(待编位) 与当前byte按位或 即把当前位置为1

        ++numbits;//码字位数加一
        leaf = parent;//把父节点作为下一个待编的 该编码过程是从树叶到树根的 高位是树根
    }

    if(bits)
        reverse_bits(bits, numbits);/* 整个码字逆序 逆序完了根节点编出码在低位(函数见下文)*/

    p = (huffman_code*)malloc(sizeof(huffman_code));
    p->numbits = numbits;//为码字结构体赋值
    p->bits = bits;/* 整数个字节 与numbits配合才可得到真正码字 */
    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);//临时存储码字 alloca开辟空间 用完马上自动释放
    unsigned long curbit;//当前字节当前位
    long curbyte = 0;

    memset(tmp, 0, numbytes);//把numbytes字节的tmp全置成0

    for(curbit = 0; curbit < numbits; ++curbit)
    {
        unsigned int bitpos = curbit % 8;//当前字节当前位

        if(curbit > 0 && curbit % 8 == 0)//位数凑满1字节 字节数加一
            ++curbyte;

        tmp[curbyte] |= (get_bit(bits, numbits - curbit - 1) << bitpos);//从编完码字的高位开始取,左移到当前位,与0或,达到反序的效果(get_bit函数见下文,取出该字节第numbits - curbit - 1位,生成最低位是该位其余为为0的一字节数)
    }
    //void *memcpy(void *dest, const void *src, size_t n) 把逆序的码字赋给bits
    memcpy(bits, tmp, numbytes);//从源src所指的内存地址的起始位置开始拷贝n个字节到目标dest所指的内存地址的起始位置中
}

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;//返回一个取出的第i位在字节最低位,其余全是0的unsigned char
}

step5 向输出信息中写入码长和码字
huffST_getcodeword函数

/* 写存储统计信息的结构体 */
//2、写码长和码字(编码完成过后写入)
int huffST_getcodeword(SymbolEncoder *se, huffman_stat *st)
{
    unsigned long i,j;

    for(i = 0; i < MAX_SYMBOLS; ++i)
    {
        huffman_code *p = (*se)[i];
        if(p)
        {
            unsigned int numbytes;
            st->numbits[i] = p->numbits;//把码字结构体里面的码字位数赋给输出信息的码字位数
            numbytes = numbytes_from_numbits(p->numbits);//位数变字节,用于下面索引到正确的码字
            for (j=0;j<numbytes;j++)
                st->bits[i][j] = p->bits[j];//把码字结构体里面的码字赋给输出信息的码字
        }
        else
            st->numbits[i] =0;//如果该码字结构体为空,则该符号没有在文件中出现,没有编码
    }

    return 0;
}

step6 将统计信息输出到一个txt文件
output_huffman_statistics函数

/* 输出统计信息表到文件 */
void output_huffman_statistics(huffman_stat *st,FILE *out_Table)
{
    int i,j;
    unsigned char c;
    fprintf(out_Table,"symbol\t   freq\t   codelength\t   code\n");//在输出文件中打印表头
    for(i = 0; i < MAX_SYMBOLS; ++i)
    {   
        fprintf(out_Table,"%d\t   ",i);//输出 符号的ASCII码十进制表示
        fprintf(out_Table,"%f\t   ",st->freq[i]);//输出 符号在输入文件中出现的频率
        fprintf(out_Table,"%d\t    ",st->numbits[i]);//输出 符号码字的码长
        if(st->numbits[i])//码长不为0 就输出码字
        {
            for(j = 0; j < st->numbits[i]; ++j)//循环取码字的每一位,从高到低输出到文件中
            {
                c =get_bit(st->bits[i], j);
                fprintf(out_Table,"%d",c);
            }
        }
        fprintf(out_Table,"\n");//打完一个符号的信息就换行打下一个
    }
}

step7 向输出文件中写码表
write_code_table函数

/* 写码表到输出文件 */
static int write_code_table(FILE* out, SymbolEncoder *se, unsigned int symbol_count)
{
    unsigned long i, count = 0;

    /* Determine the number of entries in se. */
    for(i = 0; i < MAX_SYMBOLS; ++i)
    {
        if((*se)[i])
            ++count;//根据非空码字结构体的多少来计算有多少个码字
    }

    /* Write the number of entries in network byte order. 将主机数转换成无符号长整形的网络字节顺序。*/
    i = htonl(count);    //在网络传输中,采用big-endian序,对于0x0A0B0C0D ,传输顺序就是0A 0B 0C 0D ,
    //因此big-endian作为network byte order,little-endian作为host byte order。
    //little-endian的优势在于unsigned char/short/int/long类型转换时,存储位置无需改变

    /* 码字总数写入输出文件 */
    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;
            /* Write the 1 byte symbol. */
            fputc((unsigned char)i, out);//写符号的ASCII码十进制
            /* Write the 1 byte code bit length. */
            fputc(p->numbits, out);//写码长
            /* Write the code bytes. */
            numbytes = numbytes_from_numbits(p->numbits);//位数变字节(上文提到此函数)
            //一次写一个字符的码字进去,因为长度设置的是该码字的字节数,fwrite的返回值为实际写入的数据项个数numbytes,所以当p不为空时,就会循环写入,return 1这条语句正常情况下永远不会执行,因此成功写入最后会return 0
            if(fwrite(p->bits, 1, numbytes, out) != numbytes)
                return 1;
        }
    }
    return 0;//返回标志位
}

step8 向输出文件写入编码后的数据
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];//以字符为下标索引到对应的码字结构体
        unsigned long i;

        for(i = 0; i < code->numbits; ++i)//按位取码字
        {
            /* Add the current bit to curbyte. */
            curbyte |= get_bit(code->bits, i) << curbit;//取码字

            /* If this byte is filled up then write it
             * out and reset the curbit and curbyte. */
            if(++curbit == 8)//将取出的码字以字节为单位写入,当前位不是一字节时,一样执行curbit加1操作,让下一次循环取码字能左移到下一位
            {
                fputc(curbyte, out);//码字写入输出文件
                curbyte = 0;//码字置零
                curbit = 0;//当前位置零
            }
        }
    }

    /*
     * If there is data in curbyte that has not been
     * output yet, which means that the last encoded
     * character did not fall on a byte boundary,
     * then output it.
     */
    if(curbit > 0)//当剩余未写入码字不够一字节,不能通过上面的循环中if写入,所以再补充一句,如果还有剩余比特未被写入,就继续写入输出文件
        fputc(curbyte, out);

    return 0;//写完输出文件返回标识0
}

step10 释放码树和码字结构体
free_huffman_tree函数

/* 释放码树 即释放节点结构体 传入根节点即可递归释放调整个码树*/
static void free_huffman_tree(huffman_node *subtree)
{
    if(subtree == NULL)//如果节点本就为空 不执行该函数直接返回
        return;

    if(!subtree->isLeaf)//对于非树叶节点找到左右孩子递归释放,从而释放整个码树,所以应该是叶子先被释放
    {
        free_huffman_tree(subtree->zero);
        free_huffman_tree(subtree->one);
    }
    free(subtree);//释放一个节点结构体的操作
}

free_encoder函数

/* 释放指向256个节点的指针数组 */
static void free_encoder(SymbolEncoder *pSE)
{
    unsigned long i;
    for(i = 0; i < MAX_SYMBOLS; ++i)
    {
        huffman_code *p = (*pSE)[i];//让p和数组中每一个指针指向相同
        if(p)
            free_code(p);//通过循环调用释放一个码字结构体的函数,来释放数组中每一个指针(函数见下文)
    }
    free(pSE);//最后释放指针数组
}

free_code函数

/* 释放一个码字结构体 */ 
static void free_code(huffman_code* p)
{
    free(p->bits);//先结构体中释放指向码字的指针
    free(p);//再释放结构体本身
}

三、实验结果及分析

1.依下图格式设定输入参数

这里写图片描述
2.测试十个不同类型的文件

这里写图片描述 

 3.整理程序运行输出的统计信息
把输出的txt文件用excel打开:E列是用excel 计算的码长* 概率,其累加和为平均码长;F列是用excel 计算的每个符号的 自信息 *概率,其累加和为信源熵;F列除以E列是编码效率

这里写图片描述

 4.把信源符号概率制成图表

这里写图片描述
这里写图片描述

 这里写图片描述

 这里写图片描述

这里写图片描述 

这里写图片描述 

这里写图片描述 

这里写图片描述 

这里写图片描述 

这里写图片描述 

 5.统计分析huffman编码的效果

从上表可以看出:

除了第一个doc文件,其他文件平均码长都接近信源熵,与香农第一定理符合,码长的下限是信源熵,huffman码是紧致码,编码效率都在0.99以上
除了doc、ppt文件,其他文件压缩比都接近于1,对于本身就是已压缩文件的输入文件(rar、png、jpg等),从概率分布图也可以看出,信源符号接近等概,可压缩的空间不多,加之又在输出文件中附加码表,导致压缩比小不大

原文链接:https://blog.csdn.net/youyangstrong/article/details/70990277

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值