前言
一個簡單的壓縮軟件,利用哈夫曼思想,構造哈夫曼編碼,實現對文件的二進制壓縮,以及解壓,再利用MFC製作可視化操作界面,美化軟件又簡化文件操作。(各個步驟有解釋可看)
軟件主頁面先看看
哈夫曼樹結構
構造哈夫曼樹存儲結構:w權重即每個字節出現頻度,byte結點數據即每個字節的ASCII碼,fa雙親結點下標,le左孩子下標,ri右孩子下標,從下往上開始構建哈夫曼樹。
根據已構造完成的哈夫曼樹,從上往下開始構造每個結點的哈夫曼編碼字符串,從根節點出發,如果下一個節點是其雙親的右孩子結點則在編碼後接1,如果是左孩子結點則在編碼後接0.存放哈夫曼樹信息用到的是Huff_arr數組。
struct HaffNode {
unsigned char byte;//爲節點所代表的字符(ASCII碼錶對應的字符)
long long w;//此節點代表字符的出現頻度
int num, fa, le, ri, code_len;//分別爲節點在Huff_arr數組下標,雙親節點在Huff_arr數組下標,
左子樹下標,右子樹下標,對應哈夫曼編碼長度
char code[256];//哈夫曼編碼
bool operator < (HaffNode x) const {
return w > x.w;
}
}Huff_arr[512]
步驟
①讀取文件操作(包括初始化)
1、 讀原文件,統計字節頻度,定義Huffman樹和Huffman編碼的儲存結構
讀取文件,新建一個二進制文件用於存放統計數據,用while語句逐個讀取源文件每一個字節,在每次讀取的時候分別統計出現次數(權值)w和源文件長度file_length,直至文件結束。
2、 原文件字節頻度統計
對字節頻度排序,利用sort函數對Huff_arr[0]~ Huff_arr[520]的元素以weight爲排序關鍵字進行降序排序。
void initpow(char *cp_inname) {
unsigned char ch;
CString str;
FILE *ifp = fopen(cp_inname, "rb");
if (ifp == NULL) {
MessageBox(NULL, _T("該文件已存在,請重新輸入"), _T("錯誤"), MB_ICONEXCLAMATION);
return;
}
file_len = 0, bytes_cnt = 0;
fread(&ch, 1, 1, ifp);
while (!feof(ifp)) {
Huff_arr[ch].w++, file_len++;
fread(&ch, 1, 1, ifp);
}
fclose(ifp);
for (int i = 0; i < 256; i++)
if (Huff_arr[i].w > 0)
bytes_cnt++;
sort(Huff_arr, Huff_arr + 511);
}
②建樹
利用優先隊列(priority_queue QUEUE;),每次取隊頭元素(權值第一小)First節點,出隊後再取隊頭元素S(權值第二小)econd,然後將兩棵子樹合併爲一棵子樹,權值相加,並將新子樹的根節點順序存放到數組huff_arr ,再把新子樹的根節點放進隊列再次循環步驟,直到隊列的個數爲1爲止。
void createhafftree() {
priority_queue<HaffNode> QUEUE;
HaffNode First, Second, Sum;
int tot = bytes_cnt;
while (QUEUE.size())QUEUE.pop();
for (int i = 0; i < bytes_cnt; i++)
Huff_arr[i].num = i, QUEUE.push(Huff_arr[i]);
while (QUEUE.size() > 1) {
First = QUEUE.top(), QUEUE.pop();
Second = QUEUE.top(), QUEUE.pop();
Sum.num = tot, Sum.w = First.w + Second.w;
Sum.fa = -1, Sum.le = First.num, Sum.ri = Second.num;
strcpy(Sum.code, "");
Huff_arr[First.num].fa = Sum.num, Huff_arr[Second.num].fa = Sum.num;
Huff_arr[tot++] = Sum;
QUEUE.push(Sum);
}
③構造哈夫曼編碼
根據已構造完成的哈夫曼樹,從上往下開始構造每個結點的哈夫曼編碼字符串,從根節點出發,如果下一個節點是其雙親的右孩子結點則在編碼後接1,如果是左孩子結點則在編碼後接0.哈夫曼編碼樹的左分支代表 0,右分支代表 1,則從根結點到每個葉子結點所經過的路徑組成的 0 和 1 的序列便成爲該葉子結點對應字符的編碼。
void createhaffcode() {
int tot = bytes_cnt * 2 - 1;
Huff_arr[tot - 1].code[0] = '\0';
for (int i = tot - 2; i >= 0; i--) {
strcpy(Huff_arr[i].code, Huff_arr[Huff_arr[i].fa].code);
if (Huff_arr[Huff_arr[i].fa].ri == i)
strcat(Huff_arr[i].code, "1");
else
strcat(Huff_arr[i].code, "0");
Huff_arr[i].code_len = strlen(Huff_arr[i].code);
}
}
④生成壓縮文件
生成壓縮碼:先找出根節點的位置,然後從根節點一直往下進行編碼,根據左孩子置爲0,右孩子置爲1這個規則一直往下編碼,根據編碼繼承,可以直接在父節點的編碼後面置0或1即可。
根據編碼寫入文件:得到哈夫曼編碼後先將緩衝區置爲空,然後按照每8位爲一個字節,將二進制轉爲十進制進行寫入文件,如果最後緩衝區還有元素,則在後面補8個0,然後再整除8,變爲8位元素。
ofp = fopen(cp_outname, "wb");
if (ofp == NULL) {
MessageBox(NULL, _T("未能成功打開文件"), _T("錯誤"), MB_ICONEXCLAMATION);
return;
}
fprintf(ofp, "%d,%s,%lld,%d,", strlen(file_extension), file_extension, file_len, bytes_cnt);
for (int i = 0; i < bytes_cnt; i++)
fprintf(ofp, "%c,%lld,", Huff_arr[i].byte, Huff_arr[i].w);
ifp = fopen(cp_inname, "rb");
if (ifp == NULL) {
MessageBox(NULL, _T("打開文件失敗"), _T("錯誤"), MB_ICONEXCLAMATION);
return;
}
strcpy(buff, "");
ch = fgetc(ifp);
while (!feof(ifp)) {
if (Buffmax - strlen(buff) > 256) {
for (int i = 0; i < bytes_cnt; i++) {
if (Huff_arr[i].byte == ch) {
strcat(buff, Huff_arr[i].code), ch = fgetc(ifp);
break;
}
}
}
else {
flushBuffer(ofp);
}
}
flushBuffer(ofp);
if (strlen(buff) > 0) {
strcat(buff, "00000000");
flushBuffer(ofp);
strcpy(buff, "");
}
fclose(ofp);
fclose(ifp);
注意:
生成壓縮文件一定要在文件裏面記錄相應的擴展名以及哈夫曼樹的重要存儲結構,即源文件對應的字符和字符頻度,在將哈夫曼編碼每八位轉成一個十進制值對應的字符時,有可能哈夫曼編碼不是8的整數倍,需要在哈夫曼編碼最後面補充8個0,多餘的哈夫曼編碼便可借0補位,以此避免二進制文件寫入錯誤。
爲了讀文件快點,利用緩衝區
void flushBuffer(FILE * fp) { // 把緩衝區中,儘可能多的字節,寫入文件中
strcpy(bufstr, "");
unsigned char temp = 0;
int byte_data_num = strlen(buff) / 8, i;
for (i = 0; i < byte_data_num; i++) {
temp = 0;
for (int j = 0; j < 8; j++) {
if (buff[i * 8 + j] == '1')
temp += pow(2, 7 - j);
}
bufstr[i] = temp;
}
bufstr[i] = '\0';
fwrite(bufstr, 1, byte_data_num, fp);
strcpy(buff, buff + byte_data_num * 8);
}
⑤解壓文件
1、 讀壓縮文件的頭部
(1) 讀源文件的擴展名長度,把擴展名存儲以便生成解壓文件可用來定義文件類型,讀入源文件的總字節數,讀入源文件中被編碼的字節總數
(2) 根據(1)中讀入的被編碼的字節總數,依此讀取字符和字符頻度,初始化哈夫曼樹存儲結構,構造哈夫曼樹
(3) 讀取哈夫曼總編碼生成的二進制數據。
2、 對壓縮文件進行解壓
(1) 讀取分哈夫曼總編碼生成的二進制數據分批次裝滿緩衝區,寫入文件
(2) 緩衝區內的下一位,若是0,則轉向左孩子,若是1,則轉向右孩子
(3) 找出葉子節點,並把該字節寫入解壓文件中,即是還原每個節點對應的哈夫曼編碼,找出每個哈夫曼編碼對應的節點,將節點對應的ASCII碼的字符寫入生成文件
ifp = fopen(dcp_inname, "rb");
if (ifp == NULL) {
MessageBox(NULL, _T("此壓縮文件不存在或被佔用!"), _T("錯誤"), MB_ICONEXCLAMATION);
return;
}
strcpy(dat_file_extension, "");
fscanf(ifp, "%d,", &sufname_len);
fread(&dat_file_extension, sufname_len, 1, ifp);
fscanf(ifp, ",%lld,%d,", &file_len, &bytes_cnt);
for (int i = 0; i < bytes_cnt; i++)
fscanf(ifp, "%c,%lld,", &Huff_arr[i].byte, &Huff_arr[i].w);
//構造哈弗曼樹並輸出
createhafftree();
/*printhafftree();*/
//生成文件絕對路徑
strcat(dcp_outname, ".");
strcat(dcp_outname, dat_file_extension);
//解壓
ofp = fopen(dcp_outname, "wb");
if (ofp == NULL) {
MessageBox(NULL, _T("解壓文件生成失敗!"), _T("錯誤"), MB_ICONEXCLAMATION);
return;
}
strcpy(buff, ""), strcpy(block, "");
fread(buff, 1, Buffmax - 1, ifp);
root = bytes_cnt * 2 - 2, trcur = root;
while (dfile_len < file_len) {
if (blcur >= Blockmax - 1) {
fwrite(block, 1, blcur, ofp);
blcur = 0;
}
if (Huff_arr[trcur].le == -1) {
block[blcur++] = Huff_arr[trcur].byte, block[blcur] = '\0';
trcur = root, dfile_len++;
if (blcur == 510) {
int xxdx = 1;
xxdx++;
}
}
else {
if ((buff[bucur] >> (7 - bycur)) & 1)
trcur = Huff_arr[trcur].ri;
else
trcur = Huff_arr[trcur].le;
if (bycur < 7)
bycur++;
else {
bycur = 0, bucur++;
if (bucur >= Buffmax - 1)
fread(buff, 1, Buffmax - 1, ifp), bucur = 0;
}
}
}
fwrite(block, 1, blcur, ofp);
fclose(ifp), fclose(ofp);
MFC主要三個按鈕響應事件代碼
void CHuffmanDlg::OnBnClickedButton1() //壓縮文件按鈕對應的事件
{
// TODO: 在此添加控件通知處理程序代碼
CString strFile = _T("");
CString str3;
str3.Format(_T("請選擇所需要進行壓縮的文件:"));
if (MessageBox(str3, _T("提示"), MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL) {
return;
}
else {
CFileDialog dlgFile(TRUE, NULL, NULL, OFN_HIDEREADONLY, _T("Describe Files All Files (*.*)|*.*||"), NULL);
if (dlgFile.DoModal())
{
strFile = dlgFile.GetPathName();
}
}
if (strFile == "")
return;
CString str4;
Edit_text YS;
str4.Format(_T("請選擇是否爲生成文件重新命名:"));
named_ok = false;
//是否進行對生成文件的命名
if (MessageBox(str4, _T("選擇"), MB_ICONQUESTION | MB_YESNO) == IDNO) {
named_ok = false;
}
else {
named_ok = true;
YS.DoModal();
}
//對文件名進行修改
USES_CONVERSION;
char * inFileName = T2A(strFile);
int ok = 0;
int pos1 = 0,pos2;
strcpy(cp_file_name, "");
strcpy(file_extension, "");
char temp[MAX_PATH] = "";
for (int i = strlen(inFileName) - 1; i >= 0; i--) {
if (inFileName[i] == '\\') {
break;
}
if (ok == 1) {
temp[pos1++] = inFileName[i];
}
if (inFileName[i] == '.') {
pos2 = i + 1;
ok = 1;
}
}
int tot = 0;
for (int i = pos2; i < strlen(inFileName); i++)
file_extension[tot++] = inFileName[i];
int num = 0;
for (int i = pos1 - 1; i >= 0; i--) {
cp_file_name[num++] = temp[i];
}
strcat(cp_file_name, ".dat");
CString NAME= YS.FILE_NAME + ".dat";//文本框傳來的信息
if (!named_ok)
NAME= CA2CT(cp_file_name);
char szPath[MAX_PATH];//存放選擇的目錄路徑
CString str1, str2, FileName;
CTime m_time;
ZeroMemory(szPath, sizeof(szPath));
BROWSEINFO bi;
bi.hwndOwner = m_hWnd;
bi.pidlRoot = NULL;
bi.pszDisplayName = (LPWSTR)szPath;
bi.lpszTitle = _T("請選擇生成文件的目錄:");
bi.ulFlags = BIF_BROWSEINCLUDEFILES | BIF_NEWDIALOGSTYLE;
bi.lpfn = NULL;
bi.lParam = 0;
bi.iImage = 0;
LPITEMIDLIST lp = SHBrowseForFolder(&bi);
FileName = m_time.Format(NAME);
SHGetPathFromIDList(lp, (LPWSTR)szPath);
str2.Format(_T("%s"), szPath);
CString filePath = str2 + "\\" + FileName;//路徑+文件名
if (lp && SHGetPathFromIDList(lp, (LPWSTR)szPath))
{
str1.Format(_T("選擇生成文件的路徑爲: %s"), szPath);
if (MessageBox(str1, _T("路徑"), MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL) {
return;
}
else {
USES_CONVERSION;
//函數T2A和W2A均支持ATL和MFC中的字符
char * outFileName = T2A(filePath);
clock_t clockBegin, clockEnd;
clockBegin = clock();
CFile cfile;
DOUBLE size1, size2;
if (cfile.Open(strFile, CFile::modeRead))
{
size1 = cfile.GetLength();
}
cfile.Close();
compressFile(inFileName, outFileName);
clockEnd = clock();
DOUBLE TIME = (clockEnd - clockBegin)/( CLOCKS_PER_SEC);
if (cfile.Open(filePath, CFile::modeRead))
{
size2 = cfile.GetLength();
}
cfile.Close();
UpdateData(FALSE);
CString TIMESTR;
size1 /= 1024;
size2 /= 1024;
DOUBLE YSL = size2/ size1 * 100;
char s = '%';
TIMESTR.Format(_T("壓縮文件耗時爲:%.2lfs\n起始文件大小爲:%.2lfKB\n壓縮文件大小爲:%.2lfKB\n文件的壓縮率爲:%.2lf%c"), TIME,size1, size2,YSL,s);
MessageBox(TIMESTR, _T("壓縮成功"));
}
}
else
{
AfxMessageBox(_T("無效的目錄,請重新選擇"));
return;
}
}
void CHuffmanDlg::OnBnClickedButton2()//解壓文件按鈕對應的事件
{
// TODO: 在此添加控件通知處理程序代碼
CString strFile = _T("");
CString str3;
str3.Format(_T("請選擇所需要進行解壓的文件:"));
if (MessageBox(str3, _T("提示"), MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL) {
return;
}
else {
CFileDialog dlgFile(TRUE, NULL, NULL, OFN_HIDEREADONLY, _T("Describe Files All Files (*.*)|*.*||"), NULL);
if (dlgFile.DoModal())
{
strFile = dlgFile.GetPathName();
}
}
if (strFile == "")
return;
CString str4;
JIEYA_FILENAME JY;
str4.Format(_T("請選擇是否爲生成文件重新命名:"));
named_ok = false;
if (MessageBox(str4, _T("選擇"), MB_ICONQUESTION | MB_YESNO) == IDNO) {
named_ok = false;
}
else {
named_ok = true;
JY.DoModal();
}
//對文件名進行修改
USES_CONVERSION;
char * inFileName = T2A(strFile);
int ok = 0;
int pos = 0;
strcpy(dcp_file_name, "");
char temp[MAX_PATH] = "";
for (int i = strlen(inFileName) - 1; i >= 0; i--) {
if (inFileName[i] == '\\') {
break;
}
if (ok == 1) {
temp[pos++] = inFileName[i];
}
if (inFileName[i] == '.') {
ok = 1;
}
}
int num = 0;
for (int i = pos - 1; i >= 0; i--) {
dcp_file_name[num++] = temp[i];
}
CString NAME = JY.TEXT_NAME;//文本框傳來的信息
if (!named_ok)
NAME = CA2CT(dcp_file_name);
char szPath[MAX_PATH];//存放選擇的目錄路徑
CString str1, str2, FileName;
CTime m_time;
ZeroMemory(szPath, sizeof(szPath));
BROWSEINFO bi;
bi.hwndOwner = m_hWnd;
bi.pidlRoot = NULL;
bi.pszDisplayName = (LPWSTR)szPath;
bi.lpszTitle = _T("請選擇生成文件的目錄:");
bi.ulFlags = BIF_BROWSEINCLUDEFILES | BIF_NEWDIALOGSTYLE;
bi.lpfn = NULL;
bi.lParam = 0;
bi.iImage = 0;
LPITEMIDLIST lp = SHBrowseForFolder(&bi);
FileName = m_time.Format(NAME);
SHGetPathFromIDList(lp, (LPWSTR)szPath);
str2.Format(_T("%s"), szPath);
CString filePath = str2 + "\\" + FileName;//路徑+文件名無擴展名
if (lp && SHGetPathFromIDList(lp, (LPWSTR)szPath))
{
str1.Format(_T("選擇生成文件的路徑爲: %s"), szPath);
if (MessageBox(str1, _T("路徑"), MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL) {
return;
}
else {
USES_CONVERSION;
char * outFileName = T2A(filePath);
clock_t clock1, clock2;
clock1 = clock();
deCompressFile(inFileName, outFileName);
clock2 = clock();
DOUBLE TIME = (clock2 - clock1)/ (CLOCKS_PER_SEC);
CString TIMESTR;
UpdateData(FALSE);
TIMESTR.Format(_T("\t解壓成功!!!\t\n\t解壓文件耗時爲:%.2lfs\t"), TIME);
MessageBox(TIMESTR, _T("信息提示"));
}
}
else
{
AfxMessageBox(_T("無效的目錄,請重新選擇"));
return;
}
}
void CHuffmanDlg::OnBnClickedButton3()//退出按鈕對應的事件
{
// TODO: 在此添加控件通知處理程序代碼
CString str3;
str3.Format(_T("是否確定要退出程序?"));
if (MessageBox(str3, _T("提醒"), MB_ICONEXCLAMATION | MB_OKCANCEL) == IDCANCEL) {
return;
}
else {
PostQuitMessage(0);
}
}
最後操作界面演示
啓動界面
①壓縮文件
(1)選擇所要壓縮的文件
(2)可爲生成的壓縮文件命名,並選擇生成文件目錄
(3)壓縮完成後,會顯示:壓縮耗時,起始文件大小,壓縮文件大小,文件壓縮率。
②解壓文件
(1)選擇所要解縮的文件
(2)可爲生成的解壓文件命名,並選擇生成文件目錄
(3)解壓完成後,會顯示解壓時間
最後的最後完整包
由於利用了MFC 有一堆MFC的頭文件和源文件,所以就不可能把所有代碼貼上來了,製作了一個完整的壓縮文件包可以供下載。
圖中①是exe文件打開就可以運行,圖中②是程序所有的文件,③可以用VS打開來,裏面就是主要的代碼。
打開後,主要的核心代碼在這個CPP裏面