1. 程式人生 > >生命不息 奮鬥不止 | LINUX愛好者 世界因你的敲打而改變~~

生命不息 奮鬥不止 | LINUX愛好者 世界因你的敲打而改變~~

這個程式是研一上學期的課程大作業。當時,跨專業的我只有一點 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的使用,提升了編碼能力,提升了除錯能力,總之,受益匪淺。

轉載自:http://www.cnblogs.com/keke2014/p/3857335.html