基於哈夫曼編碼的壓縮解壓程序(C 語言)


 這個程序是研一上學期的課程大作業。當時,跨專業的我只有一點 C 語言和數據結構基礎,為此,我查閱了不少資料,再加上自己的思考和分析,實現后不斷調試、測試和完善,耗時一周左右,在 2012/11/19 完成。雖然這是一個很小的程序,但卻是我完成的第一個程序。

源碼托管在 Github:點此打開鏈接

以下為完整的作業報告:

一、問題描述

名稱:基於哈夫曼編碼的文件壓縮解壓

目的:利用哈夫曼編碼壓縮存儲文件,節省空間

輸入:任何格式的文件(壓縮)或壓縮文件(解壓)

輸出:壓縮文件或解壓后的原文件

功能:利用哈夫曼編碼壓縮解壓文件

性能:快速

二、問題的初步討論

為了建立哈夫曼樹,首先掃描源文件,統計每類字符出現的頻度(出現的次數),然后根據字符頻度建立哈夫曼樹,接着根據哈夫曼樹生成哈夫曼編碼。再次掃描文件,每次讀取8bits,根據“字符—編碼”表,匹配編碼,並將編碼存入壓縮文件,同時存入編碼表。解壓時,讀取編碼表,然后讀取編碼匹配編碼表找到對應字符,存入文件,完成解壓。

三、總的UML協同圖

clip_image001

四、文件讀取方式和處理單元的分析

壓縮解壓的第一步就是讀取文件,為了能夠處理任何格式的文件,采用二進制方式讀寫文件。以一個無符號字符(unsigned char)的長度8位為處理單元,最多有256(0~255)種組合,即256類字符。

五、字符頻度掃描的分析

要建立哈夫曼樹,先要得到各類字符的頻度,我想到了兩種掃描方案:

1、利用鏈表存儲,每掃描到一類新字符就動態分配內存;

2、利用數組,靜態分配256個空間,對應256類字符,然后用下標隨機存儲。

鏈表在需要時才分配存儲空間,可以節省內存,但是每加入一個新字符都要掃描一次鏈表,很費時;考慮到僅有256個字符種類,不是很多,使用靜態數組,不會造成很大的空間浪費,而可以用數組的下標匹配字符,不需掃描數組就可以找到每類字符的位置,達到隨機存儲的目的,效率有很大的提高。當然,不一定每類字符都出現,所以,統計完后,需要排序,將字符頻度為零的結點剔除。

我定義的數組類似這樣:Node array[CHAR_KINDS],其中CHAR_KINDS為8位無符號字符對應的256(0~255)種不同組合,這樣每掃描到一個字符,直接將字符作為下標,就可以找到字符的位置。

六、建立哈夫曼樹的分析

哈夫曼樹為二叉樹,樹結點含有權重(在這里為字符頻度,同時也要把頻度相關聯的字符保存在結點中)、左右孩子、雙親等信息。

考慮到建立哈夫曼樹所需結點會比較多,也比較大,如果靜態分配,會浪費很大空間,故我們打算用動態分配的方法,並且,為了利用數組的隨機訪問特性,也將所需的所有樹節點一次性動態分配,保證其內存的連續性。另外,結點中存儲編碼的域,由於長度不定,也動態分配內存。

6.1、這時,針對上面的字符掃描結點就要做一些改動

將其定義成臨時結點TmpNode,這個結點僅保存字符及對應頻度,也用動態分配,但是一次性分配256個空間,統計並將信息轉移到樹結點后,就將這256個空間釋放,既利用了數組的隨機訪問,也避免了空間的浪費。

七、生成哈夫曼編碼的分析

每類字符對應一串編碼,故從葉子結點(字符所在結點)由下往上生成每類字符對應的編碼,左‘0’,右‘1’。為了得到正向的編碼,設置一個編碼緩存數組,從后往前保存,然后從前往后拷貝到葉子結點對應編碼域中,根據上面“建立哈夫曼樹的協商”的約定,需要根據得到的編碼長度為編碼域分配空間。對於緩存數組的大小,由於字符種類最多為256種,構建的哈夫曼樹最多有256個葉子結點,樹的深度最大為255,故編碼最長為255,所以分配256個空間,最后一位用於保存結束標志。

八、文件壓縮的分析

上面協定以8位的字符為單元編碼,這里壓縮當然也以8位為處理單元。

首先將字符及種類和編碼(編碼表)存儲於壓縮文件中,供解壓時使用。

然后以二進制打開源文件,每次讀取一個8位的無符號字符,循環掃描匹配存儲於哈夫曼樹節點中的編碼信息。

由於編碼長度不定,故需要一個編碼緩存,待編碼滿足8位時才寫入,文件結束時緩存中可能不足8位,在后面補0,湊足8位寫入,並將編碼的長度隨后存入文件。

在哈夫曼樹節點中,編碼的每一位都是以字符形式保存的,占用空間很大,不可以直接寫入壓縮文件,故需要轉為二進制形式寫入;至於如何實現,可以定義一個函數,將保存編碼的字符數組轉為二進制,但是比較麻煩,效率也不高;正好,可以利用C語言提供的位操作(與、或、移位)來實現,每匹配一位,用“或”操作存入低位,並左移一位,為下一位騰出空間,依次循環,滿足8位就寫入一次。

8.1、壓縮文件的存儲結構

clip_image002

結構說明:字符種類用來判斷讀取的字符、頻度序偶的個數,同時用來計算哈夫曼結點的個數;文件長度用來控制解碼生成的字符個數,即判斷解碼結束。

九、文件解壓的分析

以二進制方式打開壓縮文件,首先將文件前端的字符種類數讀取出來,據此動態分配足夠空間,然后將隨后的字符—編碼表讀取處理保存到動態分配的結點中,然后以8位為處理單元,依次讀取隨后的編碼匹配對應的字符,這里對比編碼依然用在文件壓縮中所用的方法,就是用C語言的位操作,同0x80與操作,判斷8bits字符的最高位是否為‘1’,對比一位后,左移一位,將最高位移除,次高位移到最高位,依次對比。這次是從編碼到字符反向匹配,與壓縮時有一點不同,需要用讀取的編碼逐位與編碼表中的編碼進行對比,對比一位后,增加一位再對比,而且每次對比都是一個循環(與每個字符的編碼對比),效率很低。

於是,我思考另外的方法,可以將哈夫曼樹保存到文件中,解碼時,從樹根到葉子對比編碼,只要一次遍歷就可以找到編碼對應的存於葉子結點中的字符,極大提高了效率。

然而,我們發現樹結點中有字符、編碼、左右孩子、雙親,而且孩子和雙親還必須是整型的(樹節點最多為256*2-1=511個),占用空間很大,會導致壓縮文件變大,這是不可取的,因為我們的目的就是壓縮文件。

我們進一步考慮,可以僅存儲字符及對應頻度(頻度為unsigned long,一般情況下與int占用空間一樣,同為4個字節),解碼時讀取數據重建哈夫曼樹,這樣就解決了空間問題。

雖然重建哈夫曼樹(雙重循環,每個循環的次數最大為511)也要花費一定的時間,但是相對上面的與編碼表匹配(每位編碼都要循環匹配所有字符(最多為256種)一次,而總的編碼位數一般很大,且隨着文件變大而增長)所花費的時間更少。

9.1由於解壓的方式變了,在這里要對上面的協商作一些修改

1、修改后的總UML協同圖:

clip_image003

2、在文件壓縮時,就不需要保存編碼表,改為保存字符及對應權重。

3、在文件壓縮時,處理最后不足8位的編碼后,不再需要保存編碼的長度,因為解壓時從樹根向下匹配,到達葉子就停止(所有葉子結點都在連續分配的樹結點空間的低端,故可以用結點下標判斷是否到達葉子結點),不會超過而讀取最后的無效編碼。

十、定義所需類:

10.1、文件壓縮所需類

clip_image004

行為說明:char_kinds保存出現的字符種類;char_temp用來保暫存字符;code_buf暫存匹配出來的編碼;compres()是主壓縮函數,接收兩個文件名,一個輸入,可以是任何格式的待壓縮文件,一個輸出,為壓縮后的編碼文件;

10.2、文件解壓所需類

clip_image005

行為說明:char_kinds保存出現的字符種類;char_temp用來保暫存字符;root保存解碼時的當前結點索引,用來判斷是否達到葉子結點;extract()是主壓縮函數,接收兩個文件名,一個輸入,為壓縮后的編碼文件,一個輸出,為解碼后的原文件;

10.3、其他重要類

clip_image006

行為說明:

1、tmp_nodes用來保存字符頻度,動態一次性分配256個空間,統計后刪除;CalChar()用於生成8位的256個字符及對應頻度(出現次數);

2、node_num保存樹結點總數,CreateTree()建立哈夫曼樹,select()函數用來找最小的兩個結點;

3、huf_node樹結點用來保存編碼信息,HufCode()生成哈夫曼編碼;

10.4、類的關聯圖

clip_image007

行為說明:CreateTree()和HufCode()供compress調用,前者建立哈夫曼樹,后者生成哈夫曼編碼;CreateTree()供extrac()調用,重建哈夫曼樹,用於解碼;

十一、編碼行為狀態圖

clip_image008clip_image009

后來我在初步編碼時,發現一些問題:解碼后無法得到完全正確的源文件,經過排查,發現以EOF判斷壓縮文件的結束不可取,因為壓縮文件是二進制文件,而EOF一般用來判斷非二進制文件的結束,所以我們加入了文件長度來控制。

11.1、於是上面的協商需要一些改動

1、修改后的字符統計類:

clip_image010

2、修改后的文件壓縮類:

clip_image011

3修改后的編碼行為狀態圖:

clip_image008[1]clip_image012

十二、函數實現:

12.1、實現語言及編碼環境:

實現語言:C語言,兼容嵌入式,運行效率高

編碼環境:XP+VS2010(debug模式)

12.2、結構體及函數定義

兩個重要的結點結構體:

clip_image014

三個函數用於建立哈夫曼樹和生成哈夫曼編碼:

clip_image016

clip_image018

clip_image020

兩個主要函數——壓縮解壓函數:

clip_image022

clip_image024

12.3、函數說明

12.3.1、其他函數:

Select函數供CreateTree函數調用,找兩個最小的結點,找到第一個后需要將其parent設為‘1’(初始化后為‘0’)表明此結點已被選中:

clip_image026

建立哈夫曼樹,每次用select()函數找兩個最小結點:

clip_image028

生成哈夫曼編碼,由葉子到根反向生成編碼,左‘0’,右‘1’,每個code域的內存動態分配:

clip_image030

12.3.2、壓縮函數中的幾個部分的說明

動態分配256個暫存結點,用下標索引統計字符頻度:

clip_image032

這里以feof來判斷文件結束,是由於eof判斷的文件類型比較局限,而feof在讀完最后一個字節之后,再次讀文件時才會設置結束標志,所以需要在while循環之前讀一次,然后每次在循環的最后讀取文件,這樣可以正確判斷文件結束;以位操作來匹配編碼,每次存入最低位,然后左移一位,依次循環處理,滿8位保存一次:

clip_image034

最后緩沖中不足8位,補0湊足8位(左移):

clip_image036

12.3.3、解壓函數中的說明

壓縮文件為二進制文件,feof在這里無法正確判斷結束,故用一個死循環處理編碼,以壓縮時存儲的文件長度來控制循環的結束。每當root小於char_kinds,就匹配到了一個字符,是因為字符的下標范圍是0~char_kinds-1。

clip_image038

十三、程序健壯性考慮

13.1、字符種類為‘1

當字符種類為‘1’時,只有一個哈夫曼結點,無法構造哈夫曼編碼,但是可以直接處理,依次保存字符種類數、字符、字符頻度(此時就是文件長度)即可,解壓時仍然先讀取字符種類數,為‘1’則特殊處理,讀取字符和頻度(此時就是文件長度),利用頻度控制循環,輸出字符到文件即可,此時壓縮文件的存儲結構為:

clip_image039

在壓縮函數前部加入特殊情況的判斷和處理:

clip_image041

在解壓函數前部加入特殊情況判斷和處理:

clip_image043

13.2、輸入文件不存在:

由於壓縮或解壓時,輸入文件必須是存在的,而用戶可能會輸錯,因此有必要加入輸入文件的存在性進行判斷,防止文件不存在而導致程序異常退出:

1、將壓縮解壓的返回值改為int:

clip_image045

clip_image047

2、在壓縮和解壓函數中加入:

clip_image049

3、在main函數中加入壓縮解壓函數是否異常退出的判斷:

clip_image051

十四、系統測試:

14.1、測試流程圖:

clip_image052

14.2、代碼運行測試

14.2.1、使用說明:

(編譯鏈接生成的可執行文件為:hufzip.exe)

雙擊hufzip.exe運行,輸入所選擇操作類型的數字代號:

1:compress(壓縮)

2:extract(解壓)

3:quit(退出)

然后提示輸入源文件和目標文件,可以輸入完整的路徑名加文件名,也可以僅輸入一個文件名(默認在當前運行目錄下尋找),如果不小心輸錯源文件名或源文件不存在,將提示出錯,然后可以再次輸入,如下圖所示:

clip_image054

14.2.2、測試的幾個文件

“1.txt”中為全為字符‘a’(共1024*1024個),由於只有一種字符,壓縮文件只保存了字符種類(4byte)、字符(1byte)和字符頻度(4byte),故為9字節,控制台及文件壓縮情況如下(1.txt.hufzip為壓縮文件,1.hufzip.txt為解壓后的文件):

clip_image056

clip_image058 clip_image062clip_image060

“2.txt”為0~255的整數,其中0出現1次,1出現2次,……,255出現256次,其控制台及壓縮情況如下(2.txt.hufzip為壓縮文件,2.hufzip.txt為解壓后的文件):

clip_image064

clip_image066clip_image070 clip_image068

“3.doc”為一個隨意的word文檔,其控制台及壓縮情況如下(3.doc.hufzip為壓縮文件,3.hufzip.doc為解壓后的文件):

clip_image072

clip_image074 clip_image078clip_image076

“4.jpg”是一個圖像文件,輸入絕對路徑對4.jpg進行壓縮,其控制台及壓縮情況如下(4.jpg.hufzip為壓縮文件,4.hufzip.jpg為解壓后的文件):

clip_image080

clip_image082clip_image086 clip_image084

14.2.3、由上面的幾種文件的壓縮前后的對比可以得出

哈夫曼編碼對文本文件,一般可以達到大約2:1的壓縮比,特別是有規律的文本文件,可以達到高於2:1的壓縮比,而對於圖像等特殊文件壓縮比幾乎為1:1,效果不理想。

十五、總結:

通過這一個壓縮和解壓程序的設計,我學習了UML的使用,提升了編碼能力,提升了調試能力,總之,受益匪淺。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM