您的位置:首页 > 编程语言 > C语言/C++

c语言实现哈夫曼huffman压缩文本

2018-01-16 20:38 337 查看
哈夫曼压缩原理就是构建二叉树,出现频率高的字母用更少的位数来表示,实现压缩的效果
比如字符串abcbbc
构建哈夫曼树



这样构建出编码表b->0,a->10,  c->11
原本6个字符要48位来表示,现在只需要9位来表示即可

1.    首先将文本文件的每一个字符进行统计,构建编码表,这个编码表大概50几kvoid readTxt(string file,Character *cList)

{

ifstream infile;

infile.open(file.data()); //将文件流对象与文件连接起来

assert(infile.is_open()); //若失败,则输出错误消息,并终止程序运行

char c;

infile >> noskipws;

while (!infile.eof())

{

addList(c,cList);

infile>>c;

}

infile.close(); //关闭文件输入流

}

void addList(char c,Character *cList){

//为空就退出

if(!c) return;

totalNum++;

int i=0;

for(;i<cnumber;i++){

if(c==cList[i].data){

break;

}

}

cList[i].number++;

if(i==cnumber){

cList[cnumber].data = c;

cnumber++;

}

if(i>0 && cList[i].number>cList[i-1].number){

int num = cList[i].number;

char ch = cList[i].data;

cList[i].data = cList[i-1].data;

cList[i].number = cList[i-1].number;

cList[i-1].data = ch;

cList[i-1].number = num;

}

}


1.    根据huffman树原理,构建编码表(如下图所示),词频越高则用更小的编码表示
//换行符

string enterCode;

////////////

typedef struct node{

char ch; //存储该节点表示的字符,只有叶子节点用的到

int val; //记录该节点的权值

structnode *self,*left,*right; //三个指针,分别用于记录自己的地址,左孩子的地址和右孩子的地址

friend booloperator <(const node &a,constnode &b) //运算符重载,定义优先队列的比较结构

{

return a.val>b.val; //这里是权值小的优先出队列

}

}node;

int codeTablePlace = 0;

node *myroot;

priority_queue<node> p; //定义优先队列

char res[10000]; //用于记录哈夫曼编码

void dfs(node *root,int level,CodeTableItem *codetable,Character *cList) //哈夫曼编码表

{

if(root->left==root->right) //叶子节点的左孩子地址一定等于右孩子地址,且一定都为NULL;叶子节点记录有字符

{

if(level==0) //“AAAAA”这种只有一字符的情况

{

res[0]='0';

level++;

}

res[level]='\0'; //字符数组以'\0'结束

//printf("%c=>%s\n",root->ch,res);

//搜索在统计表中的位置

for(int j=0;j<cnumber;j++){

if(root->ch==cList[j].data){

codetable[j].data = root->ch;

codetable[j].code =res;

}

}

// codetable[codeTablePlace].data = root->ch;

// codetable[codeTablePlace].code = res;

if(int(root->ch)==10)

enterCode =res;

// codeTablePlace++;

}

else

{

res[level]='0'; //左分支为0

dfs(root->left,level+1,codetable,cList);

res[level]='1'; //右分支为1

dfs(root->right,level+1,codetable,cList);

}

}

void huffman(Character *hash,CodeTableItem *codeTable) //构建哈夫曼树

{

node *root,fir,sec;

for(int i=0;i<cnumber;i++)

{

root=(node *)malloc(sizeof(node)); //开辟节点

root->self=root; //记录自己的地址,方便父节点连接自己

root->left=root->right=NULL; //该节点是叶子节点,左右孩子地址均为NULL

root->ch=hash[i].data; //记录该节点表示的字符

root->val=hash[i].number; //记录该字符的权值

p.push(*root); //将该节点压入优先队列

}

//下面循环模拟建树过程,每次取出两个最小的节点合并后重新压入队列

//当队列中剩余节点数量为1时,哈夫曼树构建完成

while(p.size()>1)

{

fir=p.top();p.pop(); //取出最小的节点

sec=p.top();p.pop(); //取出次小的节点

root=(node *)malloc(sizeof(node)); //构建新节点,将其作为fir,sec的父节点

root->self=root; //记录自己的地址,方便该节点的父节点连接

root->left=fir.self; //记录左孩子节点地址

root->right=sec.self; //记录右孩子节点地址

root->val=fir.val+sec.val;//该节点权值为两孩子权值之和

p.push(*root); //将新节点压入队列

}

fir=p.top();p.pop(); //弹出哈夫曼树的根节点

myroot = fir.self;

dfs(fir.self,0,codeTable,CList); //输出叶子节点记录的字符和对应的哈夫曼编码

}
1.     接下来是压缩的关键部分,首先读入一行的字符,通过编码表将该行加密成01串。根据01串构造一个个字符,一个char型字符是1个字节,也就是8个01就可以通过移位和加一来构建一个char类型,然后将该字符写入文件。这里要注意的是一行最后要加上换行符号的编码,直接读取一行是不会读取到换行符的。其次是一行最后的一个字符一般是没有构建完的,这样就会在最后一个字符的ASCII码的二进制表示前面引入几个0,应该将该字符记录下来,在新的一行构建字符的时候将该字符补全。



/*加密文件*/

string tempstr="";

ofstream out;

//记录上一行尾部没有填充完的字符leftChar和还剩多少位填完leftBit;

char leftChar=0;

int leftBit=0;

void encodeLine(string line,string path,CodeTableItem *cTable){

if (line=="" ) {

return;

}

if (out.is_open())

{

//将一行文本加密成二进制字符串

string str= "";

for(int i=0;i<line.size();i++){

for(int j=0;j<cnumber;j++){

if(line[i]==cTable[j].data){

str+=cTable[j].code;

break;

}

}

}

//加上换行符

str+=enterCode;

int count = 0;

unsigned char c=leftChar;

//先把上一行字符填充完

for(int i=0;i<leftBit;i++){

c<<=1;

if(str[i]=='1'){

c = c + 1;

}

}

//cout<<str<<"-";

//tempstr+=c;

char ch1 = c;

out<<ch1;

//加密这一行剩下的

for(int i=leftBit;i<str.size();i++){

c<<=1;

count++;

if(str[i]=='1'){

c = c + 1;

}

// //一个字节存满,将改字节保存

if(count==8){

ch1 = c;

out<<ch1;

//tempstr+=c;

c = 0;

count=0;

}

}

//记录没有保存的字符

4000
leftChar = c;

leftBit = 8-count;

out<<tempstr;

//cout<<"1";

}

}

void encodeHuffman(string file,string path,CodeTableItem *cTable){

//读取文件,一个个字符加密

ifstream infile;

infile.open(file.data()); //将文件流对象与文件连接起来

assert(infile.is_open()); //若失败,则输出错误消息,并终止程序运行

infile >> noskipws;

string line;

while (std::getline(infile, line)) {

encodeLine(line,path,cTable);

}

infile.close();

}


解密过程也很简单,就是加密的逆过程读取一个字符将其解密成01串,然后顺着01串遍历huffman树,如果是叶子节点就输出字符,并且回到根节点准备下一个字符的解密。//解密void decodeHuffman(string file){

node *p = myroot;

ifstream infile;

infile.open(file.data()); //将文件流对象与文件连接起来

assert(infile.is_open()); //若失败,则输出错误消息,并终止程序运行

unsigned char c;

infile >> noskipws;

int wordcount=0;

infile>>c;

while(!infile.eof())

{

infile>>c;

string str = "";

int beichushu = 128;

for(int i=0;i<8;i++){

int bit = (int)c/beichushu;

if(bit==1){

p = p->right;

str+= "1";

}

else{

str+= "0";

p = p->left;

}

if(p->left==NULL){

//cout<<p->ch;

out<<p->ch;

//当执行到最后一个字符就结束,不要继续输出后面多余字符

wordcount++;

if(wordcount==totalNum)

return;

p = myroot;

}

c <<= 1;

}

//cout<<str;

//cout<<wordcount;

}

infile.close(); //关闭文件输入流

}
实验结果,一开始压缩和解密用了2分钟,后来发现别人都不用1s,原来是我将构建的加密字符都不断加入一个字符串里面,该字符串是全局变量。每次执行压缩函数都会重新打开该文件,然后从头开始覆盖写入,这样每一次都写入一个超长字符串。相当于做了很多重复的操作。
改掉这个错误后压缩就只要1s多。
mac上只有zip,所以直接用命令行查看压缩时间。压缩cacm.all压缩时间



我的算法与zip的对比
规模我的压缩时间zip时间我的压缩率zip压缩率
2.2M0.9s0.11s63.6%31.8%
22M14.3s1.7s65.3%35%
220M148.6s10.8s65.5%32%
2200M1522.1s63.2s65.730%
将他们时间取对数后发现,我的代码几乎是线性递增的,我这个文件是有原来的cacm文件多次复制张贴而来,所以构成的哈夫曼树是一样的,压缩率也就差不多一样。而zip就比较神奇,也是预料之中,可能是它出现次数越多的字母的权重越大,是时间在逐渐减少,而且压缩率也降低。



完整代码
由于忙着赶作业写得有点乱,代码也没有美化和优化,现在闲下来反而不想弄了#include <iostream>

#include <iostream>

#include <fstream>

#include <cassert>

#include <string>

#include <time.h>

#include<queue>

#include<stdlib.h>

#include<bitset>

using namespace std;

class Character

{

public:

char data;

int number;

Character(){

data = ' ';

number = 0;

}

};

class CodeTableItem{

public:

char data;

string code;

};

//字符统计表

Character CList[200];

int cnumber=0,totalNum =0;

void addList(char c,Character *cList){

//为空就退出

if(!c) return;

totalNum++;

int i=0;

for(;i<cnumber;i++){

if(c==cList[i].data){

break;

}

}

cList[i].number++;

if(i==cnumber){

cList[cnumber].data = c;

cnumber++;

}

if(i>0 && cList[i].number>cList[i-1].number){

int num = cList[i].number;

char ch = cList[i].data;

cList[i].data = cList[i-1].data;

cList[i].number = cList[i-1].number;

cList[i-1].data = ch;

cList[i-1].number = num;

}

}

void readTxt(string file,Character *cList)

{

ifstream infile;

infile.open(file.data()); //将文件流对象与文件连接起来

assert(infile.is_open()); //若失败,则输出错误消息,并终止程序运行

char c;

infile >> noskipws;

while (!infile.eof())

{

addList(c,cList);

infile>>c;

}

infile.close(); //关闭文件输入流

}

//换行符

string enterCode;

////////////

typedef struct node{

char ch; //存储该节点表示的字符,只有叶子节点用的到

int val; //记录该节点的权值

structnode *self,*left,*right; //三个指针,分别用于记录自己的地址,左孩子的地址和右孩子的地址

friend booloperator <(const node &a,constnode &b) //运算符重载,定义优先队列的比较结构

{

return a.val>b.val; //这里是权值小的优先出队列

}

}node;

int codeTablePlace = 0;

node *myroot;

priority_queue<node> p; //定义优先队列

char res[10000]; //用于记录哈夫曼编码

void dfs(node *root,int level,CodeTableItem *codetable,Character *cList) //哈夫曼编码表

{

if(root->left==root->right) //叶子节点的左孩子地址一定等于右孩子地址,且一定都为NULL;叶子节点记录有字符

{

if(level==0) //“AAAAA”这种只有一字符的情况

{

res[0]='0';

level++;

}

res[level]='\0'; //字符数组以'\0'结束

//printf("%c=>%s\n",root->ch,res);

//搜索在统计表中的位置

for(int j=0;j<cnumber;j++){

if(root->ch==cList[j].data){

codetable[j].data = root->ch;

codetable[j].code =res;

}

}

// codetable[codeTablePlace].data = root->ch;

// codetable[codeTablePlace].code = res;

if(int(root->ch)==10)

enterCode =res;

// codeTablePlace++;

}

else

{

res[level]='0'; //左分支为0

dfs(root->left,level+1,codetable,cList);

res[level]='1'; //右分支为1

dfs(root->right,level+1,codetable,cList);

}

}

void huffman(Character *hash,CodeTableItem *codeTable) //构建哈夫曼树

{

node *root,fir,sec;

for(int i=0;i<cnumber;i++)

{

root=(node *)malloc(sizeof(node)); //开辟节点

root->self=root; //记录自己的地址,方便父节点连接自己

root->left=root->right=NULL; //该节点是叶子节点,左右孩子地址均为NULL

root->ch=hash[i].data; //记录该节点表示的字符

root->val=hash[i].number; //记录该字符的权值

p.push(*root); //将该节点压入优先队列

}

//下面循环模拟建树过程,每次取出两个最小的节点合并后重新压入队列

//当队列中剩余节点数量为1时,哈夫曼树构建完成

while(p.size()>1)

{

fir=p.top();p.pop(); //取出最小的节点

sec=p.top();p.pop(); //取出次小的节点

root=(node *)malloc(sizeof(node)); //构建新节点,将其作为fir,sec的父节点

root->self=root; //记录自己的地址,方便该节点的父节点连接

root->left=fir.self; //记录左孩子节点地址

root->right=sec.self; //记录右孩子节点地址

root->val=fir.val+sec.val;//该节点权值为两孩子权值之和

p.push(*root); //将新节点压入队列

}

fir=p.top();p.pop(); //弹出哈夫曼树的根节点

myroot = fir.self;

dfs(fir.self,0,codeTable,CList); //输出叶子节点记录的字符和对应的哈夫曼编码

}

/*加密文件*/

string tempstr="";

ofstream out;

//记录上一行尾部没有填充完的字符leftChar和还剩多少位填完leftBit;

char leftChar=0;

int leftBit=0;

void encodeLine(string line,string path,CodeTableItem *cTable){

if (line=="" ) {

return;

}

if (out.is_open())

{

//将一行文本加密成二进制字符串

string str= "";

for(int i=0;i<line.size();i++){

for(int j=0;j<cnumber;j++){

if(line[i]==cTable[j].data){

str+=cTable[j].code;

break;

}

}

}

//加上换行符

str+=enterCode;

int count = 0;

unsigned char c=leftChar;

//先把上一行字符填充完

for(int i=0;i<leftBit;i++){

c<<=1;

if(str[i]=='1'){

c = c + 1;

}

}

//cout<<str<<"-";

//tempstr+=c;

char ch1 = c;

out<<ch1;

//加密这一行剩下的

for(int i=leftBit;i<str.size();i++){

c<<=1;

count++;

if(str[i]=='1'){

c = c + 1;

}

// //一个字节存满,将改字节保存

if(count==8){

ch1 = c;

out<<ch1;

//tempstr+=c;

c = 0;

count=0;

}

}

//记录没有保存的字符

leftChar = c;

leftBit = 8-count;

out<<tempstr;

//cout<<"1";

}

}

void encodeHuffman(string file,string path,CodeTableItem *cTable){

//读取文件,一个个字符加密

ifstream infile;

infile.open(file.data()); //将文件流对象与文件连接起来

assert(infile.is_open()); //若失败,则输出错误消息,并终止程序运行

infile >> noskipws;

string line;

while (std::getline(infile, line)) {

encodeLine(line,path,cTable);

}

infile.close();

}

//解密

void decodeHuffman(string file){

node *p = myroot;

ifstream infile;

infile.open(file.data()); //将文件流对象与文件连接起来

assert(infile.is_open()); //若失败,则输出错误消息,并终止程序运行

unsigned char c;

infile >> noskipws;

int wordcount=0;

infile>>c;

while(!infile.eof())

{

infile>>c;

string str = "";

int beichushu = 128;

for(int i=0;i<8;i++){

int bit = (int)c/beichushu;

if(bit==1){

p = p->right;

str+= "1";

}

else{

str+= "0";

p = p->left;

}

if(p->left==NULL){

//cout<<p->ch;

out<<p->ch;

//当执行到最后一个字符就结束,不要继续输出后面多余字符

wordcount++;

if(wordcount==totalNum)

return;

p = myroot;

}

c <<= 1;

}

//cout<<str;

//cout<<wordcount;

}

infile.close(); //关闭文件输入流

}

void testWrite(string savePath){

string str = "";

for(int i=0;i<1000;i++){

str+='a';

}

ofstream ofile; //定义输出文件

ofile.open(savePath); //作为输出文件打开

for(int i=0;i<10;i++){

ofile<<str;

}

ofile.close();

}

int main(int argc, const char * argv[]) {

srand((unsigned)time(NULL));//用时间做种,每次产生随机数不一样

clock_t start,finish;

double time;

start = clock();

string file ="/Users/ish/Documents/code/c++/Huffman
976a
/cacm.all";

string encodeFile="/Users/ish/Documents/code/c++/Huffman/encode.all";

string decodefile="/Users/ish/Documents/code/c++/Huffman/decode.all";

//构建统计表

readTxt(file,CList);

//动态初始化编码表

CodeTableItem *codeTable =new CodeTableItem[cnumber];

//构建树并且建立编码表

huffman(CList,codeTable);

//输出编码表

// for(int i=0;i<cnumber;i++){

// cout<<codeTable[i].data<<"->"<<codeTable[i].code<<endl;

// }

//加密

out.open(encodeFile);

encodeHuffman(file, encodeFile,codeTable);

out.close();

//解密

out.open(decodefile);

decodeHuffman(encodeFile);

out.close();

finish=clock();

time = static_cast<double>(finish - start)/CLOCKS_PER_SEC*1000; //ms

cout<<"运行时间为:"<<time<<"ms"<<endl;

return 0;

}

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  哈夫曼压缩