基于LZ77算法的文件压缩铺垫
基于Huffman算法和LZ77算法的文件壓縮(四)
本文開始講解LZ77算法,會用到哈希,哈希原理詳解
我們在基于Huffman算法和LZ77算法的文件壓縮(一)當中總體介紹了Huffman算法和LZ77算法的原理,本文講解基于LZ77算法的文件壓縮和解壓縮
一、 什么是LZ77
1977年,兩位以色列人Jacob Ziv和Abraham Lempel,發表了一篇論文《A Universal Algorithm for Sequential Data Compression》,一種通用的數據壓縮算法,所謂通用壓縮算法指的是這種壓縮算法沒有對數據的類型有什么限定,該算法奠基了今天大多數無損數據壓縮的核心,為了紀念兩位科學家,該算法被稱為LZ77,過了一年他們又提了一個類似的算法,稱為LZ78。
二、 LZ77壓縮原理介紹
LZ77是基于字節的通用壓縮算法,它的原理就是將源文件中的重復字節(即在前文中出現的重復字節)使用 (o?set,length,nextchar)的三元組進行替換。
比如:
源文件內容:mnoabczxyuvwabc123456abczxydefgh
-
上文中"abc"字符串有多次重復,那如果用(o?set,length,nextchar)方式替換,肯定可以起到壓縮的目 的。
-
o?set:表示待匹配的當前字符距離匹配字符串首字母的距離,
-
length表示匹配字符串的長度,即有多少個字符與前文匹配
-
nextchar表示當前匹配串的下一個字符,
-
上述原文采用(o?set, length, nextchar)三元組替換完成后的結果為:mnoabczxyuvm(9,3,1)23456(18,6,d)efgh。
-
但是GZIP并沒有采用上述的三元組進行替換,而是進行了一個小小的改變,因為nextchar是否出現在三元組中,對壓縮率的提升并不能起到什么作用,因此GZIP采用(距離,長度)對的方式進行替換,具體如下:
mnoabczxyuvm(9,3)123456(18,6)defgh。
文件壓縮過程:
但是真正的壓縮,是在一個比較大的窗口中進行的,窗口越大,找到匹配的可能性就越大,但不是無限大, 因為無限大實,存在兩個問題:
因此,GZIP決定,窗口的大小取為64K,分為兩部分,一個WSIZE大小為32K,如下圖所示:
- 從上圖可以看出,隨著壓縮的進行,窗口被分割成了兩個部分,查找緩沖區和先行緩沖區。
- 先行緩沖區:即待壓縮的數據,每次都用先行緩沖區中的第一個字符與其后緊跟的兩個字符在查找緩沖區中 找匹配,
- 隨著壓縮的不斷進行,查找緩沖區不斷增大,先行緩沖區不斷縮小,當查找緩沖區增大到32K之 后,就不增大了,
- 隨著先行緩沖區向右移動。如果先行緩沖區中的數據少于一個MIN_LOOKAHEAD時,將右 窗口中的數據搬移到左窗口,給右窗口中重新補充32K的數據,繼續壓縮,直到壓縮結束。
- MIN_LOOKAHEAD = MAX_NATCH + 1;即:保證待壓縮區域至少有一個字節以及該字節的一個匹配長度。 通過以上過程介紹,發現幾個問題:
2. 高效查找最長匹配串
2.1 暴力求解
該算法的性能比較差,是一個O(N2)O(N^2)O(N2)的算法,如果待壓縮文件比較大,會嚴重影響壓縮的速度
2.2 采用哈希
使用哈希表來提高查詢的效率:使用哈希“桶”保存每三個相鄰字符構成的字符串中首字符的窗口索引。 壓縮 過程中每遇到新字符時,進行如下操作:
關于"哈希桶",引發出一堆問題:
三個字符總共可以組成2^24 種取值(即16M),桶的個數需要 2^24個,而索引大小占2個字節,總共桶占32M 字節,是一個非常大的開銷。隨著窗口的移動,表中的數據會不斷過時,維護這么大的表,會降低程序 運行的效率。因此本文哈希桶的個數設置為:2^15 (即32K)。
- 原本需要 2^24 個哈希桶,現在減少為2^15 個,必然會產生哈希沖突。如果采用開散列解決,鏈表中的節點
要不斷申請與釋放,而且浪費空間,影響呈現效率。因此本文哈希表由一整塊連續的內存構成,分為兩
個部分,每部分大小為一個WSIZE(32K),如下圖所示:
- prev指向該字典整個內存的起始位置,head = prev + WSIZE,內存是連續的,所以prev和head可以 看作兩個數組,即prev[]和head[]
- head數組用來保存三個字符串首字符的索引位置,head的索引為三個字符通過哈希函數計算的哈希 值。
- 而prev就是來解決沖突的,
3.哈希函數 哈希函數原則:簡單、離散。因此本文哈希函數設計如下:
-
A(4,5) + A(6,7,8) ^ B(1,2,3) + B(4,5) + B(6,7,8) ^ C(1,2,3) + C(4,5,6,7,8) 說明:A 指 3 個字節中的第 1 個字節,B 指第 2 個字節,C 指第 3 個字節, A(4,5)指第一個字節的第 4,5 位二進制碼,“^”是二進制位的異或操作, “+”是“連接”而不是“加”,“^”優先于“+”)
-
這樣使 3 個字節都盡量“參與”到最后的結果中來,而且每個結果值 h 都等于 ((前1個h << 5) ^ c)取右 15位
4.哈希表構建(插入字符串)
哈希表的構建即將字符串插入到哈希表中,該過程伴隨著壓縮過程一塊進行:
- 獲取當前字符ch(假設其在窗口中的位置為pos)
- 用ch之后緊鄰的兩個字符構成當前串curStr
- 插入curStr
- matchHead帶出匹配鏈的起始位置
- 通過matchHead判斷是否發生匹配。
問題:當pos超過WSIZE時,在插入函數中如果直接使用pos肯定會越界,因此需要與WMASK,即_prev[pos & WMASK] = _head[hashAddr], - 但是該語句可能會破壞匹配鏈,讓匹配鏈構成環而造成死 循環,該情況如何處理?
- 設置一個最長匹配次數,比如:255,匹配了255次也沒有匹配到,放棄本次匹配
5.查找最長匹配
字符串插入后,如果matchHead為空,表示為遇到匹配串,比如第一個"abc"的插入過程;否則,表示 在查找緩沖區出現過該字符串。此時,順著匹配鏈查找所有的匹配串,直到找到最長匹配。
// 功能:在當前匹配鏈中找最長匹配 // 參數: // hashHead: 匹配鏈的起始位置 // matchStart:最長匹配串在滑動窗口中的起始位置 // 返回值:最長匹配串的長度 USH BitZip::LongestMatch(USH hashHead, USH& matchStart) {// 哈希鏈的最大遍歷長度,防止造成死循環int chain_length = 256;// 始終保持滑動窗口為WSIZE,因為最小的超前查看窗口中有 MIN_LOOKAHEAD的數據// 因此只搜索_start左邊MAX_DIST范圍內的串USH limit = _start > MAX_DIST ? _start - MAX_DIST : 0;// 待匹配字符串的最大位置// [pScan, strend]UCH* pScan = _pwin + _start;UCH* strend = pScan + MAX_MATCH - 1;// 本次鏈中的最佳匹配int bestLen = 0;UCH* pCurMatchStart;USH curMatchLen = 0;// 開始匹配do{// 從搜索區hashHead的位置開始匹配pCurMatchStart = _pwin + hashHead;while (pScan < strend && *pScan == *pCurMatchStart){pScan++;pCurMatchStart++;} // 本次匹配的長度和匹配的起始位置curMatchLen = (MAX_MATCH - 1) - (int)(strend - pScan);pScan = strend - (MAX_MATCH - 1);/*更新最佳匹配的記錄*/if (curMatchLen > bestLen){matchStart = hashHead;bestLen = curMatchLen;}} while ((hashHead = _hash._prev[hashHead & WMASK]) > limit&& --chain_length != 0);return curMatchLen; }通過上述方式獲取到的最長匹配串,一定是最長的嗎?如何優化?
“1abc23bcdefghijklm456abcdefghijklmnopq”
2.3 找不到最長匹配怎么辦
- 找不到最長匹配時,將該源字符直接寫入壓縮文件。比如:比如:
mnoabczxyuvm(9,3)123456(18,6)defgh。 - 但是在真正壓縮結果中,<距離,長度>對實際是沒有括號的,因此上述的壓縮結果實際為:mnoabczxyuvm93123456186defgh,
- 如何區分<距離,長度>對與源文件中 的數字?
- 為了區分源字符與<距離,長度>對,在向壓縮文件中寫數據時可用0和1來進行區分,比如用0代表源字符,1 代表<距離,長度>對。
- 但真正的GZIP在保存壓縮數據時,是將源字符和長度放在一塊保存,將距離單獨保存,為什么按照該種方式 保存,
2.4 滑動窗口中數據不夠時怎么辦?
隨著滑動窗口的不斷移動,右側窗口中的數據不足MIN_LOOKAHEAD時怎么辦?在壓縮時,如果文件沒有讀
到結尾,為了保證最大匹配,必須保持look_ahead中至少有MIN_LOOKAHEAD的源數據。
此時,需要將右窗中的數據搬移到左窗中。
注意:窗口中的數據移動,此時必須更新哈希表
void LZ77::FillWindow(FILE* fIn) { // 滑動窗口中的數據不足時// 把右窗中數據(32K)移到左窗if (_start >= WSIZE + MAX_DIST) {memcpy(_pWin, _pWin + WSIZE, WSIZE);_start -= WSIZE;//更新哈希表,若是舊左窗的字串,則刪除該詞條,重置為nil,//注意,哈希表中越靠近頭部的串,在窗口位置越靠右(就是更加新鮮),_ht.Update();} size_t readSize = 0;if (!feof(fIn)){readSize = fread(_pWin + _start + _lookAhead, 1, WSIZE, fIn);if (0 == readSize)memset(_pWin + _start + _lookAhead, 0, MIN_MATCH - 1);else_lookAhead += readSize;} }void HashTable::UpdateDictionary() {// 更新_head數組for (int i = 0; i < HASH_SIZE; i++){ if (_head[i] >= WSIZE)_head[i] -= WSIZE;else_head[i] = 0;}// 更新prev數組for (int i = 0; i < WSIZE; i++){ if (_prev[i] >= WSIZE)_prev[i] -= WSIZE;else_prev[i] = 0;} }到這里,LZ77算法到基本原理已經講解完畢,其中還有很多細節在后面繼續講解
三、LZ77壓縮和解壓縮流程介紹
3.1 壓縮
上述在壓縮中用到的理論知識介紹完成之后,就可以開始LZ77的壓縮了,LZ77的壓縮過程具體如下:
a.計算哈希地址,將該字符串首字符在窗口中的位置插入到哈希桶中,并返回該桶的狀態matchHead
b.根據matchHead檢測是否找到匹配
- 如果matchHead等于0,未找到匹配,表示該三個字符在前文中沒有出現過,將該當前字符 作為源字符寫到壓縮文件中
- 如果matchHead不等于0,表示找到匹配,matchHead代表匹配鏈的首地址,從哈希桶matchHead位置開始找最長匹配,找到后用該(距離,長度對)替換該字符串寫到壓縮文件中,然后將該替換串三個字符一組添加到哈希表中。
3.2 壓縮格式數據保存
壓縮格式分三個文件保存:
3.2 解壓縮
LZ77的解壓縮非常簡單:
總結
以上是生活随笔為你收集整理的基于LZ77算法的文件压缩铺垫的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 哈希原理
- 下一篇: 基于LZ77算法的文件压缩