C++小項目 — 基於huffman壓縮算法的文件壓縮項目

基於huffman壓縮算法的文件壓縮項目






今天我們來看看一個小項目基於huffman壓縮算法的文件壓縮項目.這個項目很簡單就是使用我們自己實現的堆和huffman樹,然後開始着手來對文件裏

的內容進行一系列操作! 所以我總結了一下這個項目最關鍵的三個部分就是: 

1.huffman樹的構建(1.必須使用模板來構建(可能會存放自定義類型來構建) 2.要擁有在huffman樹當中構建huffman編碼的功能)

2.文件的壓縮(將我們的文本文件的內容轉換成由huffman編碼構成的文件),等待解壓縮)

3.文件的解壓(將huffman編碼文件轉換成正常的文本文件) 

完成這三大步,我們的文件壓縮也算是就完成的一個大概了,現在我們開始對三大塊進行個個擊破! 


huffman樹的構建



關於huffman樹的概念我以前是有一篇博客的:huffman樹的構建原理,大家不明白可以點進去看一看,裏面有huffman樹的基本概念. 爲了節約篇幅

我這裏只講講爲什麼huffman樹算法爲什麼會壓縮文件?? 它是如何節約空間的?  請看下圖:



首先我們以每個字符的出現的次數作爲構建huffman樹的鍵值,然後對棵huffman樹的每一個葉子結點生產huffman編碼,這是最基本的工作.

我們已經得到了字符串中abcd的huffman編碼了,它們分別是: '011' '010' '00' '1',注意由於這裏的huffman編碼都是01組成的,所以我們很容易

想到按位存儲,這個時候這個字符串的huffman編碼形式就是 0110100100000001111,我們將它按位寫進文件中,這個文件佔用19個比特位,所以佔用

三個字節. 而原文件中有10個字符他們都是char類型,所以佔用10個字節. 這就是huffman壓縮算法的基本思想.


所以出現的越頻繁次數越多的字符它的huffman編碼越短,佔用的空間越少. 出現較爲少的字符他們的huffman的編碼雖然長,但是出現的次數少.並且

我們的huffman編碼是按位存儲的,所以這裏的壓縮效率就顯現出來了. huffman壓縮算法壓縮率高的情況就是 出現特別多的重複字符,只有極少數的

是別的字符(比如 abbbbbbbbbbbbbbbb). 效率低的情況下就是每個字符出現的次數都差不多很平均.(比如abcdabcd)

現在項目的思路漸漸地清晰了起來了:

1.統計:首先讀取一個文件,統計出256個字符中各個字符出現的次數以及字符出現的總數.

2.建樹:按照字符出現的次數,並以次數作爲權值簡歷哈夫曼編碼樹,然後生產各個字符的huffman編碼.

3.壓縮:再次讀取文件,按照該字符對應的編碼寫到新的編碼文件當中.

4.加工:將文件的長度,文件中各個字符以及他們的出現次數,寫進編碼文件中.

5.解壓:利用壓縮文件和配置文件恢復出原文件.

6.測試:觀察解壓出的文件和原文件是否相同,再通過Beyond Compare 4軟件進行對比,驗證程序的正確性.



文件的壓縮過程




壓縮過程就是利用我們剛剛所統計出的個個字符對應的huffman編碼,然後重新讀取文件中的數據,根據讀到的每一個字符將該字符對應的huffman編

碼寫入到配置文件中,還有將每個字符和出現次數寫入這個文件(寫到文件頭和文件尾 看你的喜好了). 有人會問爲什麼? 這個下面會提到. 經歷這兩

個階段,文件的壓縮過程也基本就結束了.現在我來解釋一下爲什麼要往配置件中寫入每個字符和它的出現次數!

我們生成了這個配置文件之後,我們肯定是要刪除掉原文件的(如果不刪原文件,壓縮有什麼意思?微笑) ,但是之後的解壓縮還要把huffman編碼轉爲

字符. 但是... 你源文件都沒有了,你的huffman樹也隨着沒有啦,所以我們再之後的解壓縮的時候還需要這顆huffman樹,怎麼辦? 重建huffman

樹! 所以這就是將每個字符和出現的次數寫到文件中的原因. 這就是原因! 接下來我們來看看這段的代碼! 我註釋儘量詳細:

typedef long long LongType;



//這是這個項目當中要用到的自定義類型. 裏面存放有字符,字符出現個數,還有它的huffman編碼.
struct CharInfo
{
	char     _ch;	 // 字符
	LongType _count; // 出現次數
	string	 _code;	 // huffman code

	bool operator != (const CharInfo& info) const
	{
		return _count != info._count;
	}

	CharInfo operator+(const CharInfo& info) const
	{
		CharInfo ret;
		ret._count = _count + info._count;
		return ret;
	}

	bool operator<(const CharInfo& info) const
	{
		return _count < info._count;
	}
};

class FileCompress
{
	typedef HuffmanTreeNode<CharInfo> Node;
	//壓縮文件
	void Compress(const char* filename)
	{
		assert(filename);

		//這是專門用來以後往配置文件中寫入的字符和字符出現次數準備的.
		//爲了在解壓縮的時候進行構建huffman樹.
		struct _huffmanInfo
		{
			char _ch;
			LongType _count;
		};

		//打開文件
		FILE* fout = fopen(filename, "rb");
		cout << errno << endl;
		assert(fout);

		// 1.統計字符出現的次數
		char ch = fgetc(fout);
		while (ch != EOF)
		{
			//_infos[]是一個CharInfo類型的數組,一共有256個元素. 每一個元素維護一個字符.
			_infos[(unsigned char)ch]._count++;
			ch = fgetc(fout);
		}

		// 2.構建huffman tree

		CharInfo invalid;
		invalid._count = 0;
		HuffmanTree<CharInfo> tree(_infos, 256, invalid);

		// 3.構建huffman編碼
		GenerateHuffmanCode(tree.GetRoot());

		//4.開始壓縮文件
		string  compressFile = filename;
		compressFile += ".huffman";
		//c_str()的用法就是返回一個正規的C字符串的指針,內容和本string相等

		FILE* fIn = fopen(compressFile.c_str(), "wb");
		assert(fIn);
		_huffmanInfo info;
		size_t size;
		//這裏先往配置文件中寫入出現過的字符中,每個字符信息和每個字符的出現次數.
		//用來解壓縮的時候重建huffman樹
		for (size_t i = 0; i < 256; i++)
		{
			if (_infos[i]._count)
			{
				info._ch = _infos[i]._ch;
				info._count = _infos[i]._count;
				//利用二進制寫入,這個結構體.
				size = fwrite(&info, sizeof(_huffmanInfo), 1, fIn);
				assert(size == 1);
			}
		}
		info._count = 0;
		size = fwrite(&info, sizeof(_huffmanInfo), 1, fIn);
		assert(size == 1);

		char value = 0;
		int count = 0;
		//讓fout放置到文件開始
		fseek(fout, 0, SEEK_SET);//SEEK_SET 開頭 SEEK_CUR 當前位置 SEEK_END 文件結尾 
		ch = fgetc(fout);

		//將huffman編碼寫入到配置文件中.
		//好好體會這裏的位運算的過程,value中寫滿了8個位,就往配置文件裏面寫一次
		while (!feof(fout))
		{
			string& code = _infos[(unsigned char)ch]._code;
			for (size_t i = 0; i < code.size(); ++i)
			{
				value <<= 1;
				if (code[i] == '1')
					value |= 1;
				else
					value |= 0;
				++count;
				if (count == 8)
				{
					fputc(value, fIn);
					value = 0;
					count = 0;
				}
			}
			ch = fgetc(fout);
		}
		if (count != 0)
		{
			value <<= (8 - count);
			fputc(value, fIn);
		}
		fclose(fIn);
		fclose(fout);
	}
protected:
	CharInfo _infos[256];
};


我們要注意代碼中出現了一個feof這樣的函數,爲什麼我們沒有直接使用eof判斷呢?我們以後要儘量運用這些函數,不去使用EOF判斷. 這個函數的實

現,有興趣的人類可以下去了解一下.


文件的解壓縮過程



先去讀配置文件,構建huffman樹和huffman編碼,用壓縮文件裏的編碼去huffman樹中查找,找到對應的葉子結點. 就把葉子結點的字符寫入到解壓縮

文件中. 所以總結起來也就是那麼幾步:

1.讀取配置文件,統計所有字符的個數.

2.構建huffman樹,讀解壓縮文件,將所讀到的編碼字符的這個節點所含的字符,寫到解壓縮的文件中,直到將壓縮文件讀完

3.解壓縮完成之後,利用軟件測試,再然後測試一下壓縮率.

好啦話不多說 上代碼 分析過程: 

//構建huffman編碼的函數.
void GenerateHuffmanCode(Node* root)
{

	if (root == NULL)
	{
		return;
	}
	if (root->_left == NULL &&root->_right == NULL)
	{
		string& code = _infos[(unsigned char)root->_w._ch]._code;
		Node* cur = root;
		Node* parent = cur->_parent;
		while (parent)
		{
			if (parent->_left == cur)
			{
				code.push_back('0');
			}
			if (parent->_right == cur)
			{
				code.push_back('1');
			}
			cur = parent;
			parent = cur->_parent;
		}
		reverse(code.begin(), code.end());
	}
	GenerateHuffmanCode(root->_left);
	GenerateHuffmanCode(root->_right);

}

//解壓縮文件
void Uncompress(const char* filename)
{

	//爲了區分解壓出來的文件,不覆蓋原文件名.
	assert(filename);

	struct _huffmanInfo
	{
		char _ch;
		LongType _count;
	};
	string uncompressFile = filename;
	//讓pos等於找到第一個'.'出現的的下標位置
	size_t pos = uncompressFile.rfind('.');
	assert(pos != string::npos);
	//從0開始取pos個字符
	uncompressFile = uncompressFile.substr(0, pos);
	uncompressFile += ".unhuffman";
	FILE* fIn = fopen(uncompressFile.c_str(), "wb");
	size_t size;
	assert(fIn);
	FILE* fout = fopen(filename, "rb");

	//往解壓縮文件中維護的_infos表當中寫數據,然後利用這個_infos表生產huffman樹.
	while (1)
	{
		_huffmanInfo info;
		size = fread(&info, sizeof(_huffmanInfo), 1, fout);
		assert(1 == size);
		if (info._count)
		{
			_infos[(unsigned char)info._ch]._ch = info._ch;
			_infos[(unsigned char)info._ch]._count = info._count;
		}
		else

			break;
	}

	//重新構建huffman樹.
	CharInfo invalid;
	invalid._count = 0;
	HuffmanTree<CharInfo> tree(_infos, 256, invalid);

	//解壓縮
	Node* root = tree.GetRoot();
	GenerateHuffmanCode(tree.GetRoot());
	//統計一共有多少個數據.
	LongType charcount = root->_w._count;

	char value;
	value = fgetc(fout);
	Node* cur = root;
	int count = 0;

	//往解壓縮文件當中開始寫入數據. 在這裏直接針對性的先統計出所有元素的個數.
	//然後按照個數進行循環,寫完循環結束.
	while (charcount)
	{
		for (int pos = 7; pos >= 0; --pos)
		{
			if (value & (1 << pos)) //1
				cur = cur->_right;
			else                    //0
				cur = cur->_left;
			if (cur->_left == NULL && cur->_right == NULL)
			{
				fputc(cur->_w._ch, fIn);
				cur = root;
				--charcount;
				if (charcount == 0)
				{
					break;
				}
			}
		}
		value = fgetc(fout);
	}
	fclose(fIn);
	fclose(fout);
}

最後我們終於成功的,大概列出來代碼的框架和主題思想,接下來就是我在寫代碼當中碰到的一些BUG和錯誤,我將這些總結起來.

(1).剛開始寫的時候測試發現如果待壓縮文件中出現了中文,程序就會崩潰,將錯誤定位到構建哈夫曼編碼的函數處,最後發現是數組越界的錯誤

,因爲如果只是字符,它的範圍是-128~127,程序中使用char類型爲數組下標(0~127),所以字符沒有問題. 但是漢字的編碼是兩個字節,所以可能會

出現越界,解決方法就是將char類型強轉爲unsigned char,可表示範圍爲0~255.

(2)文件恢復的時候需要注意那些問題? 

有些特殊字符在處理需要注意一下,比如'\n',我的程序中有一個函數就是讀取一行字符,但是若是該字符本身就是一個'\n'呢? 這就非常的棘手

了. 對於這個問題一定好好好處理,讀取配置文件時若讀到了'\n',則說明該字符就是'\n',應該繼續讀取它的次數.

(3)解壓縮文件生成後丟失了大部分的內容

這個BUG困擾我很久,最後爲了測試我讓程序打印出在壓縮的時候它寫入了多少次,然後再讓程序在解壓縮的時候打印自己讀出了多少次. 最後我發現

在解壓縮讀出的時候出現了問題,究其原因我又發現了很久,我發現它過早的退出了循環,那麼真相只有一個就是程序讀到了EOF,但是我在文本末尾

纔會是EOF啊,因爲解壓縮讀取是二進制方式讀寫的,文件這麼長! 他可能碰巧讀到了EOF對於的二進制代碼. 然後就退出啦. 

解決方案: 使用feof()函數或者記錄字符數據個數然後根據個數循環,這兩種方法都可以解決上述問題.


這個項目其實就是利用我們學習過的堆和huffman樹的實際應用來解決我們實際生活的一些小問題,這種小項目還需要我們多多練習呢~ 

利用代碼解決生活的問題纔是編程的意義,所以呢我們需要將理論和實際相結合~ 這樣我們的學習就會越來越好~

這個項目的所有代碼在我的github傳送門在這裏: 戳這裏進入奇幻世界
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章