数据压缩三-Huffman编码与解码

来源:互联网 发布:播放软件大全排行 编辑:程序博客网 时间:2024/05/22 17:19

一.实验原理

1.Huffman编码

哈夫曼编码(Huffman Coding),又称霍夫曼编码,是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,概率大的符号码长短,概率小的符号码长长,是一种无失真编码。有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。1.哈夫曼编码的方法:

(1)统计所有符号发生的概率

(2)按照概率从小到大对符号进行排序

(3每一次选出最小的两个值,作为二叉树的两个叶子节点,将和作为它们的根节点,这两个叶子节点不再参与比较,新的根节点参与比较;

4重复3,直到最后得到和为1的根节点;

5将形成的二叉树的左节点标0,右节点标1,把从最上面的根节点到最下面的叶子节点途中遇到的01序列串起来,就得到了各个符号的编码。

2.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;          };          unsigned char symbol;  //如果是叶节点,表示是某个信源符号    };  } huffman_node;  

(2)Huffman码字结构:

typedef struct huffman_code_tag  //码字数据类型{      /* 码字长度(以bit为单位) */      unsigned long numbits;        /* 码字,码字第1位存于bits[0]第1位,              码字第2位存于bits[0]第2位,              码字第8位存于bits[0]第8位,              码字第9位存于bits[1]第1位。*/      unsigned char *bits;   //指向该码比特串的指针} huffman_code;  
二.Huffman编码流程


 1.读入待编码的源文件

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",//读内存            "-t - output huffman statistics\n",//输出统计表格            out);  }    int  main(int argc, char** argv)  {      char memory = 0;//为0时进行文件的编解码,为1时进行内存的编解码      char compress = 1;//compress为0时解码,为1时编码      int opt;//选项,getopt()的返回值,getopt()处理'-'开头的命令行参数      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        /*获取命令行参数选项*/      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;              break;          case 'd'://d为解压              compress = 0;              break;          case 'h'://h为输出参数使用方法              usage(stdout);              return 0;          case 'v'://v为输出版本信息              version(stdout);              return 0;          case 'm':              memory = 1;  //m表示对内存数据进行操作            break;          // by yzhang for huffman statistics          case 't'://t表示输出中间数据            file_out_table = optarg;                          break;          //end by yzhang          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;          }      }        //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;          }      }  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);      return 0;  }  
#define MAX_SYMBOLS 256  typedef huffman_node* SymbolFrequencies[MAX_SYMBOLS];//信源符号频率  typedef huffman_code* SymbolEncoder[MAX_SYMBOLS];//信源符号码字  
2.第一次扫描:统计文件中各个字符出现的频率(因为是8比特,共256个信源符号所以创建一个256个元素的指针数组)
static unsigned int  get_symbol_frequencies(SymbolFrequencies *pSF, FILE *in)//第一次扫描,统计信源符号出现的频率  {      int c;      unsigned int total_count = 0;//总信源符号数初始化为0            //将所有信源符号地址初始化为NULL(0)     init_frequencies(pSF);            /* 计算每个符号的频率(第一次扫描文件) */      while((c = fgetc(in)) != EOF)//如果符号存在      {          unsigned char uc = c;          if(!(*pSF)[uc])//如果为新符号,则产生该字符的一个新叶节点              (*pSF)[uc] = new_leaf_node(uc);          ++(*pSF)[uc]->count;//如果已经是叶节点,当前字符出现频率+1          ++total_count;//总信源符号数+1      }        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;//是叶节点则isLeaf=1      p->symbol = symbol;//叶节点代表的信源符号      p->count = 0;//信源符号数为0    p->parent = 0;//未知父节点      return p;  }  

3.建立Huffman树:(按频率从小到大排序)

1.static SymbolEncoder*  2.calculate_huffman_codes(SymbolFrequencies * pSF)//按频率从小到大建立huffman树  3.{  4.    unsigned int i = 0;  5.    unsigned int n = 0;  6.    huffman_node *m1 = NULL, *m2 = NULL;  7.    SymbolEncoder *pSE = NULL;  8. //将信源符号按出现频率大小排序.小概率符号在前  9.    qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp);//SFComp选择升序排序方式   10.   11.    //得到当前文件中信源符号的种类总数 ,因不一定需要256种信源符号12.    for(n = 0; n < MAX_SYMBOLS && (*pSF)[n]; ++n)  ;  13.  14./* 构建霍夫曼树。需要合并n-1次,所以循环n-1次。 15.    该代码基于Ian Witten等人写的第2版 Managing Gigabytes 的第34页中的算法。 注意的事,此实现使用简单计数而不是概率 */ 16.    for(i = 0; i < n - 1; ++i)  17.    {  18.        /* m1 m2置为当前频率最小的两个信源符号 */  19.        m1 = (*pSF)[0];  20.        m2 = (*pSF)[1];  21.  22.        /* m1 m2合并为1个huffman结点加入到数组中,左右孩子分别置为m1 m2的地址,频率为m1 m2频率之和 */  23.        (*pSF)[0] = m1->parent = m2->parent =  24.            new_nonleaf_node(m1->count + m2->count, m1, m2);  25.        (*pSF)[1] = NULL;  26.          27.        /* m1 m2合并后重新排列 */  28.        qsort((*pSF), n, sizeof((*pSF)[0]), SFComp);  29.    }  30.  31.    /* 由huffman树计算码字 */  32.    pSE = (SymbolEncoder*)malloc(sizeof(SymbolEncoder));  33.    memset(pSE, 0, sizeof(SymbolEncoder));  34.    build_symbol_encoder((*pSF)[0], pSE);  35.    return pSE;  

上述代码中利用qsort()函数进行快速的升序排序,对SFComp的作用进行分析:

/* qsort函数使用到的比较函数SFComp,对数组进行从小到大排序 */ 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;        /* 把所有NULL排到最后 */      if(hn1 == NULL && hn2 == NULL)//两结点都为空,返回0 ,表示相等          return 0;      if(hn1 == NULL)// 返回1,表示第二个节点大         return 1;      if(hn2 == NULL)// 返回-1,表示第一个节点大          return -1;      //如果两个节点都不为空,那么进行比较      if(hn1->count > hn2->count)          return 1;      else if(hn1->count < hn2->count)          return -1;        return 0;  }  

4.递归遍历Huffman树,根据huffman树计算码字:

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);      }  }  

5.产生码字:(从霍夫曼树中的叶子构建一个码字因为霍夫曼编码是从上往下顺序进行,所以通过从子节点从下往上走到根节点然后反转位来构建huffman代码 )

static huffman_code*  new_code(const huffman_node* leaf)  {      unsigned long numbits = 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); //确定所编bit在当前byte中的位置,因为数组从0-7,刚好用求余来表示       unsigned long cur_byte = numbits / 8;//计算码字的字节数     /* 如果需要另一个字节去保存码字,则进行再分配 */          if(cur_bit == 0)          {              size_t newSize = cur_byte + 1;              bits = (char*)realloc(bits, newSize);//realloc与malloc不同,它在保持原有数据不变的情况下重新分配新的空间,原有数据在新空间的前面部分              bits[newSize - 1] = 0; /* 初始化新分配的8bit为0 */          }         //判断是否为右子节点,Huffman树左0右1         if(leaf == parent->one)              bits[cur_byte] |= 1 << cur_bit;//左移1至当前byte的当前位(先左移后或操作)            ++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;//返回码字  }  

6.码字逆序变换:

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;//当前Byte            memset(tmp, 0, numbytes);        for(curbit = 0; curbit < numbits; ++curbit)      {          unsigned int bitpos = curbit % 8;// 判断当前位在字节中的位数          if(curbit > 0 && curbit % 8 == 0)            ++curbyte;  //当bit数超出8 那么字节数加1        //从后往前取码字中的每一位,再移位到所在字节的正确位置           tmp[curbyte] |= (get_bit(bits, numbits - curbit - 1) << bitpos);      }        memcpy(bits, tmp, numbytes);//反转结果复制到bits中  }  

7.化bit为byte

static unsigned char  get_bit(unsigned char* bits, unsigned long i)//提取第i位,第i位所在字节数i/8,当前字节的位数i%8,此位右移i%8变换到最低位,与1做与提取此位  {      return (bits[i / 8] >> i % 8) & 1;  }  

8.将Huffman码表等信息写入输出文件:

static int  write_code_table(FILE* out, SymbolEncoder *se, unsigned int symbol_count)  {      unsigned long i, count = 0;            /* 统计se中字符种类个数 */      for(i = 0; i < MAX_SYMBOLS; ++i)      {          if((*se)[i])              ++count;      }         /* 把字节种类数和字节总数以网络传输中的顺序写入文件中 */      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;        /* Write the number of bytes that will be encoded. */      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;  }  
9.再次扫描文件,进行Huffman编码,并输出:
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)//将码字写入文件          {             //把当前比特位加到编码字节的相应位置              curbyte |= get_bit(code->bits, i) << curbit;              //如果该字节被填满,则写入该字节并重新设置curbit和curbyte。             if(++curbit == 8)              {                  fputc(curbyte, out);                  curbyte = 0;                  curbit = 0;              }          }      }       //如果有数据未输出,则表示最后一个编码字符未落在字节边界上,然后输出。    if(curbit > 0)          fputc(curbyte, out);        return 0;  }  

三.Huffman解码流程:


1.读取码表并据此建立Huffman树:

static huffman_node*  read_code_table(FILE* in, unsigned int *pDataBytes)  {      huffman_node *root = new_nonleaf_node(0, NULL, NULL);      unsigned int count;            //得到码表中的符号数     if(fread(&count, sizeof(count), 1, in) != 1)      {          free_huffman_tree(root);          return NULL;      }        count = ntohl(count);        //读取此编码表示的数据字节数       if(fread(pDataBytes, sizeof(*pDataBytes), 1, in) != 1)      {          free_huffman_tree(root);          return NULL;      }        *pDataBytes = ntohl(*pDataBytes);         //读取码表,由符号、码长、码字三部分组成      while(count-- > 0)     {          int c;          unsigned int curbit;          unsigned char symbol;          unsigned char numbits;          unsigned char numbytes;          unsigned char *bytes;          huffman_node *p = root;                    if((c = fgetc(in)) == EOF)          {              free_huffman_tree(root);              return NULL;          }          symbol = (unsigned char)c;                    if((c = fgetc(in)) == EOF)          {              free_huffman_tree(root);              return NULL;          }                    numbits = (unsigned char)c;//码长          numbytes = (unsigned char)numbytes_from_numbits(numbits); //计算保存一个码长所需要的字节数          bytes = (unsigned char*)malloc(numbytes);//为读取码字分配空间          if(fread(bytes, 1, numbytes, in) != numbytes)//读取码字          {              free(bytes);              free_huffman_tree(root);              return NULL;          }            //读取当前码字的每一位,并依据读取结果逐步建立由根节点至该符号叶节点的路径          for(curbit = 0; curbit < numbits; ++curbit)          {              if(get_bit(bytes, curbit))//当前读取位是否为1              {//若为1                  if(p->one == NULL)                  {                      //是否是当前码字的最后一位,是则建立新的叶节点,否则新建非叶节点                      p->one = curbit == (unsigned char)(numbits - 1)                          ? new_leaf_node(symbol)                          : new_nonleaf_node(0, NULL, NULL);                      p->one->parent = p;//‘1’的一支的父节点指向当前节点                  }                  p = p->one;//沿'1'的方向下移一级              }              else              {//当前读取位为0                  if(p->zero == NULL) //建立左子节点                 {                    p->zero = curbit == (unsigned char)(numbits - 1)                          ? new_leaf_node(symbol)                          : new_nonleaf_node(0, NULL, NULL);                      p->zero->parent = p;                  }                  p = p->zero;              }          }                    free(bytes);      }        return root;//返回Huffman树的根节点 ,才可以进行之后的遍历  }  
2.读取Huffman码字并利用Huffman树进行输出:
int  huffman_decode_file(FILE *in, FILE *out)  {      huffman_node *root, *p;      int c;      unsigned int data_count;           //读取码表      root = read_code_table(in, &data_count);      if(!root)          return 1;//建立Huffman树失败        // 文件解码    p = root;      while(data_count > 0 && (c = fgetc(in)) != EOF)      {          //data_count>0:逻辑上仍有数据;(c = fgetc(in)) != EOF):文件中仍有数据          unsigned char byte = (unsigned char)c;//1byte的码字          unsigned char mask = 1;//mask遮罩,逐位读取码字          while(data_count > 0 && mask)          {              //走Huffman树,按照当前码字为0还是1选择子树的走向              p = byte & mask ? p->one : p->zero;              mask <<= 1;//循环一次左移1位                if(p->isLeaf)//遇到叶节点表示解码完毕              {                  fputc(p->symbol, out);//输出叶节点对应的信源符号                  p = root;//回到根节点解下一个码字                  --data_count;//未解码字数-1              }          }      }        free_huffman_tree(root);      return 0;  }  
四.实验结果

不同类型文件压缩结果:


不同类型文件的概率统计:
















0 0