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

歡迎訪問 生活随笔!

生活随笔

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

编程问答

基于LZ77算法的文件压缩收尾

發布時間:2024/4/11 编程问答 42 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于LZ77算法的文件压缩收尾 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

基于Huffman算法和LZ77算法的文件壓縮(六)

前面基于Huffman算法和LZ77算法的文件壓縮(四)
基于Huffman算法和LZ77算法的文件壓縮(五)
已經充分講解LZ77到基本原理和實現細節。

本文開始講解文件的壓縮過程。

一、回顧整個壓縮過程

1.打開帶壓縮的文件(注意:必須按照二進制格式打開,因為用戶進行壓縮的文件不確定)
2.獲取文件大小,如果文件大小小于3個字節,則不進行壓縮
3.讀取一個窗口的數據,即64K,
4.用前兩個字符計算第一個字符與其后兩個字符構成字符串哈希地址的一部分,因為哈希地址是通過三個字節算出來的,先用前兩個字節算出一部分,在壓縮時,再結合第三個字節算出第一個字符串完整的哈希地址。
5.循環開始壓縮
a.計算哈希地址,將該字符串首字符在窗口中的位置插入到哈希桶中,并返回該桶的狀態matchHead
b.根據matchHead檢測是否找到匹配

  • 如果matchHead等于0,未找到匹配,表示該三個字符在前文中沒有出現過,將該當前字符 作為源字符寫到壓縮文件中
  • 如果matchHead不等于0,表示找到匹配,matchHead代表匹配鏈的首地址,從哈希桶matchHead位置開始找最長匹配,找到后用該(距離,長度對)替換該字符串寫到壓縮文件中,然后將該替換串三個字符一組添加到哈希表中

6 . 如果窗口中的數據小于MIN_LOOKAHEAD時,將右窗口中數據搬移到左窗口,從文件中新讀取一個窗口的數據放置到右窗,更新哈希表,繼續壓縮,直到壓縮結束。

二、先封裝一個哈希表

1.先給一個公共信息的文件common.h

#pragma once #include <iostream> #include <string> #include <assert.h> #include <string.h> #include <stdio.h> #include <stdlib.h>//記錄程序中用到的常量數據typedef unsigned char UCH; typedef unsigned short USH; typedef unsigned long long ULL;const USH MIN_MATCH = 3; //最小匹配長度 const USH MAX_MATCH = 258; //最大匹配長度 const USH WSIZE = 32 * 1024; //32k

2.哈希表的整體框架:

#pragma once #include "Common.hpp"class HashTable {public:HashTable(USH size);~HashTable();//哈希表插入,//matchHead為出參,帶出整個匹配鏈的頭,ch為匹配的字符,pos為//在緩沖區當中的下標。hashAddr為哈希地址:出入輸出型參數,//因為計算本次哈希地址需要用到上一個哈希地址void Insert(USH& matchHead, UCH ch, USH pos, USH& hashAddr);//哈希函數void HashFunc(USH& hashAddr, UCH ch);//獲取哈希表前一個匹配頭USH GetNext(USH matchHead);//在處理大于64k的大文件時,需要把右窗中的文件拷貝到左窗,需要更新哈希表void Update();private://哈希函數相關USH H_SHIFT();private:USH* prev_;USH* head_;};

3.哈希表的構造:

HashTable::HashTable(USH size):prev_(new USH[size * 2]) //哈希表中存放的是索引字符串的首地址//(距離字符串開始的相對下標), head_(prev_ + size) {memset(prev_, 0, size * 2 * sizeof(USH));//初始化為0,0也代表當前匹配是//鏈表的末尾(相當于用數組模擬的鏈表) }

4.哈希表的析構函數:

HashTable::~HashTable() {delete[] prev_;prev_ = nullptr; }

5.定義哈希函數相關變量:

const USH HASH_BITS = 15; //哈希地址15位 const USH HASH_SIZE = (1 << HASH_BITS); //哈希地址個數 32K const USH HASH_MASK = HASH_SIZE - 1; //防止溢出 低15位全1,因為prev的大小就WSIZE,而當start到達右窗 //時,下標明顯大于WSIZE,如果不處理就會下標越界

6.哈希函數相關:

//abcdefgh字符串 //hashaddr1:abc //hashaddr2:bcd//hashAddr:前一次計算出的哈希地址 abc //本次需要計算bcd哈希地址 //ch:本次匹配三個字符中的最后一個 //本次哈希地址是在上一次哈希地址的基礎上計算出來的 void HashTable::HashFunc(USH& hashAddr, UCH ch) { //hashAddr是輸入,輸出型參數,ch是所查找字符串中第一個字符hashAddr = (((hashAddr) << H_SHIFT()) ^ (ch)) & HASH_MASK; }USH HashTable::H_SHIFT() {return (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH; //5 }

7.向哈希表中插入元素:

//matchHead:匹配鏈的頭 //ch:查找字符串的第三個字符(也就是最后一個) //pos:查找字符串的頭到字符串開始的距離 //hashAddr:輸入時是上一次的哈希地址,輸出時是本次哈希地址 void HashTable::Insert(USH& matchHead, UCH ch, USH pos, USH& hashAddr) {HashFunc(hashAddr, ch); //獲取本次插入的哈希地址matchHead = head_[hashAddr];//找當前三個字符在查找緩沖區中找到的最近的個,//即匹配鏈的頭,將來會用這個頭來找匹配//將新的哈希地址插入鏈表//pos & HASH_MASK的目的是防止越界prev_[pos & HASH_MASK] = head_[hashAddr];head_[hashAddr] = pos; }

三、開始壓縮

1.整體框架

#pragma once #include "HashTable.h"class LZ77 {public:LZ77();~LZ77();void CompressFile(const std::string& strFilePath);void UNCompressFile(const std::string& strFilePath);private://找最長匹配USH LongestMatch(USH matchHead, USH& curMatchDist, USH start); //寫標記文件void WriteFlag(FILE* fOUT, UCH& chFlag, UCH& bitCount, bool isLen); //合并壓縮數據文件和標記信息文件void MergeFile(FILE* fOut, ULL fileSize);//在處理大于64k文件時,需要重新填充緩沖區的右窗口void FillWindow(FILE* fIn, size_t& lookAhead, USH& start);//獲取文件的后綴std::string GetStr(std::string filename);private://用來保存待壓縮數據的緩沖區即滑動窗口UCH* pWin_; //哈希表 HashTable ht_; };

2.構造函數:

LZ77::LZ77():pWin_(new UCH[WSIZE * 2])//初始化緩沖區的大小,ht_(WSIZE) //初始化hash表大小 {}

3.析構函數:

LZ77::~LZ77() {delete[] pWin_;pWin_ = nullptr; }

3.根據MIN_LOCKAHEAD來過濾文件,如果文件大小小于MIN_LOCKAHEAD則不進行匹配。

  • 注意文件的打開方式,和Huffman壓縮的原理一樣,為了保證壓縮算法的通用性,既可以壓縮文本文件也可以壓縮二進制文件,需要以rb方式打開
  • 如何獲取文件大小??結合fseek函數和ftell函數來獲取文件大小
FILE* fIn = fopen(strFilePath.c_str(), "rb");if (nullptr == fIn) {std::cout << "打開文件失敗" << std::endl;return;}//獲取文件大小fseek(fIn, 0, SEEK_END);//把文件指針移動到文件的末尾ULL fileSize = ftell(fIn);//獲取當前文件指針到文件開始位置的偏移量//1. 如果源文件的大小小于最小匹配長度 MIN_MATCH,則不進行處理,文件太小去進行壓縮反而達不到壓縮的目的if (fileSize <= MIN_MATCH) { //此處是小于3個字符,在common.hpp文件中定義的std::cout << "文件太小,不壓縮" << std::endl;return;}

4.從壓縮文件中讀取一個緩沖區的數據到窗口中

  • 注意在計算文件大小的時候已經把文件指針移動到文件的末尾,所以此處要把文件指針重新定位
//從壓縮文件中讀取一個緩沖區的數據到窗口中//上面把文件指針移動到文件末尾,還得移回來,//否則讀不到任何數據fseek(fIn, 0, SEEK_SET);//表示先行緩沖區中剩余的字節數size_t lookAhead = fread(pWin_, 1, 2 * WSIZE, fIn);

5 .先計算最開始兩個字符的哈希地址

  • 因為哈希地址是根據前面的字符的哈希地址求的,所以要先計算前兩個字符的哈希地址。如abcdefg……,那么滿3個字符才可以進行匹配,所以需要先計算ab的哈希地址才可以計算c的哈希地址,然后進行匹配
USH hashAddr = 0;//記錄哈希地址//設置起始hashAddr (前兩個字符的hash地址)for (USH i = 0; i < MIN_MATCH - 1; ++i) {ht_.HashFunc(hashAddr,pWin_[i]);}

6.循環進行壓縮

  • 注意匹配鏈頭matchHead的含義。如果為0,代表沒找到匹配,因為哈希表初始化的時候都為0
  • 那么如果找到匹配,就順著匹配鏈往下找最長匹配
  • 封裝一個LongestMatch函數來進行查找最長匹配
  • 在找最長匹配LongestMatch的時候需要獲得當前匹配鏈的下一個匹配頭
//獲取當前匹配頭的下一個匹配頭(如果為0代表匹配結束) USH HashTable::GetNext(USH matchHead) {return prev_[matchHead & HASH_MASK]; }
  • 找到最長匹配就需要返回長度距離信息
//在找的過程中,需要將每次找到的匹配結果進行比較,保持最長匹配 USH LZ77::LongestMatch(USH matchHead, USH& MatchDist, USH start) { //找最長匹配USH curMatchLen = 0; //一次匹配的長度USH maxMatchLen = 0;UCH maxMatchCount = 255; //最大的匹配次數,解決環狀鏈USH curMatchStart = 0; //當前匹配在查找緩沖區中的起始位置//在先行緩沖區中查找匹配時,不能太遠即不能超過MAX_DISTUSH limit = start > MAX_DIST ? start - MAX_DIST : 0;do {//字符串在先行緩沖區匹配范圍UCH* pstart = pWin_ + start;UCH* pend = pstart + MAX_MATCH;//從查找緩沖區匹配串的起始開始進行字符比較UCH* pMatchStart = pWin_ + matchHead;curMatchLen = 0;//可以進行本次匹配while (pstart < pend && *pstart == *pMatchStart) {//更新變量++curMatchLen;++pstart;++pMatchStart;}//一次匹配結束if (curMatchLen > maxMatchLen) {maxMatchLen = curMatchLen;//記錄當前匹配的開始位置,方便后面計算匹配距離curMatchStart = matchHead;}} while ((matchHead = ht_.GetNext(matchHead)) > limit&& maxMatchCount--);MatchDist = start - curMatchStart;return maxMatchLen; }
  • while ((matchHead = ht_.GetNext(matchHead)) > limit && maxMatchCount--):matchHead代表下一個匹配頭即在緩沖區當中的下標。

  • USH limit = start > MAX_DIST ? start - MAX_DIST : 0;代表如果start > MAX_DIST就從start - MAX_DIST找匹配頭,否則就從開始找匹配頭

  • matchHead = ht_.GetNext(matchHead)) > limit代表在limit的范圍內進行找匹配,太遠就不進行匹配

  • maxMatchCount--是為了解決&WMASK帶來死循環的問題及太遠不進行匹配

  • 執行找最長匹配后,需要驗證是否成功找到匹配,因為matchHead可能為0,就沒有執行找最長匹配函數

  • 前面我們說過最大匹配長度為[0,258],如果用一個字節來記錄是有問題的,需要用兩個字節來保存,而寫入文件的時候又不能寫入兩個字節,所以在寫入長度的時候臨時定義一個字節變量保存原來用兩個字節來保存長度的變量-3,最后把一個字節的臨時變量寫入當壓縮文件當中,這樣就解決問題了

  • 如果沒找到匹配就需要把當前字符寫入到壓縮文件當中,如果找到最長匹配,就需要寫入長度距離信息到壓縮文件當中(注意寫長度的時候要把長度-3),但是壓縮數據和長度距離對信息無法區分

  • 所以還要寫入標記信息:用0標記原字符,1標記長度(距離不用標記,下兩個字節就是,)

  • 但是標記信息和壓縮數據應該保存在不同的文件當中,無法一邊寫壓縮數據一邊寫標記信息,那樣無法區分

//chFlag:該字節中的每個比特位是用來區分當前字節是原字符還是長度? //0:原字符 //1:長度 //bitCount:該字節中的多少個比特位已經被設置 //isCharOrLen:代表該字節是源字符還是長度,判斷壓縮數據還是長度信息 void LZ77::WriteFlag(FILE* fOUT, UCH& chFlag, UCH& bitCount, bool isLen) {chFlag <<= 1;if (isLen)//如果是長度,就給當前的比特位置為1chFlag |= 1;bitCount++;if (bitCount == 8) {//代表chFlag8位已經設置完,將該標記寫入到壓縮文件中fputc(chFlag, fOUT);//重新開始chFlag = 0;bitCount = 0;} }
  • 注意如果找到匹配,在執行寫入長度距離信息后,還需要把匹配的字符寫入到哈希表當中,否則無法進行下一次匹配(匹配的長度那么多的字符是不進行下一次匹配的,因為已經被替換掉),如abcdefg,如果abc找到匹配,把長度距離信息寫完后還要把bcd、cde寫入到哈希表當中,下一次直接從d開始找匹配,而又因為abc在之前插入的時候已經寫過了,此處就不用寫。

7.壓縮文件完整代碼

//壓縮文件 void LZ77 :: CompressFile(const std::string& strFilePath) {FILE* fIn = fopen(strFilePath.c_str(), "rb");if (nullptr == fIn) {std::cout << "打開文件失敗" << std::endl;return;}//獲取文件大小fseek(fIn, 0, SEEK_END);//把文件指針移動到文件的末尾ULL fileSize = ftell(fIn);//獲取當前文件指針到文件開始位置的偏移量//1. 如果源文件的大小小于最小匹配長度 MIN_MATCH,則不進行處理,文件太小去進行壓縮反而達不到壓縮的目的if (fileSize <= MIN_MATCH) { //此處是小于3個字符,在common.hpp文件中定義的std::cout << "文件太小,不壓縮" << std::endl;return;}//從壓縮文件中讀取一個緩沖區的數據到窗口中fseek(fIn, 0, SEEK_SET);//上面把文件指針移動到文件末尾,還得移回來,否則讀不到任何數據size_t lookAhead = fread(pWin_, 1, 2 * WSIZE, fIn);//表示先行緩沖區中剩余的字節數USH hashAddr = 0;//記錄哈希地址//設置起始hashAddr (前兩個字符的hash地址)for (USH i = 0; i < MIN_MATCH - 1; ++i) {ht_.HashFunc(hashAddr,pWin_[i]);}//開始寫壓縮數據://根據獲取的文件后綴打開一個同樣文件后綴的壓縮數據存儲文件//std::string str = GetStr(strFilePath);//FILE* ffIn = fopen("5.txt","wb");//fwrite(str.c_str(),sizeof(str),1,ffIn);//fclose(ffIn);//壓縮FILE* fOUT = fopen("2.lzp", "wb");assert(fOUT);USH start = 0;//查找字符串在緩沖區的地址,隨著壓縮的不斷進行,start不斷的在先行緩沖區中增加//與查找最長匹配相關的變量USH matchHead = 0; //匹配字符串的頭USH curMatchLength = 0; //一次匹配字符串的長度USH curMatchDist = 0; //一次匹配字符串的距離//與寫標記相關的變量UCH chFlag = 0;//代表當前字符是字符還是長度UCH bitCount = 0;//代表1個字節已經用了多少字節//寫標記的文件FILE* fOutF = fopen("3.txt", "wb");assert(fOutF);//lookAhead表示先行緩沖區中剩余字節的個數while (lookAhead) {//1.將第三個字符插入到哈希表中,因為前兩個字符已經插入到哈希表當中,//(pWin_[start],pWin_[satrt + 1].pWin_[start + 2])并獲取匹配鏈的頭ht_.Insert(matchHead, pWin_[start + 2], start, hashAddr);//因為不只進行一此匹配,每次匹配前都要置為0,防止影響后面的數據curMatchLength = 0;curMatchDist = 0;//2.驗證在查找緩沖區中是否找到匹配,如果有匹配,找最長匹配//因為在初始化哈希表的時候都設置為0,代表沒有任何匹配,如果不為0,代表有匹配if (matchHead) {//順著匹配鏈找最長匹配,最終帶出<長度,距離>對curMatchLength = LongestMatch(matchHead, curMatchDist, start);}//3.驗證是否找到匹配if (curMatchLength < MIN_MATCH) {//在查找緩沖區中未找到重復字符串//將start位置的字符寫入到壓縮文件中fputc(pWin_[start], fOUT); //寫字符//寫當前原字符對應的標記WriteFlag(fOutF, chFlag, bitCount, false);//寫標記//更新變量++ start;lookAhead--;}else {//找到匹配//將《長度,距離》對寫入壓縮文件中//寫長度UCH chLen = curMatchLength - 3;//因為長度是1個字節,其范圍本來是//[0,255],但是因為是3個一組,所以其范圍是[3,258]fputc(chLen, fOUT);//寫距離fwrite(&curMatchDist, sizeof(curMatchDist), 1, fOUT);//寫當前原字符對應的標記WriteFlag(fOutF, chFlag, bitCount, true);//更新先行緩沖區中剩余的字節數lookAhead -= curMatchLength;//將已經匹配的字符串按照三個一組將其插入到哈希表中--curMatchLength; //當前字符串已經插入++start;while (curMatchLength) {ht_.Insert(matchHead, pWin_[start + 2], start, hashAddr);--curMatchLength;++start;}}//檢測先行緩沖區中剩余字符個數,如果小于最短匹配長度,//需要更新緩沖區和哈希表,向緩沖區中重新填充內容,if (lookAhead <= MIN_LOOKAHEAD)FillWindow(fIn, lookAhead, start);}//標記位數如果不夠八位,因為寫標記是8位寫一次,最后可能//不夠8bit,所以要另外判斷if (bitCount > 0 && bitCount < 8) {chFlag <<= (8 - bitCount);fputc(chFlag, fOutF);}fclose(fOutF);//把壓縮數據文件和標記信息文件合并MergeFile(fOUT, fileSize);//刪除標記信息文件remove("./3.txt");fclose(fIn);fclose(fOUT); }

到這里文件壓縮過程基本結束了,還有大文件處理方式在后面解決

總結

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

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