哈夫曼壓縮算法


 

編程獨白

給你40分鍾的時間,你可以思考十分鍾,然后用三十分鍾的時間來寫代碼,最后浪費在無謂的調試上;你也可以思考半個小時,徹底弄清問題的本質與程序的脈絡,然后用十分鍾的時間來編寫代碼,體會代碼如行雲流水而出的感覺。在編程過程當中,相信大家都深有體會,在調試上浪費時間,問題出現在下筆之前沒有一個系統結構。

 

 

關於哈夫曼

哈夫曼在通信領域有很多的用途,將需要傳輸的數據轉換01串,相比直接傳輸,極大提高了傳輸的速率,同時還在數據壓縮的重要方法。而本篇主要介紹的是哈夫曼壓縮算法。

 

 

哈夫曼樹創建

哈夫曼樹創建用了兩種的方法,一種是基於順序表,另一種是基於最小堆。關於這兩種方法可以參考:

建立Huffman樹的基本思路:

給定有權重的一系列數據(帶權重),從中挑選最小權重的兩個數據,組成一棵樹,得到的父節點再插入到數據系列當中。

 

 

哈夫曼壓縮思路

假設一字符串是,“abcabcabcabcabcabcddddddddd”,統計各字符出現的次數如下表。

字符 出現次數
a 6
b 6
c 6
d 9

 

 

按照一搬的存儲方法,一個字符占用一個字節,那么共花費(6+6+6+9)*sizeof(char) = 27字節,27字節確實不算什么,但是如果是海量數據的時候,就可能要考慮存儲空間的問題了。

來看看哈夫曼壓縮算法是怎么做的,同樣是上面的例子,我們試着建立哈夫曼樹,出現的次數就當做是權重,出現的次數越多的話,越靠近根節點,那么編碼越短,如下圖:

image

 

於是上面的“abcabcabcabcabcabcddddddddd”,就可以轉化為“0001,1000,0110,0001,1000,0110,0001,1000,0110,1111,1111,1111,1111,11”,注意這里采用的按位存儲的,也就是說0和1都是位,而非char型。那么之前的27字節就被我們轉換成了7個字節(7字節不足,不足的話就補零),而這就達到了壓縮的效果。

所以總結一下:利用哈夫曼樹編碼的特點,權重越大越靠近根節點,得到的編碼就越短的原理,而如果把字符出現次數作為權重的話,文本當中出現次數最多的字符就被壓縮成了很短的編碼。

 

哈夫曼壓縮詳解

 

 

·壓縮過程主要步驟如下:

  1. 統計:讀入源文件,統計字符出現的次數(即統計權重),順便根據權重進行從大到小的排序(主要的話之后的操作會簡單一些);
  2. 建樹:以字符的權重(權重為0的字符除外)為依據建立哈夫曼樹;
  3. 編碼:依據2中的哈夫曼樹,得到每一個字符的編碼;
  4. 寫入:新建壓縮文件,寫入壓縮數據。

 

其中最為復雜的是步驟4,因為它涉及到了位的操作,待我細細道來。

 

假設一字符串是,“acbcbacddaddaddccd”,統計各字符出現的次數如下表。

字符 出現次數
a 4
b 2
c 5
d 7

 

image 哈夫曼樹 

 

步驟123統計,建樹,編碼都已經完成了,剩下寫入壓縮文件。將字符串一步一步翻譯成01串,

 

  • a:111
  • ac:1111 0
  • acb:1111 0110
  • acbc:1111 0110 10
  • acbcb:1111 0110 1011 0…

 

 

 

似乎都很順利,但是位操作有點麻煩。首先申請足夠大的內存,比如已知文本字符個數是1000個字符(字節),可以申請1000*4,即一個字節平均4字節(32位)的壓縮編碼空間(已經足夠大了),別把這些看成是char型了,當作位來看。

聲明nCodeIndex作為已編碼的位數,相當於counter。剛申請的足夠大的內存以8位划分,那么可以發現:

  • nCodeIndex/8可以標明第一個未存滿的字節,相當於nCodeIndex>>3;
  • nCodeIndex%8可以標明第一個未存滿的字節當中有幾位已經完成了存儲,相當於nCodeIndex&7。

 

 

可能說的不夠清楚,所以畫了圖:

image

image

 

*(long *)(pDest+(nCodeIndex>>3)) |= (p->code) << (nCodeIndex&7);(其中p為哈夫曼節點)

image

 

 

如此一來,我們就可以很理直氣壯的將*(long *)(pDest+(nCodeIndex>>3))賦值為*(long *)(pDest+(nCodeIndex>>3)) | (p->code) << (nCodeIndex&7)(0|X=X),而不用擔心到底*(long *)(pDest+(nCodeIndex>>3))里面到底有多少位已經是存儲了的。

給出壓縮主要代碼:

void CompressHuffman(char * filename)
{
	//·統計權重
	int count = Statistics(filename);

	Huffman hf;
	hf.CreateHuffmanTree(acsii,count);

	//·測試
	//hf.Print();

	//根據生成的哈夫曼樹,為每個字符生成編碼
	EnCode(&hf,count);
	HuffmanNode * root = hf.GetRoot();

	fstream fshuff;
	fshuff.open("data.huff",ios::binary|ios::out);

	//寫入壓縮文件的工作流程如下:

	//·數據個數(字符出現的個數)
	//·每個數據(data)以及其權重(rate)
	//·源文件中的字符個數,如源文件為abcd123,那么字符個數為7
	//·編碼的長度
	//·編碼

	//·寫入字符個數,比如abdarr,字符個數是4
	fshuff.write((char *)&count,sizeof(int));

	//·寫入acsii值和權重,以便在解壓縮的時候重建哈夫曼樹
	unsigned int  i;
	for(i=0; i<count; i++)
	{
		//·這里的讀寫可能會有問題的
		fshuff.write((char *)&(acsii[i].data),sizeof(char));
		fshuff.write((char *)&(acsii[i].rate),sizeof(int));	
	}

	//·注意區別fs和fshuff,前者是源文件,后者是壓縮后的文件
	fstream fs(filename,ios::in|ios::binary);

	//·獲取源文件文件字節數大小,seekg用在讀中,seekp用在寫中
	fs.seekg(0, ios_base::end);
	long nFileLen = fs.tellg();
	fs.seekg(0,ios::beg);

	//·將源文件逐個寫入到壓縮文件當中
	char * pDest = new char [nFileLen];
	for(i=0; i<nFileLen; i++)		//·清零,不清零會出現致命性的錯誤
		*(pDest+i) = 0;

	unsigned int nCodeIndex = 0;

	char * temp = new char[nFileLen+1];
	fs.read(temp,nFileLen);

	HuffmanNode *  p;
	for(i=0; i<nFileLen; i++)
	{
		//·在acsii表當中
		p = GetCodeFromACSIITable(temp[i],count);
		assert(p != NULL);

		*(long *)(pDest+(nCodeIndex>>3)) |= 
			(p->lCode) << (nCodeIndex&7);

		nCodeIndex += p->nCodeLen;
	}
	//·寫入源文件的字符個數
	fshuff.write((char *)&nFileLen,sizeof(long));


	//·寫入編碼長度
	fshuff.write((char *)&nCodeIndex,sizeof(int));

	unsigned int  nDestLen = nCodeIndex/8;
	if(nDestLen*8 < nCodeIndex)
		nDestLen ++;

	//·寫入編碼
	fshuff.write(pDest,nDestLen);
	fshuff.close();
	fs.close();
}

 

 

簡介哈夫曼壓縮文件DL結構

前一段時間在接觸位圖的時候被位圖結構觸動了,感覺它存儲得有條理,於是萌生了為哈夫曼壓縮文件定義一個存儲結構,稱之為哈夫曼壓縮文件DL結構關鍵是要統一,這篇博文用的是一種結構,另一篇用的又是另一種,紛雜的樣式會讓初學者發暈,所以統一結構對於學習哈夫曼壓縮文件會有很大的幫助。

DL結構組成部分:

  • 節點個數-用以規定創建哈夫曼樹節點個數,以便讀入節點域;
  • 節點域-用以創建哈夫曼樹和產生字符編碼;
  • 源文件字符數-在解壓的時候有用;
  • 編碼長度(以位為單位)-源文件編碼的總長度,以位為單位;
  • 編碼域-存儲所有字符編碼的存儲域。

為了便於讓你不看文字就能明白,看下圖,按着這種結構就相當於有了大概的思路。

image

 

 

 

哈夫曼解壓詳解

解壓的過程就簡單很多了,因為一些代碼已經在解壓過程當中完成,比如哈夫曼樹的建立,我們只要設計壓縮和解壓通用的接口,就可以很簡單的按照編碼域的內容,將編碼翻譯成原文。

  1. 讀入節點個數;
  2. 根據1,讀入節點域;
  3. 創建哈夫曼樹;
  4. 讀入編碼長度;
  5. 根據4,讀入編碼域;
  6. 翻譯;
  7. 寫入解壓后的文件。

 

根據編碼長度(以bit為單位),可以計算出編碼域的大小(以byte為單位),讀入編碼域就很方便了。其中翻譯部分我給出一部分代碼,根據哈夫曼樹,將編碼域的01串按位處理,轉換為字符。

  • Code&1判斷最低位是0還是1,從而決定指向left還是right;
  • Code>>=1,將Code右移,方便處理下一位;
  • 每當翻譯出一個字符,就要將當前的哈夫曼指針重新指向root,p = hf.GetRoot()。
	for(i=0; i<nFileLen-1;)		//·特地少處理一個字節
	{
		Code = *(temp+(nSrcIndex));
		for(int j=7; j>=0; j--)
		{
			p = (Code&1) ? p->right : p->left;
			if(!p->left && !p->right)
			{
				*(pDest+i) = p->data;
				p = hf.GetRoot();
				i++;
			}
			Code>>=1;			//·為了處理下一位,右移一位
		}
		nSrcIndex ++;
	}

	Code = *(temp+(nSrcIndex));
	for(int j=0; j<nOffset; j++)
	{
		p = (Code&1) ? p->right : p->left;
		if(!p->left && !p->right)
		{
			*(pDest+i) = p->data;
			cout << p->data << endl;
			p = hf.GetRoot();
		}
		Code>>=1;			//·為了處理下一位,右移一位
	}

附哈夫曼壓縮算法工程:哈夫曼壓縮算法.rar

本文完。Monday, December 26, 2011


免責聲明!

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



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