1. 程式人生 > >深入解析資料壓縮演算法

深入解析資料壓縮演算法

 

1、為什麼要做資料壓縮?

       資料壓縮的主要目的還是減少資料傳輸或者轉移過程中的資料量。

2、什麼是資料壓縮?

        是指在不丟失資訊的前提下,縮減資料量以減少儲存空間,提高傳輸、儲存和處理效率的一種技術方法。或者是按照一定的演算法對資料進行重新組織,減少資料的冗餘和儲存的空間。

  3、常見的資料壓縮演算法

(1).LZW壓縮

        LZW壓縮是一種無失真壓縮,應用於gif圖片。適用於資料中存在大量重固子串的情況。

原理:

       LZW演算法中,首先建立一個字串表,把每一個第一次出現的字串放入串表中,並用一個數字來表示,這個數字與此字串在串表中的位置有關,並將這個數字存入壓縮檔案中,如果這個字串再次出現時,即可用表示它的數字來代替,並將這個數字存入檔案中。壓縮完成後將串表丟棄。如"print" 字串,如果在壓縮時用266表示,只要再次出現,均用266表示,並將"print"字串存入串表中,在圖象解碼時遇到數字266,即可從串表中查出266所代表的字串"print",在解壓縮時,串表可以根據壓縮資料重新生成。

編碼過程:

     編碼後輸出:41 42 52 41 43 41 44 81 83 82 88 41 80。輸入為17個7位ASC字元,總共119位,輸出為13個8位編碼,總共104位,壓縮比為87%。

解碼過程:

     對輸出的41 42 52 41 43 41 44 81 83 82 88 41 80進行解碼,如下表所示:

解碼後輸出:

ABRACADABRABRABRA

特殊標記:
       隨著新的串(string)不斷被發現,標號也會不斷地增長,如果原資料過大,生成的標號集(string table)會越來越大,這時候操作這個集合就會產生效率問題。如何避免這個問題呢?Gif在採用lzw演算法的做法是當標號集足夠大的時候,就不能增大了,乾脆從頭開始再來,在這個位置要插入一個標號,就是清除標誌CLEAR,表示從這裡我重新開始構造字典,以前的所有標記作廢,開始使用新的標記。
      這時候又有一個問題出現,足夠大是多大?這個標號集的大小為比較合適呢?理論上是標號集大小越大,則壓縮比率就越高,但開銷也越高。 一般根據處理速度和記憶體空間連個因素來選定。GIF規範規定的是12位,超過12位的表達範圍就推倒重來,並且GIF為了提高壓縮率,採用的是變長的字長。比如說原始資料是8位,那麼一開始,先加上一位再說,開始的字長就成了9位,然後開始加標號,當標號加到512時,也就是超過9為所能表達的最大資料時,也就意味著後面的標號要用10位字長才能表示了,那麼從這裡開始,後面的字長就是10位了。依此類推,到了2^12也就是4096時,在這裡插一個清除標誌,從後面開始,從9位再來。

      GIF規定的清除標誌CLEAR的數值是原始資料字長表示的最大值加1,如果原始資料字長是8,那麼清除標誌就是256,如果原始資料字長為4那麼就是16。另外GIF還規定了一個結束標誌END,它的值是清除標誌CLEAR再加1。由於GIF規定的位數有1位(單色圖),4位(16色)和8位(256色),而1位的情況下如果只擴充套件1位,只能表示4種狀態,那麼加上一個清除標誌和結束標誌就用完了,所以1位的情況下就必須擴充到3位。其它兩種情況初始的字長就為5位和9位。

程式碼示例:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>
#include <algorithm>
#include <vector>
using namespace std;
long len=0;//原字串的長度
long loc=0;//去重之後字串的長度
map<string,long> dictionary;
vector <long> result;
#define MAX 100;
void LZWcode(string a,string s)
{
    //memset(&result,0,sizeof(int));
    string W,K;
    for(long i=0;i<loc;i++)
    {
        string s1;
        s1=s[i];//將單個字元轉換為字串
        dictionary[s1]=i+1;
    }
    W=a[0];
    loc+=1;
    for(int i=0;i<len-1;i++)
    {
        K=a[i+1];
        string firstT=W;
        string secontT=W;
        if(dictionary.count(firstT.append(K))!=0)//map的函式count(n),返回的是map容器中出現n的次數
            W=firstT;
        else
        {
            result.push_back(dictionary[W]);
            dictionary[secontT.append(K)]=loc++;
            W=K;
        }
    }
    if(!W.empty())
        result.push_back(dictionary[W]);
    for(int i=0;i<result.size();i++)
        cout<<result[i];
}
 
void LZWdecode(int *s,int n)
{
    string nS;
    for(int i=0;i<n;i++)
        for(map<string,long>::iterator it=dictionary.begin(); it!=dictionary.end();it++)
            if(it->second==s[i])
            {
                cout<<it->first<<" ";
            }
    for(map<string,long>::iterator it=dictionary.begin(); it!=dictionary.end();it++)//輸出壓縮編碼的字典表
        cout<<it->first<<" "<<it->second<<endl;
}
int main(int argc, char const *argv[])
{
    cout<<"本程式的解碼是根據輸入的編碼字元進行的解碼,並不是全256 的字元"<<endl;
    cout<<"選擇序號:"<<endl;
    cout<<"1.壓縮編碼   2.解碼"<<endl;
    int n;
    while(scanf("%d",&n)!=EOF)
    {
        switch(n)
        {
            case 1:
            {
                char s[100],a[100];
                cout<<"輸入一串字元:"<<endl;
                cin>>s;
                len=strlen(s);
                for(int i=0;i<len;i++)
                    a[i]=s[i];
                sort(s,s+len);//排序
                loc=unique(s,s+len)-s;//去重
                LZWcode(a,s);
                break;
            }
            case 2:
            {
                cout<<"輸入解碼陣列的長度:"<<endl;
                int changdu;
                cin>>changdu;
                cout<<"輸入解碼數串(每個數串以空格隔開):"<<endl;
                int s[changdu];
                for(int i=0;i<changdu;i++)
                    cin>>s[i];
                LZWdecode(s, changdu);
                break;
            }
            default:
                cout<<"你的輸入不正確,請從重新開始"<<endl;
        }
        if(n==2)
        {
            auto iter=result.begin();   // 每次正確輸入結束後對結果進行清零
            while(iter!=result.end())
                result.erase(iter++);
        }
    }
    return 0;
}

(2).霍夫曼壓縮

      哈夫曼編碼是無失真壓縮當中最好的方法。它使用預先二進位制描述來替換每個符號,長度由特殊符號出現的頻率決定。常見的符號需要很少的位來表示,而不常見的符號需要很多位來表示。哈夫曼演算法在改變任何符號二進位制編碼引起少量密集表現方面是最佳的。然而,它並不處理符號的順序和重複或序號的序列。

原理:

     利用資料出現的次數構造Huffman二叉樹,並且出現次數較多的資料在樹的上層,出現次數較少的資料在樹的下層。於是,我們就可以從根節點到每個資料的路徑來進行編碼並實現壓縮。

編碼過程:


假設有一個包含100000個字元的資料檔案要壓縮儲存。各字元在該檔案中的出現頻度如下所示:

       在此,我會給出常規編碼的方法和Huffman編碼兩種方法,這便於我們比較。

       常規編碼方法:我們為每個字元賦予一個三位的編碼,於是有:


       此時,100000個字元進行編碼需要100000 * 3 = 300000位。

       Huffman編碼:利用字元出現的頻度構造二叉樹,構造二叉樹的過程也就是編碼的過程。

這種情況下,對100000個字元編碼需要:(45 * 1 + (16 + 13 + 12 + 9)*3 + (9 + 5)*4) * 1000 = 224000

 

孰好孰壞,例子說明了一切!好了,老規矩,下面我還是用上面的例子詳細說明一下Huffman編碼的過程。

       首先,我們需要統計出各個字元出現的次數,如下:

       接下來,我根據各個字元出現的次數對它們進行排序,如下:

       好了,一切準備工作就緒。

       在上文我提到,huffman編碼的過程其實就是構造一顆二叉樹的過程,那麼我將各個字元看成樹中將要構造的各個節點,將字元出現的頻度看成權值。Ok,有了這個思想,here we go!

       構造huffman編碼二叉樹規則:

從小到大,

從底向上,

依次排開,

逐步構造。

       首先,根據構造規則,我將各個字元看成構造樹的節點,即有節點a、b、c、d、e、f。那麼,我先將節點f和節點e合併,如下圖:

於是就有:

經過排序處理得:

 

        接下來,將節點b和節點c也合併,則有:

       於是有:

       經過排序處理得:

       第三步,將節點d和節點fe合併,得:

       於是有:

       繼續,這次將節點fed和節點bc合併,得:

       於是有:

       最後,將節點a和節點bcfed合併,有:

       以上步驟就是huffman二叉樹的構造過程,完整的樹如下:

       二叉樹成了,最後就剩下編碼了,編碼的規則為:左0右1

       於是根據編碼規則得到我們最終想要的結果:

       從上圖中我們得到各個字元編碼後的編碼位:


程式碼示例:

哈夫曼樹結構:

struct element
{
    int weight;        // 權值域
    int lchild, rchild, parent;  // 該結點的左、右、雙親結點在陣列中的下標
};
weight儲存結點權值;lchild儲存該節點的左孩子在陣列中的下標;rchild儲存該節點的右孩子在陣列中的下標;parent儲存該節點的雙親孩子在陣列中的下標。

哈夫曼演算法的C++實現:

#include<iostream>
#include <iomanip>
 
using namespace std;
// 哈夫曼樹的結點結構
struct element
{
    int weight;        // 權值域
    int lchild, rchild, parent;  // 該結點的左、右、雙親結點在陣列中的下標
};
// 選取權值最小的兩個結點
void selectMin(element a[],int n, int &s1, int &s2)
{
    for (int i = 0; i < n; i++)
    {
        if (a[i].parent == -1)// 初始化s1,s1的雙親為-1
        {
            s1 = i;
            break;
        }
    }
    for (int i = 0; i < n; i++)// s1為權值最小的下標
    {
        if (a[i].parent == -1 && a[s1].weight > a[i].weight)
            s1 = i;
    }
    for (int j = 0; j < n; j++)
    {
        if (a[j].parent == -1&&j!=s1)// 初始化s2,s2的雙親為-1
        {
            s2 = j;
            break;
        }
    }
    for (int j = 0; j < n; j++)// s2為另一個權值最小的結點
    {
        if (a[j].parent == -1 && a[s2].weight > a[j].weight&&j != s1)
            s2 = j;
    }
}
// 哈夫曼演算法
// n個葉子結點的權值儲存在陣列w中
void HuffmanTree(element huftree[], int w[], int n)
{
    for (int i = 0; i < 2*n-1; i++)    // 初始化,所有結點均沒有雙親和孩子
    {
        huftree[i].parent = -1;
        huftree[i].lchild = -1;
        huftree[i].rchild = -1;
    }
    for (int i = 0; i < n; i++)    // 構造只有根節點的n棵二叉樹
    {
        huftree[i].weight = w[i];
    }
    for (int k = n; k < 2 * n - 1; k++) // n-1次合併
    {
        int i1, i2; 
        selectMin(huftree, k, i1, i2); // 查詢權值最小的倆個根節點,下標為i1,i2
        // 將i1,i2合併,且i1和i2的雙親為k
        huftree[i1].parent = k;
        huftree[i2].parent = k;
        huftree[k].lchild = i1;
        huftree[k].rchild = i2;
        huftree[k].weight = huftree[i1].weight + huftree[i2].weight;
    }
    
}
  // 列印哈夫曼樹
void print(element hT[],int n)
{
    cout << "index weight parent lChild rChild" << endl;
    cout << left;    // 左對齊輸出 
    for (int i = 0; i < n; ++i) 
    {
        cout << setw(5) << i << " ";
        cout << setw(6) << hT[i].weight << " ";
        cout << setw(6) << hT[i].parent << " ";
        cout << setw(6) << hT[i].lchild << " ";
        cout << setw(6) << hT[i].rchild << endl;
    }
}
int main()
{
    int x[] = { 5,29,7,8,14,23,3,11 };        // 權值集合
    element *hufftree=new element[2*8-1];    // 動態建立陣列
    HuffmanTree(hufftree, x, 8);
    print(hufftree,15);
    system("pause");
    return 0;
}
說明:

      parent域值是判斷結點是否寫入哈夫曼樹的唯一條件,parent的初始值為-1,當某結點加入時,parent域的值就設定為雙親結點在陣列的下標。構造哈夫曼樹時,首先將n個權值的葉子結點存放到陣列haftree的前n個分量中,然後不斷將兩棵子樹合併為一棵子樹,並將新子樹的根節點順序存放到陣列haftree的前n個分量的後面。

(3).遊程編碼(RLC)

           遊程編碼又稱“執行長度編碼”或“行程編碼”,是一種無失真壓縮編碼,JPEG圖片壓縮就用此方法,很多柵格資料壓縮也是採用這種方法。

           柵格資料如圖3-1所示:

                                                   

                                                                     3-1 柵格資料

  原理:

         用一個符號值或串長代替具有相同值的連續符號(連續符號構成了一段連續的“行程”。行程編碼因此而得名),使符號長度少於原始資料的長度。只在各行或者各列資料的程式碼發生變化時,一次記錄該程式碼及相同程式碼重複的個數,從而實現資料的壓縮。

        常見的遊程編碼格式包括TGA,Packbits,PCX以及ILBM。
例如:5555557777733322221111111
行程編碼為:(5,6)(7,5)(3,3)(2,4)(1,7)。可見,行程編碼的位數遠遠少於原始字串的位數。
並不是所有的行程編碼都遠遠少於原始字串的位數,但行程編碼也成為了一種壓縮工具。
例如:555555 是6個字元 而(5,6)是5個字元,這也存在壓縮量的問題,自然也會出現其他方式的壓縮工具。
在對影象資料進行編碼時,沿一定方向排列的具有相同灰度值的畫素可看成是連續符號,用字串代替這些連續符號,可大幅度減少資料量。

        遊程編碼記錄方式有兩種:①逐行記錄每個遊程的終點列號:②逐行記錄每個遊程的長度(像元數)

第一種方式:

        

      上面的柵格圖形可以記為:A,3  B,5  A,1  C,4  A,5

      第二種就記作:A,3  B,2  A,1  C,3   A,1

     行程編碼是連續精確的編碼,在傳輸過程中,如果其中一位符號發生錯誤,即可影響整個編碼序列,使行程編碼無法還原回原始資料。

程式碼示例:

       根據輸入的字串,得到大小寫不敏感壓縮後的結果(即所有小寫字母均視為相應的大寫字母)。輸入一個字串,長度大於0,且不超過1000,全部由大寫或小寫字母組成。輸出輸出為一行,表示壓縮結果,形式為:
(A,3)(B,4)(C,1)(B,2)

      即每對括號內部分別為字元(都為大寫)及重複出現的次數,不含任何空格。

樣例輸入:aAABBbBCCCaaaaa

樣例輸出:(A,3)(B,4)(C,3)(A,5)

#include<stdio.h>
#include<string.h>
char a[1001];
int main()
{
    char t;
    int i;
gets(a);
int g=1;
int k=strlen(a);
if(a[0]>='a'&&a[0]<='z')
    a[0]-=32;
  t=a[0];
for(i=1;i<=k;i++)
{
  if(a[i]>='a'&&a[i]<='z')
  a[i]-=32;
  if(a[i]==t)
      g++;
if(a[i]!=t)
  {
    printf("(%c,%d)",t,g);
     g=1;
       t=a[i];
  }
}return 0;
}
應用場景:

(1).區域單色影像圖

(2).紅外識別圖形

(3).同色區塊的彩色圖形

參閱資料:

https://blog.csdn.net/u012455213/article/details/45502573

https://www.cnblogs.com/smile233/p/8184492.html

    說明:部分圖源來自網路,感謝作者的分享。
--------------------- 
作者:老樊Lu碼 
來源:CSDN 
原文:https://blog.csdn.net/fanyun_01/article/details/80211799 
版權宣告:本文為博主原創文章,轉載請附上博文連結!

在網路網路傳輸過程中,最關心的就是傳輸效率問題。而提高傳輸效率最有效的方法就是對傳輸的資料進行壓縮。但壓縮資料也要耗費一定的時間,是不是壓縮後一定能提高效率呢?該如何選擇合適的壓縮演算法呢?請看本文的具體分析。

1.資料傳輸時間
假設資料大小為D (MB)

網路頻寬為       N (MBps)  -------------注意這裡是MBps,而不是通常說的Mbps,      1MBps = 10Mbps,       1000Mbps=100MBps.

那麼資料傳輸時間T1 = D/N 

2.壓縮後的資料傳輸時間


假設壓縮演算法壓縮率為 R     ------------------ 即壓縮後資料大小為D*R

壓縮速度為           Vc  MB/S

解壓縮速度為       Vd MB/S

那麼壓縮後的資料傳輸時間 T2 =  D/Vc + D*R/N + D/Vd  = D/N * ( R + N/Vc + N/Vd)

3.分析
對比:

        T1 = D/N

        T2 = D/N*(R+N/Vc+N/vd)

發現:

        如果R + N/Vc + N/Vd < 1,則壓縮後傳輸要更快,否則壓縮後傳輸反而更慢。

        也就是壓縮後傳輸能否更快是和壓縮演算法的 “壓縮率”,“壓縮/解壓縮速度” 以及當前“頻寬”相關

        壓縮率越小,壓縮/解壓縮越快,頻寬越小,壓縮後傳輸越能提高效率。而在頻寬不變得情況下,壓縮率越小,壓縮/解壓縮越快 越好。 

       而由於壓縮率和壓縮/解壓縮速度成指數型反比(壓縮率提高一點點,壓縮/解壓縮速度就大幅降低),所以在選用壓縮演算法時:

        最好選擇壓縮/解壓縮速度快的演算法,而不必太關注壓縮率(當然也不能完全不壓縮)

4.常用壓縮演算法對比
    這是來自網上一個常用壓縮演算法壓縮比,壓縮/解壓縮速度對比圖: 

    

來源:http://blog.csdn.net/zhangskd/article/details/17009111

    壓縮率R為 圖中的 1/Ratio。

    那麼帶入到上面公式:

    LZ4:1/2.084 + N/422 + N/1820 = 0.48 + N*0.0029   也就是說在頻寬N<179MBps的情況下,採用LZ4壓縮能提高傳輸效率。

    zlib:1/3.099 + N/21 + N/300 =  0.32 + N*0.051          也就是說在頻寬N<13.3Mbps的情況下,採用zlib壓縮才能提高傳輸效率,如果頻寬夠高,就不要壓縮了,否則會更慢

5.總結
  一般客戶端訪問伺服器,需進行壓縮。 (目前客戶端到伺服器的頻寬還是比較低的)

  伺服器間傳輸,可以不壓縮,或者用LZ4壓縮。 (伺服器間的頻寬一般是1000bps,即100MBps)

  在頻寬 N<3.3MBps的情況下, 使用zlib要比LZ4更快。

 0-3.3MBps                       zlib壓縮傳輸最快,lz4壓縮傳輸次之,普通傳輸最慢

 3.3 - 13.3MBps               lz4壓縮傳輸最快,zlib壓縮傳輸次之,普通傳輸最慢

 13.3-179MBps                lz4壓縮傳輸最快,普通傳輸次之,zlib壓縮傳輸 反而更慢

  大於179MBps                普通傳輸就可以,因為網路傳輸速度 遠遠高於壓縮及解壓縮速度了
--------------------- 
作者:jmppok 
來源:CSDN 
原文:https://blog.csdn.net/jmppok/article/details/38121115 
版權宣告:本文為博主原創文章,轉載請附上博文連結!