1. 程式人生 > >C++實現LZ77壓縮演算法

C++實現LZ77壓縮演算法

     LZ77演算法是一種基於詞典壓縮的方法,並且該詞典不是靜態的,而是自適應的動態詞典。LZ77把已經壓縮過的資料當成詞典,未壓縮的資料在已經壓縮的資料中查詢(即動態詞典),然後把偏移量和匹配長度表示出來的一種方法。

     介紹LZ77原理的文章很多,這裡就不囉嗦了。其實演算法思路也是比較簡單。本著"紙上得來終覺淺"的觀念,我這裡實現最基本的LZ77的壓縮和解壓演算法,實際上出於壓縮效率的考慮,LZ77的壓縮演算法存在很多的變種。但是理解變種之前,先理解最原始的思路更有幫助,而且知道來龍去脈。如下是一個例子的簡單圖解。

     

#include <Windows.h>
#include <stdio.h>
#include "MyLZ77.h"


void TestMyLZ77()
{
	//為了方便檢視,使用字串測試,MyLZ77Encode函式是支援二進位制壓縮的
	char *pTestStr = "aacaacabcabaaac";
	int testStrLen = strlen(pTestStr);
	PLZ77_ENCODE_LIST header = MyLZ77Encode((unsigned char*)pTestStr,testStrLen);//最後一個位元組看能會多出來,暫時未處理
	PLZ77_ENCODE_LIST p = header;
	if(header)
	{
		 while(p)
		 {
			 printf("(%2d,%2d,%2c)\r\n",p->item->searchResult->pos,p->item->searchResult->matchLen,p->item->nextChar);	
			 p = p->next;
		 }
	}
	else
	{
		printf("Error: MyLZ77Encode return NULL\r\n");
	}

    char decodeBuf[1024] = {'\0'};
	int decodeLen = MyLZ77Decode(header,(unsigned char*)decodeBuf); //最後一個位元組看能會多出來,暫時未處理

	if(decodeLen != testStrLen)
	{
		printf("Error:Decode len is not match data len before encode.May be add a byte in the end!!!\r\n");
	}

	if(strncmp(pTestStr,decodeBuf,testStrLen))
	{
		printf("Error:Decode data is not match data len before encode\r\n");
	}
	else
	{
		printf("Decode Success!!!\r\n");
	}
}


int main(int agrc,char* argv[])
{
	TestMyLZ77();

	getchar();
	return 0;
}

       和圖解的例子一樣,測試結果如下:

       
#ifndef _MYLZ77_H_
#define _MYLZ77_H_

typedef struct _t_LZ77_ENCODE_SEARCH_RESULT
{
	_t_LZ77_ENCODE_SEARCH_RESULT()
	{
		pos = -1;
		matchLen = 0;
	}
	short pos;//字典匹配的起始位置,相對待編碼資料的偏移
	short matchLen;//字典匹配的長度
}LZ77_ENCODE_SEARCH_RESULT,*PLZ77_ENCODE_SEARCH_RESULT;

typedef struct _t_LZ77_ENCODE_ITEM
{
	PLZ77_ENCODE_SEARCH_RESULT searchResult;
	unsigned char nextChar;//匹配後的下一個未匹配的字元
}LZ77_ENCODE_ITEM,*PLZ77_ENCODE_ITEM;

typedef struct _t_LZ77_ENCODE_LIST
{
	PLZ77_ENCODE_ITEM item;
	_t_LZ77_ENCODE_LIST* next;
}LZ77_ENCODE_LIST,*PLZ77_ENCODE_LIST;


//標準的LZ77編碼輸出,這裡使用連結串列儲存編碼的結果
PLZ77_ENCODE_LIST MyLZ77Encode(unsigned char* data,int dataLen);

//標準的LZ77解碼輸出,輸入需要編碼後的連結串列,輸出解碼後的位元組數
int MyLZ77Decode(PLZ77_ENCODE_LIST header,unsigned char *pDecodeOut);

#endif

#include <Windows.h>
#include "MyLZ77.h"


#define MY_LZ77_DICTIONARY_LEN 0x06 //動態字典的長度設定為6位元組
#define MY_LZ77_ENCODE_BUFFER_LEN 0x04 //動態編碼的buf長度裝置為4位元組

//更新連結串列
PLZ77_ENCODE_LIST UpdataLZ77Link(PLZ77_ENCODE_LIST *lz77Header,PLZ77_ENCODE_LIST *pCur,PLZ77_ENCODE_ITEM item)
{
	PLZ77_ENCODE_LIST p = new LZ77_ENCODE_LIST;
	if(!p)
	{
		return NULL;
	}

	p->item = item;
	p->next = NULL;
	if(!*pCur)
	{
		*pCur = p;
		*lz77Header = p;
	}
	else
	{
		(*pCur)->next = p;
		*pCur = p;
	}

	return p;
}

//通過m*n的時間複雜度搜尋最長匹配,可以優化該函式
PLZ77_ENCODE_SEARCH_RESULT MyLZ77Search(unsigned char *pDictionary,
										unsigned short dictionarySize,
										unsigned char* pEncodeBuffer,
										unsigned short encodeBufferSize)
{
	PLZ77_ENCODE_SEARCH_RESULT searchResult =(PLZ77_ENCODE_SEARCH_RESULT)new LZ77_ENCODE_SEARCH_RESULT;
	if(!searchResult)
	{
		return NULL;
	}

	//在字典中搜索
	for(int i=0;i<dictionarySize;i++)
	{
		int pos = i;
		int index = i;
		for(int j=0;j<encodeBufferSize;j++)
		{
			if(pEncodeBuffer[j] == pDictionary[index])
			{
				++index;
				continue;
			}
			else
			{
				break;
			}
		}
		int matchLen = index - i;
		if(matchLen && matchLen>searchResult->matchLen)
		{
			searchResult->pos = dictionarySize-i;
			searchResult->matchLen = matchLen;
		}
	}

	return searchResult;
}

//標準的LZ77編碼輸出,這裡使用連結串列儲存編碼的結果
PLZ77_ENCODE_LIST MyLZ77Encode(unsigned char* data,int dataLen)
{
	if(!data || dataLen<=0)
	{
		return NULL;
	}

	int hasbeenEncodeSize = 0;//已經編碼的資料大小

	PLZ77_ENCODE_LIST lz77Header = NULL;
	PLZ77_ENCODE_LIST pCur = NULL;

	unsigned short dictionarySize = 0;//開始時字典長度為0
	unsigned short encodeBufferSize = MY_LZ77_ENCODE_BUFFER_LEN;
	if(dataLen<MY_LZ77_ENCODE_BUFFER_LEN)
	{
		encodeBufferSize = dataLen;
	}

	unsigned char *pDictionary = data;
	unsigned char *pEncodeBuffer = data;

	while(encodeBufferSize>0)
	{
		PLZ77_ENCODE_SEARCH_RESULT pResult = MyLZ77Search(pDictionary,dictionarySize,pEncodeBuffer,encodeBufferSize);
		if(!pResult)
		{
			break;
		}

		PLZ77_ENCODE_ITEM item = new LZ77_ENCODE_ITEM;
		if(!item)
		{
			break;
		}

		item->searchResult = pResult;
		item->nextChar = pEncodeBuffer[pResult->matchLen];

		hasbeenEncodeSize += (pResult->matchLen+1);//已編碼的位元組數同步增加

		//更新連結串列
		if(!UpdataLZ77Link(&lz77Header,&pCur,item))
		{
			break;
		}

		//更新動態字典
		dictionarySize += pResult->matchLen+1;
		if(dictionarySize>MY_LZ77_DICTIONARY_LEN)
		{
			dictionarySize = MY_LZ77_DICTIONARY_LEN;
			pDictionary = (pEncodeBuffer+pResult->matchLen+1)-MY_LZ77_DICTIONARY_LEN;
		}

		//更新待編碼的buffer指標,往後移動
		pEncodeBuffer += (pResult->matchLen+1);

		//未編碼的資料已經不足MY_LZ77_ENCODE_BUFFER_LEN,更新未編碼的資料,否則未編碼的buffer大小仍然設定為MY_LZ77_ENCODE_BUFFER_LEN
		if(dataLen-hasbeenEncodeSize<MY_LZ77_ENCODE_BUFFER_LEN)
		{
			encodeBufferSize = ((dataLen-hasbeenEncodeSize)>=0)?(dataLen-hasbeenEncodeSize):0;
		}
	}

	return lz77Header;
}

//標準的LZ77解碼輸出,輸入需要編碼後的連結串列,輸出解碼後的位元組數
int MyLZ77Decode(PLZ77_ENCODE_LIST header,unsigned char *pDecodeOut)
{
	int decodeLen = 0;
	PLZ77_ENCODE_LIST p = header;
	unsigned char* pCursorData = pDecodeOut;
	while(p)
	{	
		unsigned char *pCopy = pCursorData-p->item->searchResult->pos;
		for(int i=0;i<p->item->searchResult->matchLen;i++)
		{
			*pCursorData = *pCopy;
			pCursorData++;
			pCopy++;
		}
		*pCursorData = p->item->nextChar;
		pCursorData++;

		decodeLen += (p->item->searchResult->matchLen+1);

		p = p->next;
	}

	return decodeLen;
}

     這個例子可以看到,其實標準的原始LZ77很多時候壓縮比率並不高。而在實際運用中,動態字典長度和待編碼的長度都會比較長,這樣壓縮效果會更好。

     我們修過如下:

      #define MY_LZ77_DICTIONARY_LEN 32 //動態字典的長度設定為32位元組
      #define MY_LZ77_ENCODE_BUFFER_LEN 16 //動態編碼的buf長度裝置為16位元組

      測試字串修過為:

      char *pTestStr = "abababababababababababababababababababababababababababababab";

      那麼,測試結果為: