这个是代码是昨天写完的,一开始的时候还出了点小bug,这个bug在晚上去吃饭的路上想明白的,回来更改之后运行立刻完成最后一步,大获成功。

简单说下huffman编码和文件压缩主要的技术。

Huffman编码,解码:

I 创建Huffman树

II 根据Huffman树实现编码,并将编码结果和要编码的数据建立映射关系。

III Huffman解码,也就是根据获取的Huffman码来逆向获取解码信息,而且你从解压文件中一次性获取的数据是一个很长的字符串,没有预处理好的成段字符串式Huffman码。1

I 首先,如何创建Huffman树?

在这个我在前天的那篇文章中简单的提了一下,现在好好说一下。如果你不知道什么是Huffman树,请google之~

对于获取到的文件,首先要做的就是,建立一个长度为256的int数组,全部置零,然后以字节流的形式读取文件,并对字节流中的字节出现次数进行统计,方法就是以字节数值为数组偏移地址,对应的数组元素进行+1操作。另外这里需要提一下的就是,用于存储文件字节流的缓冲区最好是unsigned char类型,因为这样能直接使用,如果是char的,在转化为int类型的时候,一旦数值大于127,因为补码问题,你就直接乘上了通往未知数值的高铁~

完成统计之后,将这个数组中出现次数不为0的元素添加对应大小的二叉树节点数组中,然后以出现次数为Key值,进行排序。

在排序完成之后,就能开始构建Huffman树了。操作如下:

1 如果数组中元素个数不为1,将前两个元素构造为一个临时节点的子树,此时临时节点的Key值为两个元素Key值之和,然后删除数组中的第一个元素(从数组中删除),再将临时节点赋值给当前数组的第一个元素。

(其实就是将前两个元素添加到一个临时节点的左右根节点,然后在原数组中删除这两个元素,接着再将这个临时节点插入到数组头部,充当新的节点。上面的那段描述我觉得说的不是很清楚,但是那个是我在代码中发现的一个可以优化的地方,减少了一个元素的删除操作)

2 此时数组依据key值的排序很有可能已经不再有序,而又因为仅有一个乱序元素,所以专门设计了一个函数,一次完成排序,效率,应该是最高的了。重复1

这样当数组中只有1个元素的时候,就是Huffman树的根节点了。

这样,Huffman树的构造就完成了。我上面说的可能不是很清楚,你看了之后可能会有疑问,所以我在这贴下部分代码,你可以看一下,就是这么简单,而且很巧妙。

Huffman树节点,一开始就是一个Struct,但是因为涉及到了STL,所以添加了方法

 struct HaffmanStruct
{
//a small structure
HaffmanStruct():val(),ncounts(),lNext(NULL),rNext(NULL){}
bool operator < (HaffmanStruct &);
bool operator > (HaffmanStruct &);
void Reset();
unsigned char val;
unsigned int ncounts;
char HuffmanCode[];
//used for tree
HaffmanStruct * lNext;
HaffmanStruct * rNext;
};

给他一个数组,他给你一颗Huffman树

 void HuffManEncode(vector<HaffmanStruct> & vecValidNumberArray)
{
HaffmanStruct ValidStruct;//temporary struct
//Analysis
while(vecValidNumberArray.size() != )
{
ValidStruct.Reset();
ValidStruct.ncounts = vecValidNumberArray[].ncounts + vecValidNumberArray[].ncounts;
ValidStruct.lNext = new HaffmanStruct;
*ValidStruct.lNext = vecValidNumberArray[];
ValidStruct.rNext = new HaffmanStruct;
*ValidStruct.rNext = vecValidNumberArray[];
vecValidNumberArray.erase(vecValidNumberArray.begin());
vecValidNumberArray[] = ValidStruct;
SingleSort(&vecValidNumberArray[], vecValidNumberArray.size(), );
}
}

以上就是Huffman树构造的全部过程。

II 根据Huffman树获取Huffman编码

对树最有效的访问方式就是遍历,而遍历有两种方式:深度优先遍历和广度优先遍历。不过学过Huffman编码的人都知道,Huffman的编码,必须使用深度优先遍历,你懂得~

我在此默认的模式是,左树为0,右树为1.而这个遍历函数需要使用一个编码缓冲区和输出目标,以及深度探测。于是乎,一个参数好多的递归函数新鲜出炉了,昨天才被我正式造出来。

 template <class T>
void ErgodicTree(T & Root, char * szStr, int nDeep, string pStrArray[])
{
if(Root.lNext == NULL && Root.rNext == NULL)
pStrArray[Root.val] = szStr;
szStr[nDeep] = '';
if(Root.lNext != NULL)
ErgodicTree(*Root.lNext, szStr, nDeep + , pStrArray);
szStr[nDeep] = '';
if(Root.rNext != NULL)
ErgodicTree(*Root.rNext, szStr, nDeep + , pStrArray);
szStr[nDeep] = ;
}

需要注意的是,编码和解码的递归函数是不一样的,在这专门提一下,因为编码是一次性遍历完成全部的节点,而解码是每次只遍历到叶子节点。

可以看到,每次向下传递参数的时候,左树就置'0',右树就置'1',返回的时候必须清零。这样下一级函数会获取的结果,并且根据Deep的值对应置位,上级函数函数的乞讨递归也不会受到影响。代码写的很简单,但是其实很细致。

一旦访问到了叶子节点,就直接输出,这里写的也很巧妙,也就是在这里,获取到了Huffman编码,输出到对应的string数组中。

这样,就完成了Huffman编码。

III Huffman解码

用Huffman解码之前,你获取到的是一个很长的,内容是'0'和'1'的字符串。在我的代码中,这个字符串的长度是1024.

其实Huffman的解码实现起来也很简单,但是,存在细节性问题。

比如:从递归函数中获取返回值、下次解码的偏移地址、字符串访问已经到头了,但是解码失败(你想想这个问题出现的圆心),此时字符串中还剩下几个未解码的字符。

这些都是相当细节性的问题,另外文件中一般有n多个1024长度的以上的字节数,如何承上启下也是问题。

这一切,都在下面这段代码中解决:

 char Buffer[];
char DecodeBuffer[];//增加了八个缓冲字节
DWORD dwReadByte;
DWORD dwFlag = ;
DWORD dwDeep = ;
char tmpchar;
int EffectiveBufferSize = ;
int nLeftNumberInBuffer = ;
char szSmallBuffer[] = {}; //创建解压文件
HANDLE hDeCompressionFile = QuickCreateFile("C:\\DCRecord.txt");
assert(hDeCompressionFile != INVALID_HANDLE_VALUE); while()
{
int i = ;
dwReadByte = ReadHuffCodeFromFile(hHuffFile, Buffer, );
if(dwReadByte == )
break;
EffectiveBufferSize = ReadBitToBuffer(Buffer, (int)dwReadByte, DecodeBuffer + nLeftNumberInBuffer, );
EffectiveBufferSize += nLeftNumberInBuffer;
//TextFileFunction(DecodeBuffer, 1024);
for(i = ;(i + dwDeep) < EffectiveBufferSize;i += dwDeep)
{
dwDeep = ;
tmpchar = DecodeHuffman(&vecHuffmanArray[], DecodeBuffer + i, EffectiveBufferSize - i, dwDeep, dwFlag);
if(dwFlag == )
{
szSmallBuffer[] = tmpchar;
WriteBufferIntoFileNormally(hDeCompressionFile, szSmallBuffer, );
}
else
{
dwFlag = ;
break;
}
}
nLeftNumberInBuffer = EffectiveBufferSize - i;
memcpy(DecodeBuffer, DecodeBuffer + i, nLeftNumberInBuffer);
}

这段代码中对于这种问题完成的很好,我上面说的在晚上去吃饭的路上就是想明白了实现承上启下那个问题的。

大致步骤如下:

要注意到参数Deep是引用值,是会修改原值的。这个值同时是递归时使用的字符串偏移地址,这个地址所在的值,决定了下一级是向左子树走还是向右走的方向。也就是根据字符串数据来访问Huffman树,一旦访问到叶子节点,就表明此次的解码完成了,返回这个对应值。虽然是递归调用,但是每一级递归调用只有一条通路选择,所以返回值具有可传递性。

完成一段字符串的解码之后,此时的Deep参数就已经是访问过的字符串个数了,就能用于下一次解码的地址偏移,能够用于循环代码操作。

另外还有个问题就是,我从获取的huffman解码整条字符串,都是8的倍数(因为下级函数时将一个字节的8位数据按位解读,写入字符串),所以到最后的一段字符串解码失败很正常,因为这段字符串码不完全。此时就需要将这段字符码移到首部,然后与下一次读出来的字符码进行拼接。你可以注意到,我的代码中,一般都是在for循环中直接声明int i,但是在这里却是在while循环外声明的i,就是为了实现拼接,使得解码操作能够传递下去。如果一次性创建一个很大很大的缓冲区把整个文件都读进来,我只能说:图样图森破。

具体的操作就看代码吧,我写的时候是有点小纠结的,但是写完了一看,呵呵,就这么简单。

这样,解码也就完成了。

最后说一下文件操作。

首先写文件有一块很重要的就是,需要写一个文件头。而且是一个变长的文件头。

文件头主要内容:(不涉及文件夹)

1 文件原名,以及后缀

2 需要编码的数据的数据个数

3 存在的字符和该字符出现的次数

上面的2 3其实就是把构造Huffman树最基础的数组存入文件中,这样解压文件就能根据文件头来构造Huffman树,从而实现解码了。为此我专门写了一个负责文件头的函数。而且这里有个注意事项就是,写文件的时候,要把排好序的数组写进去,这样解压文件就不需要再次进行排序了,能省则省嘛。

查看文件头数据:

这个是我以前水平还很烂很烂的时候写的一个查看程序,最可笑的就是,我这缓冲区用的是一个CString,现在看看,真是荒唐可笑。

不过现在对于小文件,还是能看一看的。你能看到,前几个都是1的,就是int数据,只有出现1次的字符。后面出现的次数逐渐增加。

再往下拉的话,就是各种乱码了,都是按字节解释起来乱七八糟的东西了。

其实编码解码的文件操作这块,我觉得应该算是计算机中,对最小数据单元的操作了,绝对没有比这更小的了。因为要做的是根据编码结果一位一位的将数据写到char变量中,而在读文件这块,也是整块的读内存,然后按位解析字节,获取到以字节为单位的解码数据。

这块我就贴这几个函数,用于从字节到位,和从位到字节的操作。这活,还真的是挺细致的。记得我昨天调代码的时候,还专门调试这几个函数,因为当时解码出来的是乱码,然后我对照写文件前的Huffman码,还真的找到了问题所在。

Byte to Bit

 void SetByteBit(char * ByteAddr, int Val, int BitAddr)
{
int TmpVal = -;
int tmpval = ;
tmpval <<= BitAddr;
if(Val == )
{
TmpVal ^= tmpval;
*ByteAddr &= TmpVal;
}
else
{
*ByteAddr |= tmpval;
}
}
/*
将huffman码按位写入文件
*/
void WriteByteToFile(HANDLE hFile, const char * lpszHuffCode, int Mode)
{
if(hFile == INVALID_HANDLE_VALUE)
return; static int snBytePointer = ;
static int snBitPointer = ;
static char WriteBuffer[];
DWORD wfCounter = ;//写缓冲区指针
int nLength = ;
if(lpszHuffCode != NULL)
nLength = strlen(lpszHuffCode); if(Mode == )
{
WriteFile(hFile, WriteBuffer, snBytePointer + !!snBitPointer, &wfCounter, NULL);
snBytePointer = ;
snBitPointer = ;
wfCounter = ;
return;
} for(int i = ;i < nLength;\
++snBitPointer >= ?(snBytePointer ++,snBitPointer = ):snBitPointer,i ++)
{
if(snBytePointer > )
{
WriteFile(hFile, WriteBuffer, , &wfCounter, NULL);
snBytePointer = ;
}
SetByteBit(&WriteBuffer[snBytePointer], lpszHuffCode[i] - '',snBitPointer);
}
}

Bit to Byte

 /*
ReadByteBit Function
2013 10 05
*/ void ReadBitFromByte(const char Byte, char * buf = NULL)
{
if(buf == NULL)
return;
int nTmpVal = ;
int nAndResult;
for(int i = ;i < ;++ i)
{
nTmpVal <<= i;
nAndResult = nTmpVal & Byte;
buf[i] = '' + !!nAndResult;
nTmpVal = ;//this is prrety important
}
}
/*
还是写中文注释吧
这个函数是用于将从文件读出来的二进制信息取出来,并存放到字符串中。
返回值就是读出来的位数的长度
*/ int ReadBitToBuffer(const char * ReadBuf, int nByteNumber,char * OutputBuf, int nOBufLength)
{
if(nOBufLength < nByteNumber * )
return ;
for(int i = ;i < nByteNumber;i ++)
{
ReadBitFromByte(ReadBuf[i], OutputBuf + * i);
}
return nByteNumber * ;
}

刚才看了看自己写的一部分代码,感觉,还真的感觉到了代码中有不少自己的努力和智慧。

代码就不全贴了,内容就这么多,都是最基础的操作。不过此番之后,我觉得,我已经有能力编写压缩文件程序了,至少数据压缩存储这一块,我有了最基础的技术。

看下解码效果:

简单提下:

我的解压文件字符没问题,但是为什么有些符号却不一样?你可以看到,最后的输出的结果是有些许不同的,这令我很费解~

源文件:

压缩后的文件,这感觉,用三个字母表示:WTF……

解压文件:

至此完成

用C++实现Huffman文件编码和解码(2 总结)的更多相关文章

  1. Python学习笔记八:文件操作(续),文件编码与解码,函数,递归,函数式编程介绍,高阶函数

    文件操作(续) 获得文件句柄位置,f.tell(),从0开始,按字符数计数 f.read(5),读取5个字符 返回文件句柄到某位置,f.seek(0) 文件在编辑过程中改变编码,f.detech() ...

  2. Java 8中的Base64编码和解码

    转自:https://juejin.im/post/5c99b2976fb9a070e76376cc Java 8会因为将lambdas,流,新的日期/时间模型和Nashorn JavaScript引 ...

  3. NET MVC全局异常处理(一) 【转载】网站遭遇DDoS攻击怎么办 使用 HttpRequester 更方便的发起 HTTP 请求 C#文件流。 Url的Base64编码以及解码 C#计算字符串长度,汉字算两个字符 2019周笔记(2.18-2.23) Mysql语句中当前时间不能直接使用C#中的Date.Now传输 Mysql中Count函数的正确使用

    NET MVC全局异常处理(一)   目录 .NET MVC全局异常处理 IIS配置 静态错误页配置 .NET错误页配置 程序设置 全局异常配置 .NET MVC全局异常处理 一直知道有.NET有相关 ...

  4. python 应用开发之-用base64 对图片文件的编码和解码处理

    用base64 对图片文件的编码和解码处理 import base64 def convert(image): f = open(image) img_raw_data = f.read() f.cl ...

  5. Java利用Base64编码和解码图片文件

    1.编码与解码代码如下所示: import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import jav ...

  6. Kotlin/Java Base64编码和解码(图片、文件)

    原文: Kotlin/Java Base64编码和解码(图片.文件) | Stars-One的杂货小窝 最近在项目中使用到了Base64编码和解码,便是稍微写篇文章记录一下 PS:本文代码都是使用Ko ...

  7. 转载:AAC文件解析及解码

    转自:http://blog.csdn.net/wlsfling/article/details/5876016 http://www.cnblogs.com/gaozehua/archive/201 ...

  8. java编码原理,java编码和解码问题

    java的编码方式原理 java的JVM的缺省编码方式由系统的“本地语言环境”设置确定,和操作系统的类型无关 . 在JAVA源文件-->JAVAC-->Class-->Java--& ...

  9. RapidJSON 代码剖析(三):Unicode 的编码与解码

    根据 RFC-7159: 8.1 Character Encoding JSON text SHALL be encoded in UTF-8, UTF-16, or UTF-32. The defa ...

随机推荐

  1. [ Python - 3 ] python3.5中不同的读写模式

    r 只能读.r+可读可写,不会创建不存在的文件.如果直接写文件,则从顶部开始写,覆盖之前此位置的内容,如果先读后写,则会在文件最后追加内容.w+ 可读可写 如果文件存在 则覆盖整个文件不存在则创建w ...

  2. [ 总结 ] Linux系统测试硬盘I/O

    检测硬盘I/O相对来说还是一个比较抽象的概念,但是对系统性能的影响还是至关重要的. (1)使用hdparm命令检测读取速度:    hdparm命令提供了一个命令行的接口用于读取和设置IDE和SCSI ...

  3. CompareUtil

    java package com.daojia.beauty.open.utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; ...

  4. ETL(Extract-Transform-Load的缩写,即数据抽取、转换、装载的过程)

    ETL(Extract-Transform-Load的缩写,即数据抽取.转换.装载的过程)

  5. 一分钟了解ruby中的单测

    之前用gtest写过很多c++的单测case, 对gtest的强大和灵活印象深刻:最近需要用ruby写一个小工具, 接触了下ruby, 写了代码就要写单测啊(好的单测确实对代码的健壮性和正确性保证上太 ...

  6. (十)MySQL日志

    1)日志种类 error log:错误日志 拍错 /var/log/mysqld.log \这是yum安装mysqld生成error默认目录 bin blog 二进制日志 备份 增量备份,记录DDL, ...

  7. django URL参数在view中传递和Template的反向解析方式

    一. URL参数在view中传递 1.带参数名:通过named group方式传递指定参数,语法为: (?P<name>pattern), name 为传递参数的名称,pattern代表所 ...

  8. HDU 6319.Problem A. Ascending Rating-经典滑窗问题求最大值以及COUNT-单调队列 (2018 Multi-University Training Contest 3 1001)

    2018 Multi-University Training Contest 3 6319.Problem A. Ascending Rating 题意就是给你长度为k的数列,如果数列长度k<n ...

  9. python模块之XlsxWriter

    官网Tutorial:http://xlsxwriter.readthedocs.io/tutorial Xlsx是python用来构造xlsx文件的模块,可以向excel2007+中写text,nu ...

  10. 51nod 1459 迷宫游戏【最短路拓展】

    1459 迷宫游戏 基准时间限制:1 秒 空间限制:131072 KB   你来到一个迷宫前.该迷宫由若干个房间组成,每个房间都有一个得分,第一次进入这个房间,你就可以得到这个分数.还有若干双向道路连 ...