热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Huffman算法原理及代码实现

Huffman算法原理前缀码在一个字符集中,任何一个字符的编码都不是另一个字符编码的前缀,即前缀码。例如,有两个码字111与1111&#x

Huffman 算法原理

前缀码

在一个字符集中,任何一个字符的编码都不是另一个字符编码的前缀,即前缀码。 例如,有两个码字 111 1111,那么这两个码字就不符合前缀码的规则,因为 111 是

1111 的前缀。放到二叉树里来讲,只用叶子节点编码的码字才是前缀码,如果同时使用 中间节点和叶子节点编码,那结果就不是前缀码。因为压缩中经过编码的码字全部是前 缀码,所以在对照码表解压的时候,碰到哪个码字就是哪个码字,不用担心出现某个字 符的编码是另一个字符的编码的前缀的情况,该意识一定要具备。

关于前缀码,下面一段话摘自《算法导论》:“前缀码的作用是简化解码过程。由于 没有码字是其他码字的前缀,编码文件的开始码字是无歧义的。我们可以简单的识别出 开始码字,将其转换会原字符,然后对编码文件剩余部分重复这种解码过程。

1111111     ? 111 1111 ? 1111 111

 

哈夫曼编码

 

哈夫曼设计了一个贪心算法来构造最优前缀码,被称为哈夫曼编码(Huffman code), 其正确性证明依赖于贪心选择性质和最优子结构。哈夫曼编码可以很有效的压缩数据,

 

具体压缩率依赖于数据本身的特性。这里我们先介绍几个概念:码字、码字长度、定长 编码与变长编码

每个字符可以用一个唯一的二进制串表示,这个二进制串称为这个字符的码字,这 个二进制串的长度称为这个码字的码字长度。码字长度固定就是定长编码,码字长度不 同则为变长编码。变长编码可以达到比定长编码好得多的压缩率,其思想是赋予高频字 符(出现频率高的字符)短(码字长度较短)码字,赋予低频字符长码字。例如,我们 用 ASCII 字符编辑一个文本文档,不论字符在整个文档中出现的频率,每个字符都要占 用一个字节;如果我们使用变长编码的方式,每个字符因在整个文档中的出现频率不同 导致码字长度不同,有的可能占用一个字节,而有的可能只占用一比特,这个时候,整 个文档占用空间就会比较小了。当然,如果这个文本文档相当大,导致每个字符的出现 频率基本相同,那么此时所谓变长编码在压缩方面的优势就基本不存在了(这点要十分 明确,这是为什么压缩要分块的原因之一,后续源码分析会详细讲解)。

哈夫曼编码会自底向上构造出一棵对应最优编码的二叉树,我们使用下面这个例子 来说明哈夫曼树的构造过程。首先,我们已知在某个文本中有如下字符及其出现频率,

 

字符

a

b

c

d

e

f

出现频率

45

13

12

16

9

5

构造过程如下图所示:

 

 

 

 

图 1 到图 6 列除了整个哈夫曼树构造过程中的每个步骤。在一开始,每个字符都已 经按照出现频率大小排好顺序,在后续的步骤中,每次都将频率最低的两棵树合并,然 后用合并后的结果再次排序(注意,排序不是目的,目的是找到这时出现频率最低的两 项,以便下次合并。gzip 源码中并没有专门去“排序”,而是使用专门的数据结构把频率 最低的两项找到即可)。叶子节点用矩形表示,每个叶子节点包含一个字符及其频率。中 间节点用圆圈表示,包含其孩子节点的频率之和。中间节点指向左孩子的边标记为 0, 指向右孩子的边标记为 1。一个字符的码字对应从根到其叶节点的路径上的边的标签序 列。图 1 为初始集合,有六个节点,每个节点对应一个字符;图 2 到图 5 为中间步骤,

图 6 为最终哈夫曼树。此时每个字符的编码都是前缀码。

 

 

哈夫曼压缩和解压文件

利用库中的优先级队列实现哈夫曼树,最后基于哈夫曼树最终实现文件压缩。

1.统计文件中字符出现的次数,利用优先级队列构建 Haffman 树,生成 Huffman 编 码。构造过程可以使用 priority_queue 辅助,每次 pq.top()都可以取出权值(频数)最小 的节点。每取出两个最小权值的节点,就 new 出一个新的节点,左右孩子分别指向它 们。然后把这个新节点 push 进优先队列。

2.压缩:利用 Haffman 编码对文件进行压缩,即在压缩文件中按顺序存入每个字符 的 Haffman 编码。 码表(实际存储是对应数值的概率,然后调用程序生成码表) + 编码

 

 

3.将文件中出现的字符以及它们出现的次数写入配置文件中,以便后续压缩使用。

4.解压缩:利用配置文件重构 Haffman 树,对文件进行减压缩。

HuffmanTree.hpp

#ifndef _HUFFMAN_TREE_H_
#define _HUFFMAN_TREE_H_
#include
#include
#include
#include
using namespace std;template
struct HuffmanTreeNode
{HuffmanTreeNode(const W &weight): _pLeft(NULL), _pRight(NULL), _pParent(NULL), _weight(weight){}HuffmanTreeNode*_pLeft;HuffmanTreeNode*_pRight;HuffmanTreeNode*_pParent;W _weight;
};template
class HuffmanTree
{typedef HuffmanTreeNode*PNode;
public:HuffmanTree(): _pRoot(NULL){}HuffmanTree(W*array, size_t size, const W&invalid){_CreateHuffmantree(array, size, invalid);}void _Destroy(PNode&pRoot){//后序if (pRoot){_Destroy(pRoot->_pLeft);_Destroy(pRoot->_pRight);delete pRoot;pRoot = NULL;}}~HuffmanTree(){_Destroy(_pRoot);}PNode GetRoot(){return _pRoot;}
private: //构建哈夫曼树void _CreateHuffmantree(W*array, size_t size, const W&invalid){struct PtrNodeCompare{bool operator()(PNode n1, PNode n2)//重载“()”{return n1->_weight _weight;}};priority_queue, PtrNodeCompare>hp; // 最大堆for (size_t i = 0; i (array[i])); // 数据入堆}}//空堆if (hp.empty())_pRoot = NULL;while (hp.size()>1){PNode pLeft = hp.top();hp.pop();PNode pRight = hp.top();hp.pop();PNode pParent = new HuffmanTreeNode(pLeft->_weight + pRight->_weight);//左加右的权值,作为新节点pParent->_pLeft = pLeft;pLeft->_pParent = pParent;pParent->_pRight = pRight;pRight->_pParent = pParent;hp.push(pParent);}_pRoot = hp.top(); // 节点在堆}public:PNode _pRoot;
};
#endif

 FileCompress.hpp

/*利用库中的优先级队列实现哈夫曼树,最后基于哈夫曼树最终实现文件压缩。
描述: 1.统计文件中字符出现的次数,利用优先级队列构建Haffman树,生成Huffman编码。 构造过程可以使用priority_queue辅助,每次pq.top()都可以取出权值(频数)最小的节点。每取出两个最小权值的节点,就new出一个新的节点,左右孩子分别指向它们。然后把这个新节点push进优先队列。 2.压缩:利用Haffman编码对文件进行压缩,即在压缩文件中按顺序存入每个字符的Haffman编码。 3.将文件中出现的字符以及它们出现的次数写入配置文件中,以便后续压缩使用。 4.解压缩:利用配置文件重构Haffman树,对文件进行减压缩。
*/
#ifndef _FILE_COMPRESS_H_
#define _FILE_COMPRESS_H_
#include "HuffmanTree.hpp"
#include
#include
#include
#include
using namespace std;unsigned long getFileSize(const char *path)
{unsigned long filesize = -1;FILE *fp;fp = fopen(path, "r");if(fp == NULL)return filesize;fseek(fp, 0L, SEEK_END);filesize = ftell(fp);fclose(fp);return filesize;
}typedef unsigned int CountType;
typedef unsigned char CharType;
struct CharInfo
{CharType _ch;//字符CountType _count;//次数string _code;//编码bool operator!&#61;(const CharInfo &info){return _count !&#61; info._count;}CharInfo operator&#43;(const CharInfo &info){CharInfo ret;ret._count &#61; _count &#43; info._count;return ret;}bool operator<(const CharInfo &info){return _count > info._count;}
};class FileCompress
{typedef HuffmanTreeNode Node;struct TmpInfo{CharType _ch;//字符CountType _count;//次数};public://构造函数FileCompress(){for (size_t i &#61; 0; i <256; &#43;&#43;i){_infos[i]._ch &#61; i;_infos[i]._count &#61; 0;}}//获取哈夫曼编码void GenerateHuffmanCode(Node *root, string code) {if (root &#61;&#61; NULL)return;//前序遍历生成编码if (root->_pLeft &#61;&#61; NULL && root->_pRight &#61;&#61; NULL){_infos[(unsigned char)root->_weight._ch]._code &#61; code;return;}GenerateHuffmanCode(root->_pLeft, code &#43; &#39;0&#39;);GenerateHuffmanCode(root->_pRight, code &#43; &#39;1&#39;);}// 真正的压缩算法窗口是32k, 我们这里直接对所有数据进行编码 void Compress(const char *file) //file:源文件{unsigned long fileSize &#61; getFileSize(file);//1.统计字符出现的次数FILE *fout &#61; fopen(file, "rb");assert(fout);char ch &#61; fgetc(fout);while (feof(fout) &#61;&#61; 0) //如文件结束&#xff0c;则返回值为1&#xff0c;否则为0{_infos[(unsigned char)ch]._count&#43;&#43;; // 计算对应字符出现的频率ch &#61; fgetc(fout);}//2.生成Huffmantree 及codeCharInfo invalid;invalid._count &#61; 0;// 2.1 生成Huffmantree&#xff0c; 构建哈夫曼树HuffmanTree tree(_infos, 256, invalid); //参数&#xff1a;数组&#xff0c;256个&#xff0c;无效值&#xff08;出现0次&#xff09;string compressfile &#61; file; //compressfile &#43;&#61; ".huffman"; //?FILE *fin &#61; fopen(compressfile.c_str(), "wb"); //打开压缩文件assert(fin);string code;// 2.2 生成codeGenerateHuffmanCode(tree.GetRoot(), code);//3.0 写入字符出现的信息int writeNum &#61; 0;int objSize &#61; sizeof(TmpInfo);for (size_t i &#61; 0; i <256; &#43;&#43;i) // 这里实质是讲码表存储到文件的头部{if (_infos[i]._count > 0) {TmpInfo info;info._ch &#61; _infos[i]._ch;info._count &#61; _infos[i]._count;printf("codec ch:%u, cout:%u\n", (unsigned char)info._ch, info._count);fwrite(&info, objSize, 1, fin); // 将对应的码流信息写入writeNum&#43;&#43;;}}TmpInfo info;info._count &#61; -1;printf("code objSize:%d\n", objSize);fwrite(&info, objSize, 1, fin); //把info._count &#61; -1写进去作为结束标志位//3.压缩int writeCount &#61; 0;int readCount &#61; 0;fseek(fout, 0, SEEK_SET); //文件指针、偏移量、参照位置ch &#61; fgetc(fout);readCount&#43;&#43;;unsigned char value &#61; 0;size_t pos &#61; 0;while (feof(fout) &#61;&#61; 0) // 一个个字符读取,效率虽然低一些&#xff0c;但不影响实验{// 读取数据&#xff0c;查找对应编码string &code &#61; _infos[(unsigned char)ch]._code; // 查找对应的编码printf("code[%d]:%u:%s\n", readCount, (unsigned char)ch, code.c_str());// code 实际存储的是对应 某个字符的哈夫曼编码for (size_t i &#61; 0; i 0){writeCount&#43;&#43;;fputc(value, fin); //写入压缩文件&#xff08;fin&#xff09;}printf("huffman code table size::%d\n", objSize * (writeNum &#43; 1));printf("compress file data size:%d\n", writeCount);unsigned long totalSize &#61; writeCount &#43; objSize * (writeNum &#43; 1);printf("compress file total size:%lu, orign file size:%lu, ratio:%0.2f\n",totalSize, fileSize, (float)(totalSize*1.0/fileSize));fclose(fout);fclose(fin);}void Uncompress(const char *file){string uncompressfile &#61; file; //file:Input.txt.huffmansize_t pos &#61; uncompressfile.rfind(&#39;.&#39;); //找到倒数第一个&#39;.&#39;assert(pos !&#61; string::npos);uncompressfile.erase(pos); //删除掉&#39;.&#39;后面字符串uncompressfile &#43;&#61; ".unhuffman"; //Input.txt&#43;&#39;.unhuffman&#39;FILE *fin &#61; fopen(uncompressfile.c_str(), "wb"); //打开解压缩文件assert(fin);FILE *fout &#61; fopen(file, "rb"); //打开压缩文件assert(fout);//3.0 读入字符出现的信息TmpInfo info;int cycleNum &#61; 1;int objSize &#61; sizeof(TmpInfo);fread(&info, objSize, 1, fout);while (info._count !&#61; -1) //-1为结束标志 {// printf("decodec ch:%u, cout:%u\n", (unsigned char)info._ch, info._count);_infos[(unsigned char)info._ch]._ch &#61; info._ch;_infos[(unsigned char)info._ch]._count &#61; info._count;fread(&info, objSize, 1, fout);cycleNum&#43;&#43;;}int aaa &#61; 0;//重建huaffman树CharInfo invalid;invalid._count &#61; 0;HuffmanTree tree(_infos, 256, invalid); //参数&#xff1a;数组&#xff0c;256个&#xff0c;无效值&#xff08;出现0次&#xff09;Node *root &#61; tree.GetRoot();Node *cur &#61; root;CountType n &#61; root->_weight._count; //所有叶子节点的和&#xff08;源文件字符的个数&#xff09;char ch &#61; fgetc(fout); //从fout(压缩文件)读字符int readCount &#61; 0;while (ch !&#61; EOF || n > 0){for (size_t i &#61; 0; i <8; &#43;&#43;i){if ((ch & (1 <_pLeft; // 往左边找elsecur &#61; cur->_pRight; // 往右边找if (cur->_pLeft &#61;&#61; NULL && cur->_pRight &#61;&#61; NULL){//cout <_weight._ch;readCount&#43;&#43;;if(readCount % 1024 &#61;&#61; 0) {// printf("uncompress %dk data\n", readCount/1024);}fputc(cur->_weight._ch, fin); //fin解压缩文件cur &#61; root;if (--n &#61;&#61; 0)break;}}ch &#61; fgetc(fout);}printf("uncompress end\n");fclose(fin);fclose(fout);}protected:CharInfo _infos[256];
};#endif

main.cpp

 

/** &#64;Author: your name* &#64;Date: 2020-06-04 16:21:01* &#64;LastEditTime: 2020-06-04 19:13:01* &#64;LastEditors: Please set LastEditors* &#64;Description: In User Settings Edit* &#64;FilePath: \huffman\main.cpp*/
/*利用库中的优先级队列实现哈夫曼树&#xff0c;最后基于哈夫曼树最终实现文件压缩。
描述&#xff1a; 1.统计文件中字符出现的次数&#xff0c;利用优先级队列构建Haffman树&#xff0c;生成Huffman编码。 构造过程可以使用priority_queue辅助&#xff0c;每次pq.top()都可以取出权值&#xff08;频数&#xff09;最小的节点。每取出两个最小权值的节点&#xff0c;就new出一个新的节点&#xff0c;左右孩子分别指向它们。然后把这个新节点push进优先队列。 2.压缩&#xff1a;利用Haffman编码对文件进行压缩&#xff0c;即在压缩文件中按顺序存入每个字符的Haffman编码。 3.将文件中出现的字符以及它们出现的次数写入配置文件中&#xff0c;以便后续压缩使用。 4.解压缩&#xff1a;利用配置文件重构Haffman树&#xff0c;对文件进行减压缩。
*/#include "FileCompress.hpp"
#include
#include
#include
#include // 注意是用g&#43;&#43;编译器
// 编译 g&#43;&#43; -o huffman main.cpp HuffmanTree.hpp FileCompress.hpp -std&#61;c&#43;&#43;11
int main(int argc, char** argv)
{if (argc !&#61; 2){std::cout <<"usage: " < " <}

g&#43;&#43; -o huffman main.cpp HuffmanTree.hpp FileCompress.hpp  -std&#61;c&#43;&#43;11

 


推荐阅读
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 本文介绍了SPOJ2829题目的解法及优化方法。题目要求找出满足一定条件的数列,并对结果取模。文章详细解释了解题思路和算法实现,并提出了使用FMT优化的方法。最后,对于第三个限制条件,作者给出了处理方法。文章最后给出了代码实现。 ... [详细]
  • 广度优先遍历(BFS)算法的概述、代码实现和应用
    本文介绍了广度优先遍历(BFS)算法的概述、邻接矩阵和邻接表的代码实现,并讨论了BFS在求解最短路径或最短步数问题上的应用。以LeetCode中的934.最短的桥为例,详细阐述了BFS的具体思路和代码实现。最后,推荐了一些相关的BFS算法题目供大家练习。 ... [详细]
  • 查找给定字符串的所有不同回文子字符串原文:https://www ... [详细]
  • C++ STL复习(13)容器适配器
    STL提供了3种容器适配器,分别为stack栈适配器、queue队列适配器以及priority_queue优先权队列适配器。不同场景下,由于不同的序列式 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • AFNetwork框架(零)使用NSURLSession进行网络请求
    本文介绍了AFNetwork框架中使用NSURLSession进行网络请求的方法,包括NSURLSession的配置、请求的创建和执行等步骤。同时还介绍了NSURLSessionDelegate和NSURLSessionConfiguration的相关内容。通过本文可以了解到AFNetwork框架中使用NSURLSession进行网络请求的基本流程和注意事项。 ... [详细]
  • 深入理解计算机系统之链接(一)
    程序是怎样运行的写好的c程序怎样运行的呢?答案是一个写好的程序要先经过语言预处理器,编译器,汇编器和链接器生成最后的可执行文件,然后加载器将可执行文件加载到内存中才能运行。这里以一 ... [详细]
  • c语言 怎么访问64位地址_C语言调动硬件的原理是什么?
    大家都知道我们可以使用C语言写一段程序来控制硬件工作,但你知道其工作原理吗?1c语言在实际运行中,都是以汇编指令的方式运行的,由编译器把C ... [详细]
  • 点击打开链接去除换行updatezhzl_addresstsett.add_administration_numreplace(t.add_administration_num,chr(10 ... [详细]
author-avatar
西红柿
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有