Huffman樹,又稱爲最優二叉樹,是加權路徑最短的二叉樹。Huffman樹的構建利用到貪心算法。
【貪心算法】
貪心算法是指在問題求解時,總是做出當前最好的選擇,也就是說貪心算法做出的不一定是全局的最優解,但是某種意義上的局部最優解。貪心算法不一定能夠求得整體的最優解。
使用貪心算法構建哈夫曼樹:
/*
主要原理:將每個字符與哈夫曼編碼相對應,
壓縮:統計字符出現的次數-》構建哈夫曼樹-》生成哈夫曼編碼-》壓縮到新的文件中
解壓:讀壓縮後的文件-》將哈夫曼編碼與字符相對應-》將字符寫入新的文件中
—次數多的字符路徑短,次數少的字符路徑長
*/
--heap.h
#pragma once
//堆
#include <vector>
#include <assert.h>
//添加仿函數
template<class T>
struct Less
{
bool operator()(const T* L, const T* R)
{
return L < R;
}
};
template<class T>
struct Greate
{
bool operator()(const T* L, const T* R)
{
return L > R;
}
};
template <class T, class compare = Less> //Less爲默認的類型
//template <class T, template<class> compare = Less> //模板的模板參數
class Heap
{
public:
Heap() //無參構造
:_size(0)
{}
Heap(vector<T>& a) //拷貝構造
{
_a.swap(a); //vector中swap函數?
//建堆
for (int i = (_a.size() - 2) / 2; i >= 0; --i)
{
AdjustDown(i);
}
}
Heap(const T* a, size_t size)
{
_a.reverse(size); //申請空間
for (int i = 0; i < size; i++) //將所有元素壓入容器中
{
_a.push_back(a[i]);
}
for (int i = (_a.size() - 2) / 2; i >= 0; i--) //i爲元素下標,調整堆
{
_AdjustDown(i);
}
}
void Push(const T& x) //將數據插入堆中
{
_a.push_back(x);
AdjustUp(_a.size() - 1); //進行向上調整
++_size;
}
void Pop() //刪除數據
{
size_t size = _a.size();
assert(size > 0);
//將堆的頭元素和尾元素進行交換,然後進行pop,對堆進行調整
swap(_a[0], _a[size - 1]);
_a.pop_back();
AdjustDown(0);
--_size;
}
T Top() //堆頂上元素
{
return _a[0];
}
size_t Size() //求堆中數據的個數
{
return _size;
}
public:
void AdjustDown(size_t parent) //將堆進行下調(小堆)
{
size_t child = parent * 2 + 1; //計算孩子節點的下標
size_t size = _a.size();
while (child < size)
{
compare com;
if (child + 1 < size && com(_a[child + 1], _a[child])) //尋找左右孩子中最小的
{
++child;
}
if (com(_a[child], _a[parent])) //將最大的孩子與父節點進行比較
{
swap(_a[parent], _a[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void AdjustUp(size_t child) //上調
{
size_t parent = (child - 1) / 2;
while (child > 0)
{
compare com;
if (com(_a[child], _a[parent]))
{
swap(_a[child], _a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
protected:
vector<T> _a;
size_t _size;
};
--Huffman.h
#pragma once
#include "Heap.h"
//實現哈夫曼樹
template <class T>
struct HuffmanNode
{
HuffmanNode<T>* _left; //指向哈夫曼樹的左節點的指針
HuffmanNode<T>* _right; //指向哈夫曼樹的右節點的指針
T _weight; //節點的權值
HuffmanNode(const T& x)
:_left(NULL)
, _right(NULL)
, _weight(x)
{}
};
template <class T>
class HuffmanTree
{
typedef HuffmanNode<T> Node;
public:
HuffmanTree() //進行無參構造
:_root(NULL)
{}
~HuffmanTree() //析構函數
{
_clear(_root);
}
//構造哈夫曼樹
HuffmanTree(T* a, size_t size, const T& invalid)
{
_root = CreateTree(a, size, invalid);
}
Node* GetRootNode() //獲得根節點
{
return _root;
}
protected:
Node* CreateTree(T* a, size_t size, const T& invalid)
{
struct compare //構造仿函數
{
bool operator()(const Node* dt, const Node* st)
{
return dt->_weight < st->_weight;
}
};
Heap<Node*, compare> minHeap; //創建最小堆
for (size_t i = 0; i < size; ++i)//將所有的數據壓入堆中
{
if (a[i] != invalid)
{
minHeap.Push(new Node(a[i]));
}
}
Node* parent = new Node(0);
while (minHeap.Size() > 1) //小堆的大小不爲空
{
Node* left = minHeap.Top(); //將小堆中最小的兩個數據取出
minHeap.Pop();
Node* right = minHeap.Top();
minHeap.Pop();
parent = new Node(left->_weight + right->_weight);
parent->_left = left;
parent->_right = right;
minHeap.Push(parent);
}
return parent;
}
void _clear(Node* root)
{
if (root)
{
_clear(root->_left);
_clear(root->_right);
delete root;
}
}
protected:
Node* _root;
};
--FileCompress.h
#pragma once
//利用哈夫曼樹實現文件壓縮
/*
主要原理:將每個字符與哈夫曼編碼相對應,
壓縮:統計字符出現的次數-》構建哈夫曼樹-》生成哈夫曼編碼-》壓縮到新的文件中
解壓:讀壓縮後的文件-》將哈夫曼編碼與字符相對應-》將字符寫入新的文件中
次數多的字符路徑短,次數少的字符路徑長
*/
#include "Huffman.h"
#include <string>
#include <math.h>
typedef long long LongType;
struct charInfo
{
unsigned char _ch; //字符
LongType _count; //出現的次數
string _code; //哈夫曼編碼
charInfo()
:_count(0)
, _ch(0)
{}
charInfo(const LongType& count) //構造
:_count(count)
, _ch(0)
{ }
bool operator!=(const charInfo& info)const //重載!=(常成員函數)
{
return _count != info._count;
}
charInfo operator+(const charInfo& info)const //重載+
{
return charInfo(_count + info._count);
}
bool operator<(const charInfo& info)const //重載<
{
return _count < info._count;
}
};
class FileCompress
{
public:
FileCompress() //無參構造
{
for (int i = 0; i < 256; ++i)
{
_infos[i]._count = 0;
_infos[i]._ch = i;
}
}
void compress(const char* Filename) //壓縮
{
assert(Filename);
FILE* FOut = fopen(Filename, "rb"); //打開源文件
assert(FOut); //判斷打開文件是否失敗
//使用直接定址法統計每個字符出現的次數
char ch = fgetc(FOut); //取一個字符
while (!feof(FOut))
{
_infos[(unsigned char)ch]._count++;
ch = fgetc(FOut);
}
//構建哈夫曼樹
charInfo invalid(0);
HuffmanTree<charInfo> tree(_infos, 256, invalid);
//生成哈夫曼編碼
string code;
GenerateHuffmanCode(tree.GetRootNode(),*this, code);
/*HuffmanNode<charInfo>* root = tree.GetRootNode();
GenerateHuffmanCode(root, *this, code);*/
//寫配置文件,方便解壓縮時重建HuffmanTree。
/*新建配置文件,給中間放入讀取的字符種類,不需要存每個字符讀取的次數,
可以直接利用哈夫曼樹的根節點的權值,就可以確定文件字符的總個數*/
string configFile = Filename;
string compressFileName = Filename; //新建壓縮文件
size_t last_ = configFile.find_last_of('.');
if (last_ < configFile.size())
{
configFile.erase(last_);
compressFileName.erase(last_);
}
configFile += ".config";
FILE* FInconfig = fopen(configFile.c_str(), "wb");
assert(FInconfig);
string str; //使用str來保存出現的字符
char buffer[20] = {0};
for (size_t i = 0; i < 256; ++i)
{
if (_infos[i]._count != 0)
{
str += _infos[i]._ch;
str += ':';
str += (string)_itoa(_infos[i]._count, buffer, 10);
//str += buffer;
str += '\n';
fputs(str.c_str(), FInconfig);
str.clear(); //每次對str進行清除,就能夠保存下一個出現的字符
}
}
//將文件進行壓縮
compressFileName += ".compress";
FILE* FIn = fopen(compressFileName.c_str(), "wb");
assert(FIn);
fseek(FOut, 0, SEEK_SET); //將fout文件指針移動到0的位置
ch = fgetc(FOut);
unsigned char value = 0;
int pos = 0;
while (!feof(FOut)) //將每個字符的編碼寫入文件
{
str = _infos[(unsigned char)ch]._code;
for (size_t i = 0; i < str.size(); ++i)
{
value <<= 1;
value |= (str[i] - '0');
if (++pos == 8)
{
fputc(value, FIn);
pos = 0;
value = 0;
}
}
ch = fgetc(FOut);
}
//如果編碼最後一個寫入時,一個字符的空間沒有佔滿時,採用的方式是進行補0操作
if (pos > 0)
{
value <<= (8 - pos);
fputc(value, FIn);
}
fclose(FIn);
fclose(FOut);
fclose(FInconfig);
}
void unCompress(const char* Filename) //解壓
{
assert(Filename);
FILE* FOut = fopen(Filename, "rb");
assert(FOut);
//配置文件
string configFile = (string)Filename;
string FileInName = (string)Filename;
size_t last_ = configFile.find_last_of('.'); //查找字符串中出現的最後一個‘.’
if (last_ < configFile.size())
{
configFile.erase(last_); //將後面的字符進行刪除
FileInName.erase(last_);
}
configFile += ".config";
FILE* FConfig = fopen(configFile.c_str(), "rb");
assert(FConfig);
//解壓後的文件
FileInName += "_com.txt";
FILE* FIn = fopen(FileInName.c_str(), "wb");
assert(FIn);
//修改_count,注意\n,有可能代表字符,有可能是行結束標誌
char buff[20] = { 0 };
unsigned char ch = fgetc(FConfig);
while (!feof(FConfig))
{
fgetc(FConfig);
fgets(buff, 20, FConfig);
this->_infos[ch]._count = (LongType)atoi(buff);
ch = fgetc(FConfig);
}
//重建哈夫曼樹
charInfo invalid(0); //定義非法值
HuffmanTree<charInfo> tree(_infos, 256, invalid);
HuffmanNode<charInfo>* root = tree.GetRootNode();
HuffmanNode<charInfo>* cur = root;
ch = fgetc(FOut);
int count = root->_weight._count; //記錄字符的總個數控制結束,根節點的權值表示字符的總個數
int pos = 7;
while (count > 0) //讀取文件的編碼
{
while (pos >= 0)
{
if (ch & (1 << pos))
{
cur = cur->_right;
}
else
{
cur = cur->_left;
}
if (cur->_left == NULL && cur->_right == NULL)
{
fputc(cur->_weight._ch, FIn);
if (--count == 0) //將剩餘沒有寫入的字符總次數減1
{
break;
}
cur = root;
}
--pos;
}
pos = 7;
ch = fgetc(FOut);
}
fclose(FOut);
fclose(FIn);
fclose(FConfig);
}
/*void PrintCode()const
{
for (int i = 0; i < 256; ++i)
{
if (this->_infos[i]._count != 0)
{
cout << this->_infos[i]._ch << ":>" << this->_infos[i]._code << endl;
}
}
}*/
protected:
//後序遍歷生成哈夫曼編碼(使用遞歸)
void GenerateHuffmanCode(HuffmanNode<charInfo>* root, FileCompress& file, string code)
{
if (root == NULL)
{
return;
}
if (root->_left == NULL && root->_right == NULL) //葉子節點
{
file._infos[root->_weight._ch]._code = code;
return;
}
GenerateHuffmanCode(root->_left, file, code + '0'); //string類型的可以直接進行+追加字符
GenerateHuffmanCode(root->_right, file, code + '1'); //左加0,右加1
}
protected:
charInfo _infos[256];
};
方法:
//記錄補字符的位數,
//解壓和壓縮的字符是相同的,
//源文件出現字符的次數爲哈夫曼樹的根節點的值,每處理一個字符,減值進行減減
--testRunningTime.h
#pragma once
#ifndef __TIME_CHECK_H__
#define __TIME_CHECK_H__
#include <windows.h>
class MyTimer
{
public:
MyTimer()
{
QueryPerformanceFrequency(&_freq);
costTime = 0.0;
}
void Start()
{
for (int i = 0; i<EN_NUMER; ++i)
{
QueryPerformanceCounter(&_array[i]._begin);
}
}
void Stop()
{
for (int i = 0; i<EN_NUMER; ++i)
{
QueryPerformanceCounter(&_array[i]._end);
}
}
void Reset()
{
costTime = 0.0;
}
void showTime()
{
double allTime = 0.0;
for (int i = 0; i<EN_NUMER; ++i)
{
allTime += (((double)_array[i]._end.QuadPart - (double)_array[i]._begin.QuadPart) / (double)_freq.QuadPart);
}
costTime = allTime / EN_NUMER;
costTime *= 1000000;
if ((((int)costTime) / 1000000) > 0)
{
cout << costTime / 1000000 << " s" << endl;
}
else if (((int)costTime) / 1000 > 0)
{
cout << costTime / 1000 << " ms" << endl;
}
else
{
cout << costTime << " us" << endl;
}
}
private:
class Array
{
public:
LARGE_INTEGER _begin;
LARGE_INTEGER _end;
};
enum{ EN_NUMER = 5 };
LARGE_INTEGER _freq;
double costTime;
Array _array[EN_NUMER];
};
#endif
--test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include "FileCompress.h"
#include "testRunningTime.h"
#include "testRunningTime.h"
void Test()
{
string filename = "1.txt";
FileCompress ht;
ht.compress(filename.c_str());
//ht.PrintCode();
string filename1 = "1.compress";
ht.unCompress(filename1.c_str());
}
void Test1() //測試壓縮時間
{
//string filename = "Input.BIG";
cout << "壓縮時間";
MyTimer timer;
timer.Start();
FileCompress ht;
ht.compress("Input.BIG");
timer.Stop();
timer.showTime();
}
void Test2() //測試解壓時間
{
//string filename = "compressFileName";
cout << "解壓時間";
MyTimer timer;
timer.Start();
FileCompress ht;
ht.unCompress("Input.compress");
timer.Stop();
timer.showTime();
}
int main()
{
//Test();
Test1();
Test2();
system("pause");
return 0;
}