日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

基于LZ77算法的文件压缩铺垫

發布時間:2024/4/11 编程问答 49 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于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原理介紹
  • 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

    // 最小匹配長度 static const size_t MIN_MATCH = 3;// 最大匹配長度 // GZIP認為:長度超過255之后,長度必須要用兩個字節表示,會影響壓縮率,而大部分情況下, // 能夠匹配的長度都不會超過255,因此長度使用一個字節表示 // 而一個字節能夠表示的范圍是[0, 255], 如果讓0表示匹配長度為3個字符,1表示匹配長度為 //4個字符,...,則一個字節最多可以表示的匹配長度為255+3=258,即最長的匹配長度, // 如果某個匹配長度超過258,則拆成兩個匹配來進行表示 static const size_t MAX_MATCH = 258;

    文件壓縮過程:


    但是真正的壓縮,是在一個比較大的窗口中進行的,窗口越大,找到匹配的可能性就越大,但不是無限大, 因為無限大實,存在兩個問題

  • 空間成本:窗口越大,需要的內存空間就越大
  • 時間成本:窗口越大,查找匹配串時需要耗費的時間也就越多
  • 因此,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返回的狀態監測是否找到匹配串,如果當前桶為空,說明未找到匹配, 否則:可能找到匹配,再定位到匹配串位置詳細進行匹配即可。
  • 關于"哈希桶",引發出一堆問題:

  • 哈希桶的大小分析
    三個字符總共可以組成2^24 種取值(即16M),桶的個數需要 2^24個,而索引大小占2個字節,總共桶占32M 字節,是一個非常大的開銷。隨著窗口的移動,表中的數據會不斷過時,維護這么大的表,會降低程序 運行的效率。因此本文哈希桶的個數設置為:2^15 (即32K)
  • // 哈希桶的個數為2^15 const USH HASH_BITS = 15;// 哈希表的大小 const USH HASH_SIZE = (1 << HASH_BITS); // 哈希掩碼:主要作用是將右窗數據往左窗搬移時,用來更新哈希表中數據,具體參見后文 const USH HASH_MASK = HASH_SIZE - 1;
  • 哈希表的結構
    • 原本需要 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位

    // hashAddr: 上一個字符串計算出的哈希地址 // ch:當前字符 // 本次的哈希地址是在前一次哈希地址基礎上,再結合當前字符ch計算出來的 // HASH_MASK為WSIZE-1,&上掩碼主要是為了防止哈希地址越界 void HashTable::HashFunc(USH& hashAddr, UCH ch) {hashAddr = (((hashAddr) << H_SHIFT()) ^ (ch)) & HASH_MASK; } USH HashTable::H_SHIFT() {return (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH; }

    4.哈希表構建(插入字符串)
    哈希表的構建即將字符串插入到哈希表中,該過程伴隨著壓縮過程一塊進行:

    • 獲取當前字符ch(假設其在窗口中的位置為pos)
    • 用ch之后緊鄰的兩個字符構成當前串curStr
    • 插入curStr
    // hashAddr:上一次哈希地址 ch:先行緩沖區第一個字符 // pos:ch在滑動窗口中的位置 matchHead:如果匹配,保存匹配串的起始位置 void HashTable::InsertString(USH& hashAddr, UCH ch, USH pos, USH& macthHead) {// 計算哈希地址HashFunc(hashAddr, ch);// 隨著壓縮的不斷進行,pos肯定會大于WSIZE,與上WMASK保證不越界_prev[pos & WMASK] = _head[hashAddr];macthHead = _head[hashAddr];_head[hashAddr] = pos; }
    • 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的壓縮過程具體如下:

  • 打開帶壓縮的文件(注意:必須按照二進制格式打開,因為用戶進行壓縮的文件不確定)
  • 獲取文件大小,如果文件大小小于3個字節,則不進行壓縮
  • 讀取一個窗口的數據,即64K
  • 用前兩個字符計算第一個字符與其后兩個字符構成字符串哈希地址的一部分,因為哈希地址是通過三個字節算出來的,先用前兩個字節算出一部分,在壓縮時,再結合第三個字節算出第一個字符串完整的哈希地址。
  • 循環開始壓縮
    a.計算哈希地址,將該字符串首字符在窗口中的位置插入到哈希桶中,并返回該桶的狀態matchHead
    b.根據matchHead檢測是否找到匹配
    • 如果matchHead等于0,未找到匹配,表示該三個字符在前文中沒有出現過,將該當前字符 作為源字符寫到壓縮文件中
    • 如果matchHead不等于0,表示找到匹配,matchHead代表匹配鏈的首地址,從哈希桶matchHead位置開始找最長匹配,找到后用該(距離,長度對)替換該字符串寫到壓縮文件中,然后將該替換串三個字符一組添加到哈希表中。
  • 如果窗口中的數據小于MIN_LOOKAHEAD時,將右窗口中數據搬移到左窗口,從文件中新讀取一個窗口的數據放置到右窗,更新哈希表,繼續壓縮,直到壓縮結束。
  • 3.2 壓縮格式數據保存
    壓縮格式分三個文件保存:

  • 文件1保存比特標記為信息,用8個字節表示標記為長度,后面緊跟標記位
  • 文件2保存原字符和長度
  • 文件3保存所有的距離
  • 3.2 解壓縮
    LZ77的解壓縮非常簡單:

  • 從文件1中讀取標記,并對該標記進行分析
  • 如果當前標記是0,表示原字符,從文件2中讀取一個字節,直接寫到解壓縮之后的文件中
  • 如果當前標記是1,表示遇到(距離,長度對),從文件3中讀取一個兩個字節表示距離,從文件1中讀取一個字節表示長度,構建(距離,長度)對,然后從解壓縮過的結果中找出匹配長度
  • 獲取下一個標記,直到所有的標記解析完
  • 超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生

    總結

    以上是生活随笔為你收集整理的基于LZ77算法的文件压缩铺垫的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。