自己动手写压缩软件
自己动手写压缩软件
作者: huzy
【 源码下载 : http://sourceforge.net/projects/hzyzip/ 】
咳咳 !!!
首先,有点小激动,(*^_^*),写了两天两夜再加一个清晨的压缩软件“成功”通过啦!
压缩了一曲劲爆的 MV ,再解压,然后边听边写 …… 如有笔误,纯属激动!!!
打这个“歪主意”很久了,就是没动手,前些天被偶那亲爱的哥哥给激了下,所以决心“玩玩”。
经过偶的“高速 CPU ”规划了下,首先得准备好 Huffman 算法(算法是偶的强项,过去一年多,偶吃饱了就干这个,
所以小小 Huffman 不成问题);然后得测试一下读取所有格式的文件,以 ASCII 码方式读取
(这个是偶哥哥提示的,其实偶也知道,可就是想歪了,一直没到这个点上);
最后就是把这两个idea 合成在一起,听起来似乎很简单哦……动手玩玩!
整体的构想:
1. 按 ASCII 码读取文件,统计文件中每个ASCII码值对应字符的个数,作为权值,然后进行 Huffman 编码;
2. 然后将目标文件中的每个 ASCII 码字符用对应的 Huffman 编码字符串替换,替换后再将 '0' '1' 字符串转化为二进制流,
再将二进制流依次分割成8位的若干小片段,最后将每8位二进制转化为对应大小的整数,即为新的 ASCII 码值;
(参看函数:bool Huffman::condensingFile(char sourceFile[], char targetFile[], HCNode *HC))
嗯 ~~ 大概你没看太明白……
所以我涂鸦了个“思路图”,看看 ~ ~
上面的图中所示数据是我在调试程序的时候 copy 下来的,认真的你有没有发现……
呵呵!源文件中的头 10 个ASCII 码压缩后变成了 8 个ASCII 码了 ! (*^_^*)效果来了!
也许你还发现了第一个 ASCII 码对应的 Huffman 编码长度为 14,也就是说一个字节的数据被
“压缩”成了近两个字节(14 / 8 = 1.75) !
是否文件不但没有被压缩反而会被扩张呢?咳咳!实践加理论证明:不会。
为了便于解压,每次都会保存目标文件对应的 Huffman 树;
3. 压缩流程想好了,接下来是解压,首先从压缩文件对应的 Huffman 树文件开始,构建一棵 Huffman 树,
再就是压缩的逆操作:
以 ASCII 码形式读取压缩文件 ==> 转化成二进制字符串 ==> 二叉搜索 Huffman 树与二进制字符串匹配
==> 锁定叶子节点得到节点中保存的 ASCII 码 ==> 写入文件即得到解压文件。
首先测试以 ASCII 码方式读文件:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { FILE *fp; FILE *fcopy;
/* ** 如果: ch 是 unsigned char 型的,那么 ch = fgetc(fp), ch 将不可能为 EOF; ** 如果: ch 是 char 型的,那么 ch = fgetc(fp), 当 ch = EOF 结束时,
** 可能没有读完文件就已终止! ** 所以: ch 应该设为 int 型 。 */ // unsigned char ch; // wrong ! can't be EOF ! // char ch; int ch; // right ! int i, len ; int count = 0; char fileName[100]; char copyFile[100]; char postfix[] = "_copy"; printf("> Input fileName : "); scanf("%s",fileName); fp = (FILE*)fopen(fileName,"rb"); if( !fp ) { printf("can't open the file .\n"); return 0; } strcpy(copyFile, fileName); len = strlen(fileName); for(i=len; i>0; i--) if(fileName[i] == '.') break; strcpy(&File[i], postfix); len = strlen(postfix); strcpy(&File[i+len], &fileName[i]); fcopy = fopen(copyFile,"wb"); // copy while( (ch = fgetc(fp)) != EOF ) { fputc(ch,fcopy); if((++count)%20 == 0) printf("\n"); printf("%d ",ch); } printf("\n total : %d \n",count); fclose(fp); fclose(fcopy); return 0; }
输入目标文件路径名后就看到整版的数字啦,如下图所示:
【源代码参看 readfile_test 文件夹】
然后就是测试Huffman 算法:
/*=============================================================*/ /* */ /* Huffman 编码 */ /* */ /*=============================================================*/ #ifndef HUFFMAN_CODE_STRUCT_H #define HUFFMAN_CODE_STRUCT_H /*=============================================================*/ #define INFINITY 1000000 // 自定义“无穷大” /* 数据结构 */ typedef struct { unsigned int weight; unsigned int parent; unsigned int lchild; unsigned int rchild; }HTNode,*HuffmanTree; typedef char **HuffmanCode; /*=============================================================*/ /* 函数声明 */ void HuffmanCoding(HuffmanTree *HT, HuffmanCode *HC, int *w, int n); void Select(HuffmanTree *tree, int n, int *s1, int *s2); int Min(HuffmanTree tree, int n); /*=============================================================*/ // 从哈弗曼树的 n 个结点中选出权值最小的结点 int Min(HuffmanTree tree, int n) { unsigned int min = INFINITY; int flag; int i; for(i=1; i<=n; i++) if(tree[i].weight<min && tree[i].parent==0) { min = tree[i].weight; flag = i; } tree[flag].parent = 1; return flag; } /*=============================================================*/ // 在哈弗曼树的 n 个结点中选出权值最小的两个结点,记录其序号s1,s2 void Select(HuffmanTree *tree, int n, int *s1, int *s2) { int temp; *s1 = Min(*tree,n); *s2 = Min(*tree,n); // if(s1 > s2) // attention ! if( (*tree)[*s1].weight > (*tree)[*s2].weight ) { temp = *s1; *s1 = *s2; *s2 = temp; } } /*=============================================================*/ void HuffmanCoding(HuffmanTree *HT, HuffmanCode *HC, int *w, int n) // HT 为二级指针,双向传值 { char *cd; int i; int s1, s2; int go; int cdlen; int m = 2*n-1; HuffmanTree p; if( n <= 1 ) return ; /*--------------------------------------------------------*/ // 1>. 初始化 //*HT = (HuffmanTree)malloc((m+1)*sizeof(HuffmanTree)); // *HT = (HuffmanTree)malloc((m+1)*sizeof(HTNode)); // 0 号单元不用 p = *HT + 1; for(i=1; i<=n; i++, p++, w++) { (*p).parent = 0; (*p).lchild = 0; (*p).rchild = 0; (*p).weight = *w; } for(i=n+1; i<=m; i++, p++) { p->parent = 0; p->rchild = 0; p->lchild = 0; p->weight = 0; } /*--------------------------------------------------------*/ // 2>. 构建树 for(i=n+1; i<=m; i++) // i<=m { Select(HT, i-1, &s1, &s2); (*HT)[s1].parent = i; (*HT)[s2].parent = i; (*HT)[i].lchild = s1; (*HT)[i].rchild = s2; (*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight; } /*--------------------------------------------------------*/ // 3>. 求 HF 编码 ( 从根结点到叶子结点求取 ) *HC = (HuffmanCode)malloc( (n+1)*sizeof(char *) ); cd = (char *)malloc( n*sizeof(char) ); // 编码暂存串 if( !cd ) { printf("> failure \n"); return ; } cdlen = 0; go = m; // 从根结点开始 for(i=1; i<=m; i++) // 利用 weight 来做左右孩子遍历的标记 (*HT)[i].weight = 0; while( go ) { if( 0 == (*HT)[go].weight ) // 左右孩子都未遍历 { (*HT)[go].weight = 1; // 标为左访问 if( (*HT)[go].lchild != 0 ) // 左孩子存在 { go = (*HT)[go].lchild; cd[cdlen++] = '0'; } else // 左孩子不存在 { if( 0 == (*HT)[go].rchild ) // 右孩子不存在 { (*HC)[go] = (char *)malloc( (cdlen+1) * sizeof(char) ); cd[cdlen] = '\0'; strcpy( (*HC)[go], cd ); } } } else { if( 1 == (*HT)[go].weight ) // 左孩子已经遍历 { (*HT)[go].weight = 2; // 标为右访问 if( (*HT)[go].rchild != 0 ) { go = (*HT)[go].rchild; cd[cdlen++] = '1'; } } else // 左右孩子都已经遍历 { go = (*HT)[go].parent; // 退回到双亲结点 -- cdlen; } } } } /*=============================================================*/ #endif // 预编译结束
最后测试了一下我的大名(hu zhen yang)和今天的日期(2011.8.6)组成的叶子节点和权值,
得到每个字符串的对应编码,如下图所示:
【源代码参看 Huffman 文件夹】
偶特别的喜欢用 C 语言写程序,虽然偶的 C++ 学得特别认真,看了很多 C++ 写的代码,
偶在MFC下面也是用C++风格来写的,
可一旦要偶自己来封装个类,偶就不愿了,改不了 C 这行当。
不过这次偶可是认真筹划,用 C++ 自己封装了两个类(*^_^*),不是很有技术含量,但还勉强过意得去啦!
…… 【预编译和宏定义略】
class Huffman { public: Huffman(); Huffman(Map *mapArray, int countLeaf); ~Huffman(); bool createFromFile(char *InFileName, char postfix[]); bool writeToFile(char *OutFileName, char *postfix); bool CodingFromTree(); // 二叉遍历已有的 Huffman 树获取编码 void setInfo(Map *mapArray, int countLeaf); void HuffmanCoding(); HuffmanCode getHFcode(); int getLeafCount(); /*===================================================================*/ public: bool condensingFile(char sourceFile[], char targetFile[], HCNode *HC); bool expandingFile(char sourceFile[], char targetFile[]); protected: int BStringToInt(char str[], int str_len); void IntToBString(int k, char str[], int str_len); /*===================================================================*/ protected: int Min(HuffmanTree tree,int n); void Select(HuffmanTree *tree, int n, int *s1, int *s2); private: int m_countLeaf; // 叶子数 Map *m_pMapArray; // 叶子权值数组指针 HuffmanCode HC; HuffmanTree HT; }; …… class Zip { public: Zip(char *fileName, bool flag); Zip(); ~Zip(); bool createZipFromFile(char *fileName, bool flag); bool countMapArray(); void condenseFile(); // 压缩文件(进行编码) void expandeFile(); // 解压文件 bool saveHuffmanTree(char fileName[], char *postfix); bool loadHuffmanTree(char fileName[], char postfix[]); /*================================================*/ void printMapArray(); void printHuffmanCode(); long totalByte(); // 返回文件的大小 /*================================================*/ protected: bool openFile(); HuffmanCode getHFcode(); // 获取编码 private: char m_fileName[256]; Map m_mapArray[256]; long m_totalByte; int m_leafCount; // 有效叶子数 Huffman m_huffmanProc; HuffmanCode m_code; }; ……
好不容易写完,兴奋的测试起来,结果首次测试,就满文件的乱码(如下图所示)……
是偶邪恶了?还好让偶看到了一点点希望,那一串串“======================”证明还没“邪”多远!
经过认真排查,终于发现问题出在解压时,搜索 Huffman 树,匹配成功的情况下二进制流未回退一步,更正代码截图如下所示:
修改后再测试截图如下:
最左边是源文件,中间是压缩后再解压的文件,哈哈,兴奋!
好了,再来看看怎么压缩文件和解压文件的:
/*=============================================================*/ // 从目标文件到压缩文件,按 Huffman 编码 ( HC )压缩并存储 const int BUF_LEN = 960; const int BUF_LEN2 = BUF_LEN + 40; const int STR_LEN = 8; // str 的长度固定为 8 const int STR_LEN2 = STR_LEN + 2; bool Huffman::condensingFile(char sourceFile[], char targetFile[], HCNode *HC) { FILE *sfp = fopen(sourceFile,"rb"); if( !sfp ) return false; FILE *tfp = fopen(targetFile,"wb"); if( !tfp ) return false; int i, j, k; int len, pos; int ch, key; int res_len = 0; char str[STR_LEN2]; // 比 STR_LEN 大一点 char temp[BUF_LEN2]; // 比 BUF_LEN 大一点 /* ** 关于已取得的 Huffman 编码表 HC ** 建立一个 ascii 码到 HC 数组下标的映射表 !!! ** 如果每次都遍历匹配会降低压缩效率。 */ int asciiMap[256]; for(i=0; i<m_countLeaf; i++) { asciiMap[ HC[i+1].ascii ] = i+1; // 0 号单元未用 } while( (ch = fgetc(sfp)) != EOF ) { len = strlen( HC[ asciiMap[ch] ].code ); for(i=0; i<len; i++) temp[ res_len++ ] = HC[ asciiMap[ch] ].code[i]; // 按 Huffman 编码转化 if( res_len >= BUF_LEN ) // 长度达到 BUF_LEN 就处理 { pos = 0; k = 0; while( pos <= BUF_LEN ) { str[ k++ ] = temp[pos++]; //if( k == STR_LEN - 1 ) // wrong ! if( k == STR_LEN ) { k = 0; key = BStringToInt(str,STR_LEN); fputc(key, tfp); } } for(i=BUF_LEN, j=0; i<res_len; i++,j++) // 把未处理完的字符前移 temp[j] = temp[i]; res_len = j; } } if( res_len > 0 ) // res_len < BUF_LEN ( 960 ) { pos = 0; k = 0; while( pos < res_len ) { str[ k++ ] = temp[pos++]; //if( k == STR_LEN - 1 ) // wrong ! if( k == STR_LEN ) { k = 0; key = BStringToInt(str, STR_LEN); fputc(key, tfp); } } if( k > 0 ) // k < STR_LEN ( 8 ) { /* ** 对整个文件最后一个字符的处理: */ //key = BStringToInt(str, k); // 不足八位,高位补零 key = BStringToInt(str, STR_LEN); // 不足八位,地位补零 fputc(key, tfp); } } fclose(sfp); fclose(tfp); return true; } /*=============================================================*/ // 从压缩文件到目标文件,解压并存储 bool Huffman::expandingFile(char sourceFile[], char targetFile[]) { FILE *sfp = fopen(sourceFile,"rb"); // 源文件(压缩文件) if( !sfp ) return false; FILE *tfp = fopen(targetFile,"wb"); // 目标文件(即将被解压后的文件) if( !tfp ) return false; int ch; int i, j, rear; int r, r_pre; int pos; int res_len = 0; char key[STR_LEN2]; // 10 char temp[BUF_LEN2]; // 1000 while( (ch = fgetc(sfp)) != EOF ) { IntToBString(ch, key, STR_LEN); for(i=0; i<STR_LEN; i++) temp[res_len++] = key[i]; if(res_len >= BUF_LEN) // 长度达到 BUF_LEN 就处理 { pos = 0; r = m_countLeaf * 2 - 1; // 根 r_pre = r; while( pos <= BUF_LEN ) { if( r == 0 ) // r=0, r_pre 指向叶子结点 { /* ** 当 r == 0 时,表示 r 的前一个结点是 Huffman 树的叶子结点; ** 然而,还多进行了一次 pos ++ 操作;应该回退一位。 ** 故: 应该在找到叶子结点时 pos -- 。 */ pos -- ; // very important !!! rear = pos; // 记录串中已处理的位置 r = m_countLeaf * 2 - 1; // 根 //fputc(r_pre, tfp); // wrong ! fputc(HT[r_pre].ascii, tfp); // !!! } r_pre = r; temp[ pos ] == '0' ? r = HT[r].lchild : r = HT[r].rchild; pos ++; } for( i=rear,j=0; i<res_len; i++,j++ ) // 把未处理完的字符前移 temp[j] = temp[i]; res_len = j; } } if( res_len > 0 ) // res_len < BUF_LEN ( 960 ) { pos = 0; r = m_countLeaf * 2 - 1; r_pre = r; while( pos < res_len ) { if( r == 0 ) { pos -- ; // very important !!! rear = pos; r = m_countLeaf * 2 - 1; //fputc(r_pre, tfp); // wrong ! fputc(HT[r_pre].ascii, tfp); } r_pre = r; temp[ pos++ ] == '0' ? r = HT[r].lchild : r = HT[r].rchild; } // 如果还有未处理的,省略。因为写入时最后一个字节采用了地位补零的方式。 } fclose(sfp); fclose(tfp); return true; } /*=============================================================*/
接下来,我又测试了 BMP , jpg 文件,Map4 文件:
下图是一部 491M 大小的电影的 Huffman 编码表部分截图。
关键错误排查:
1. 当我测试全篇只有一个ASII码值的字符文件时,程序崩溃了!
原因很简单:
Huffman编码至少得需要两个节点才能编码。
对策:
方案1> 对文件遍历,对上述情况直接“跳出”,不予编码,记录该ASCII码和字符数量,简单快捷,压缩比最大。
方案2> 我再添加一个任意的ASCII码值,并且令其权值为0,
这时与文件中的那个ASCII码值就凑成了两个,就可以编码了!
我的处理方法:
为了适应整个软件的通用性,即压缩后产生两个文件,一个“资源文件”和一个“编码文件”,我采用的时方案2。
【补充: 对于空文件,直接跳出,因为对空文件压缩毫无意义。 】
2.在我随机的改变了文件大小的情况下,测试解压,发现在解压后的文件的尾部出现了乱码:
原因:
参看上文图解“压缩映射表”,我将每 8 位二进制码组成一个小片段,很显然在大多数情况下全文的二进制流的大小不会恰好是 8 的整数倍 !
而我的处理方法是将全文件的最后一个不足 8 位的二进制片段补 0 成为 8 位,而解压时,
很有可能补上的 0 恰好构成了一个编码,导致解压出了多余的字符。所以就有可能出现了上图中所示的乱码。
对策:
在压缩时,统计整个文件的大小 Count,并将文件的总字节数Count写入编码文件。在压缩时就只解压出 Count 个字节,
多余部分是无效的 0 ,予以略去。
性能比较与软件扩展:
好了!该“臭美”一下了!
与专业的 zip 压缩软件“比拼”!我的软件压缩速度居然比 zip 快 !呵呵……不过压缩比就逊色多了, 同一部电影,
我的压缩后还有 490 M,而 zip 压缩后只有 477 M;而且解压速度也差了很多,zip 解压 491 M的电影只须22 秒,
我的却要将近 3 分钟,小小打击了……不过我知道时间消耗在哪了:我的解压采用的是每次从 Huffman 树的根节点搜索,
这种方式无疑会更耗时。
虽然在效率上比不了 zip 等专业压缩软件,但是我可以换换角度——把它做成小型文件的加密软件 !
各种细节处理与技巧运用,参考源码文件,偶注解得还算详细 ! (*^_^*)
huzy
2011.8.6
( 今天情人节 ! 没情人的孩子在家写软件 ! )
补充:
载入界面后的压缩软件截图:
【 采用多线程技术避免界面冻结 】