哈夫曼树应用-压缩文件
大家好~时隔好几天,我来给大家讲哈夫曼树和它的应用了!
在此前,我们很少谈及c/cpp语言对字节或者bit的操作,或者说是二进制文件的操作。因为今天我内容涉及到bit以及字节的操作,所以我们不得不介绍一些c/cpp与字节的知识。
我们将从哈夫曼树开始,构造一棵哈夫曼树,补充一些c/cpp的操作,实现一个简陋的无损压缩&&解压的core。
即使操作bit很吃力,请你坚持实现,不管用什么思路,这将会让你对计算机,有更清楚的认识。甚至有时,你在编码时,脑海里会是一小片内存。
哈夫曼树
有关哈夫曼树,网上有太多的讲解。他们的讲解都十分的出色。为了节约时间,请移步如下学习。
哈夫曼树的理论,不难,希望大家能过一遍理论,再去实践。
构造哈夫曼树
在这些讲解中,普遍存在一个致命的缺陷–避重就轻,屏蔽了代码实现的最难部分。
根据身边同学的反馈。
以下是本次构造的难点
- 选择两个最小结点。
- 获取哈夫曼编码。
那么在构造这棵树之前,我们需要定义树结点的结构,以及哈夫曼树的一些操作。
我想我们应该从实践出发,因为我们要做一个压缩文件的程序,首要的就是得到哈夫曼编码。
假设我们有一棵哈夫曼树,我们应该怎么样去获取每个结点的编码呢?
请回忆我们之前所学的前序遍历。
getCode(root,"");
void HuffmanTree::getCode(node *root, std::string s) {
if(root== nullptr)return;
if(root->lChild== nullptr&&root->rChild== nullptr){
root->code=s;
}
getCode(root->lChild,s+"0");
getCode(root->rChild,s+"1");
}我们只需要一次前序遍历。在访问左节点时,s+“0”,右结点时s+“1”,当到叶子结点时,令该结点的code等于s。
眼下,我们需要构造一棵哈夫曼树。
我们很容易定义如下的结点结构。
class node{
public:
unsigned char data;
node* lChild{nullptr};
node* rChild{nullptr};
unsigned long long wight;
int parent{-1};
std::string code;
node()=default;
node(int t_data):data(t_data){}
};我们还需要一个临时的数组,自下而上的构造哈夫曼树。
但结点是n是,我们只需要一张长度位2xn-1的表就行了,因此我们采用一个指针,动态分配2xn-1空间。
n个结点的构造,我们需要这些结点的权重,一张统计文件过各个字符的数目的哈希表来初始化这些结点,再好不过了。
class HuffmanTree{
public:
node* huffmanTable; //哈夫曼树构造表
node* root;//根节点
HuffmanTree()=default;
~HuffmanTree();
HuffmanTree(std::unordered_map<unsigned char ,unsigned long long>&wightTable );//初始化结点
void forEachPre() const; //树的前序遍历,可以检测是否成功构造
};首先是通过哈希表初始化结点
HuffmanTree::HuffmanTree(std::unordered_map<unsigned char, unsigned long long>&wightTable) {
create(wightTable);
}void HuffmanTree::create(std::unordered_map<unsigned char, unsigned long long> &wightTable) {
huffmanTable = new node[2*wightTable.size()-1];
//insert wight node
int index{0}; //this is a huffmanTable point.
for(auto &w:wightTable){
huffmanTable[index].data = w.first;
huffmanTable[index].wight = w.second;
index++;
}
//important!
//find two min wight node.
for(;index<2*wightTable.size()-1;index++){
connectNode(index);
}
//return root node;
this->root = &huffmanTable[2*wightTable.size()-2];
getCode();
//std::cout<<"huffman tree create successful!"<<std::endl;
}当初始完毕后,就可以构造哈夫曼树了,上述代码不难,但我们需要实现,选择两个最小结点,这点很重要。
利用贪心算法,在排除存在父结点的结点中挑选,合成结点并插入在huffmanTable中的insert_index位置上。
判断是否存在父结点,可以用parent是否等于-1来判断。
void HuffmanTree::connectNode(int insert_index) {
//find two min wight node in huffmanTable.
//lchild and rchild;
node* l;
node* r;
unsigned long long min_temp{0};
//找到第一个没有父节点的结点
for(int i=0;i<insert_index;i++){
if(huffmanTable[i].parent==-1){
min_temp = huffmanTable[i].wight;
break;
}
}
//贪心1
for(int i=0;i<insert_index;i++){
if(huffmanTable[i].wight<=min_temp&&huffmanTable[i].parent==-1){
min_temp = huffmanTable[i].wight;
l = &huffmanTable[i];
}
}
l->parent = insert_index;
//找到第一个没有父节点的结点
for(int i=0;i<insert_index;i++){
if(huffmanTable[i].parent==-1){
min_temp = huffmanTable[i].wight;
break;
}
}
//贪心2
for(int i=0;i<insert_index;i++){
if(huffmanTable[i].wight<=min_temp&&huffmanTable[i].parent==-1){
min_temp = huffmanTable[i].wight;
r = &huffmanTable[i];
}
}
r->parent=insert_index;
//insert node
huffmanTable[insert_index].lChild = l;
huffmanTable[insert_index].rChild = r;
huffmanTable[insert_index].wight = l->wight+r->wight;
}以上操作就可以构造一棵哈夫曼树。
以下是完整代码
huffmanTree.h
//
// Created by zhujiyuan on 2022/9/23.
//
#ifndef HUFFMANZIP_HUFFMANTREE_H
#define HUFFMANZIP_HUFFMANTREE_H
#include <string>
#include <unordered_map>
class node{
public:
unsigned char data;
node* lChild{nullptr};
node* rChild{nullptr};
unsigned long long wight;
int parent{-1};
std::string code;
node()=default;
node(int t_data):data(t_data){}
};
class HuffmanTree{
public:
node* huffmanTable;
node* root;
HuffmanTree()=default;
~HuffmanTree();
HuffmanTree(std::unordered_map<unsigned char ,unsigned long long>&wightTable );
void forEachPre() const;
private:
void create(std::unordered_map<unsigned char ,unsigned long long>&wightTable );
//insert connect node
void connectNode(int insert_index);
void forEachPre(node *root) const;
void getCode();
void getCode(node *root,std::string s);
};
#endif //HUFFMANZIP_HUFFMANTREE_H
huffmanTree.cpp
//
// Created by zhujiyuan on 2022/9/23.
//
#include "huffmanTree.h"
#include <iostream>
HuffmanTree::HuffmanTree(std::unordered_map<unsigned char, unsigned long long>&wightTable) {
create(wightTable);
}
//create huffman tree with wight table.
//new node
void HuffmanTree::create(std::unordered_map<unsigned char, unsigned long long> &wightTable) {
huffmanTable = new node[2*wightTable.size()-1];
//insert wight node
int index{0}; //this is a huffmanTable point.
for(auto &w:wightTable){
huffmanTable[index].data = w.first;
huffmanTable[index].wight = w.second;
index++;
}
//important!
//find two min wight node.
for(;index<2*wightTable.size()-1;index++){
connectNode(index);
}
//return root node;
this->root = &huffmanTable[2*wightTable.size()-2];
getCode();
//std::cout<<"huffman tree create successful!"<<std::endl;
}
void HuffmanTree::connectNode(int insert_index) {
//find two min wight node in huffmanTable.
//lchild and rchild;
node* l;
node* r;
unsigned long long min_temp{0};
for(int i=0;i<insert_index;i++){
if(huffmanTable[i].parent==-1){
min_temp = huffmanTable[i].wight;
break;
}
}
for(int i=0;i<insert_index;i++){
if(huffmanTable[i].wight<=min_temp&&huffmanTable[i].parent==-1){
min_temp = huffmanTable[i].wight;
l = &huffmanTable[i];
}
}
l->parent = insert_index;
for(int i=0;i<insert_index;i++){
if(huffmanTable[i].parent==-1){
min_temp = huffmanTable[i].wight;
break;
}
}
for(int i=0;i<insert_index;i++){
if(huffmanTable[i].wight<=min_temp&&huffmanTable[i].parent==-1){
min_temp = huffmanTable[i].wight;
r = &huffmanTable[i];
}
}
r->parent=insert_index;
//insert node
huffmanTable[insert_index].lChild = l;
huffmanTable[insert_index].rChild = r;
huffmanTable[insert_index].wight = l->wight+r->wight;
}
void HuffmanTree::forEachPre(node *root) const {
if(root== nullptr)return;
if(root->lChild== nullptr&&root->rChild== nullptr)
std::cout<<char(root->data)<<" ";
forEachPre(root->lChild);
forEachPre(root->rChild);
}
void HuffmanTree::forEachPre() const {
forEachPre(root);
}
void HuffmanTree::getCode(node *root, std::string s) {
if(root== nullptr)return;
if(root->lChild== nullptr&&root->rChild== nullptr){
root->code=s;
}
getCode(root->lChild,s+"0");
getCode(root->rChild,s+"1");
}
void HuffmanTree::getCode() {
getCode(this->root,"");
}
HuffmanTree::~HuffmanTree() {
if(huffmanTable!= nullptr){
delete[] huffmanTable;
huffmanTable = nullptr;
}
}
压缩与解压
压缩难在,操作字节。
我们先不去考虑这个最难的部分。
每个文件的大小都是字节的整数倍,我们可以字节作为结点的数据,统计这个字节出现过多少次,来构造哈夫曼树。
一个字节0-255,所以最多有256个结点。
构造完哈夫曼树之后,开始编码。
逐个字节读取源文件,把这个字节转成哈夫曼编码。
[用一个字符串存储每个字节的哈夫曼编码]{.red},这样每个8个字符,写入一个字节就行啦。
要注意的是,最后可能不满8个字节,需要在后面补0,补足8位。
这就是压缩的过程啦。
解压的过程,可以通过逐位读取压缩文件
- 0访问左子树
- 1访问右子树
- 叶子结点,把结点的字节,写入解压文件中。
思路不一,以下是这个思路的实现代码。
compress.h
//
// Created by zhujiyuan on 2022/9/25.
//
#ifndef HUFFMANZIP_COMPRESS_H
#define HUFFMANZIP_COMPRESS_H
#include "huffmanTree.h"
#include <unordered_map>
class FileIO{
public:
unsigned char*buf;
long buf_size;
FileIO()=default;
void readIntoBuf(const char* fileName);
};
class Compress:public FileIO{
public:
HuffmanTree *h_tree;
int endBit;//add 0 number
std::unordered_map<unsigned char,unsigned long long>hash;
Compress()=default;
Compress(const char* filename);
void compressFile();
void decompressFile();
private:
void init(const char* filename);
inline unsigned char strBinToUchar(const std::string&s);
inline std::string ucharToStrBin(unsigned char s);
};
inline unsigned char Compress::strBinToUchar(const std::string &s) {
unsigned char sum{0};
for(const char&a:s){
sum = sum*2 + (a-'0');
}
return sum;
}
inline std::string Compress::ucharToStrBin(unsigned char s){
std::string ans = "";
while(s!=0){
if(s%2!=0){
ans = '1'+ans;
}else{
ans = '0'+ans;
}
s/=2;
}
while(ans.length()!=8){
ans = '0'+ans;
}
return ans;
}
#endif //HUFFMANZIP_COMPRESS_H
compress.cpp
//
// Created by zhujiyuan on 2022/9/25.
//
#include "compress.h"
#include <unordered_map>
#include <iostream>
#include <string.h>
#include "processBar.h"
void FileIO::readIntoBuf(const char *fileName) {
FILE * q = fopen(fileName,"rb");
//get file size
fseek(q,0,SEEK_END);
this->buf_size = ftell(q);
//create file size buf
this->buf = new unsigned char[buf_size];
fseek(q,0,SEEK_SET);
fread(this->buf,1,buf_size,q);
fclose(q);
}
Compress::Compress(const char *filename) {
init(filename);
}
void Compress::init(const char *filename) {
readIntoBuf(filename);
for(long i{0};i<buf_size;i++){
hash[buf[i]]++;
}
h_tree = new HuffmanTree(hash);
}
void Compress::compressFile() {
//create find table,key is node's data,value is node's code.
std::unordered_map<unsigned char,std::string> quickTable;
for(int i{0};i<hash.size();i++){
quickTable[this->h_tree->huffmanTable[i].data]=this->h_tree->huffmanTable[i].code;
}
FILE *q = fopen("test.hip","wb");
//start compress!!!
std::string byteBuf="";
unsigned char streamBuf[1048576];
memset(streamBuf,0,sizeof(streamBuf));
int cnt{0};
for(long i{0};i<buf_size;i++){
byteBuf.append(quickTable[buf[i]]);
}
//add 0 to bytrBuf
endBit=0;
while(byteBuf.length()%8!=0){
byteBuf.push_back('0');
endBit++;
}
//delete buf
if(buf!= nullptr){
delete[]buf;
buf = nullptr;
}
//write into stream buf.
std::cout<<"start compress file."<<std::endl;
ProcessBar bar(byteBuf.length());
for(long i{0};i<byteBuf.length();i+=8){
streamBuf[cnt++] = strBinToUchar(byteBuf.substr(i,8));
if(cnt%1048576==0){
cnt=0;
fwrite(streamBuf,sizeof(streamBuf),1,q);
}
bar.show(8);
}
if(cnt!=0){
fwrite(streamBuf,sizeof(unsigned char)*cnt,1,q);
}
fclose(q);
std::cout<<std::endl<<"compress over!"<<std::endl;
}
void Compress::decompressFile() {
node* sign = this->h_tree->root;
unsigned char streamBuf[1048576];
memset(streamBuf,0,sizeof(streamBuf));
FILE *q = fopen("test.hip","rb");
FILE *uq = fopen("test.unhip","wb");
int cnt{0};
std::string codeTxt="";
unsigned char t;
while(fread(&t,sizeof(unsigned char),1,q)!=0){
codeTxt.append(ucharToStrBin(t));
}
while(endBit!=0){
codeTxt.pop_back();
endBit--;
}
//std::cout<<codeTxt<<std::endl;
fclose(q);
std::cout<<"start decompress file."<<std::endl;
ProcessBar bar(codeTxt.length());
for(const char&s:codeTxt){
if(s=='0'){
sign=sign->lChild;
}else{
sign=sign->rChild;
}
if(sign->rChild== nullptr&&sign->lChild== nullptr){
streamBuf[cnt++]=sign->data;
sign=this->h_tree->root;
if(cnt%1048576==0){
fwrite(streamBuf,sizeof (streamBuf),1,uq);
cnt=0;
}
}
bar.show();
}
if (cnt!=0){
fwrite(streamBuf,sizeof (unsigned char )*cnt,1,uq);
}
fclose(uq);
std::cout<<std::endl<<"decompress over!"<<std::endl;
}
有关c语言的知识,可以参考如下内容
完整代码=>github
后续可能会优化压缩代码。逃~