數據壓縮算法---霍夫曼編碼的分析與實現

霍夫曼編碼是一種基於最小冗餘編碼的壓縮算法。最小冗餘編碼是指,若是知道一組數據中符號出現的頻率,就能夠用一種特殊的方式來表示符號從而減小數據須要的存儲空間。一種方法是使用較少的位對出現頻率高的符號編碼,用較多的位對出現頻率低的符號編碼。咱們要意識到,一個符號不必定必須是文本字符,它能夠是任何大小的數據,但每每它只佔一個字節。node

熵和最小冗餘

每一個數據集都有必定的信息量,這就是所謂的熵一組數據的熵是數據中每一個符號熵的總和。符號z的熵S定義爲:c++

Sz = -lg2 Pz算法

其中,Pz就數據集中z出現的頻率。來看一個例子,若是z在有32個符號的數據集中出現了8次,也就是1/4的機率,那麼z的熵就是:數組

-lg 2(1/4) = 2位緩存

這意味着若是用超過兩位的數來表示z將是一種浪費。若是在通常狀況下用一個字節(即8位)來表示一個符號,那麼在這種狀況下使用壓縮算法能夠大幅減少數據的容量。數據結構

下表展現如何計算一個有72個數據實例熵的例子(其中有5個不一樣的符號)。要作到這一點,需將每一個字符的熵相加。以U爲例,它在數據集中出現了12次,因此每一個U的實例的熵計算以下:函數

符號 機率 每一個實例的熵 總的熵
U 12/72 2.584 963 31.019 55
V 18/72 2.000 000 36.000 00
W 7/72 3.362 570 23.537 99
X 15/72 2.263 034 33.945 52
Y 20/72 1.847 997 36.959 94

-lg2(12/72) = 2.584 963 位測試

因爲U在數據中出現了12次,所以整個數據的熵爲:ui

2.584 963 * 12 = 31.019 55 位編碼

爲了計算數據集總體的熵,將每一個字符所貢獻的熵相加。每一個字符的熵在表中已經計算出來了:

31.019 55 + 36.000 00 + 23.537 99 + 33.945 52 + 36.959 94 = 161.463 00 位

若是使用8位來表示每一個符號,須要72 * 8 = 576位的空間,因此理論上來講,能夠最多將此數據壓縮:

1 - (161.463 000/576) = 72%

構造霍夫曼樹

霍夫曼編碼展示了一種基於熵的數據近似的最佳表現形式它首先生成一個稱爲霍夫曼樹的數據結構,霍夫曼樹自己是一棵二叉樹,用它來生成霍夫曼編碼。霍夫曼編碼是用來表示數據集合中符號的編碼,用這種編碼的方式達到數據壓縮的目的。然而,霍夫曼編碼的壓縮結果每每只能接近於數據的熵,由於符號的熵每每是有小數位的,而在實際中,霍夫曼編碼所用的位數不可能有小數位,因此有些代碼會超過實際最優的代碼位數。

 下圖展現了用上表中的數據來構建一棵霍夫曼樹的過程。構建的過程每每是從葉子結點向上進行。首先,將每一個符號和頻率放到它自身的樹中(步驟1)而後,將兩個頻率最小的根結點的樹合併,並將其頻率之和放到樹的新結點中(步驟2)這個過程反覆持續下去,直到最後只剩下一棵樹(這棵樹就是霍夫曼樹,步驟5)霍夫曼的根結點包含數據中符號的總個數,它的葉子結點包含原始的符號和符號的頻率。因爲霍夫曼編碼就是在不斷尋找兩棵最適合合併的樹,所以它是貪婪算法的一個很好的例子。

壓縮和解壓縮數據

創建一棵霍夫曼樹是數據壓縮和解壓縮的一部分。

用霍夫曼樹壓縮數據,給定一個具體的符號從樹的根開始,而後沿着樹的葉向葉子結點追蹤。在向下追蹤的過程當中,當向左分支移動時,向當前編碼的末尾追加0;當向右移動時,向當前編碼的末尾追加1。在上圖中,追蹤「U」的霍夫曼編碼,首先向右移動(1),而後向左移動(10),而後再向右(101)。圖中符號的霍夫曼編碼分別爲:

U=101,V=01,W=100,X=00,Y=11

要解壓縮用霍夫曼樹編碼的數據,要一位一位地讀取壓縮數據。從樹的根開始,當在數據中遇到0時,向樹的左分支移動;當遇到1時,向右分支移動。一旦到達一個葉子結點就找到了符號。接着從頭開始,重複以上過程,直到全部的壓縮數據都找出。用這種方法來解壓縮數據是可能的,這是由於霍夫曼樹是屬於前綴樹。前綴樹是指一組代碼中,任何一個編碼都不是另外一個編碼的前綴。這就保證了編碼被解碼時不會有多義性。例如,「V」的編碼是01,01不會是任何其餘編碼的前綴。所以,只要在壓縮數據中碰到了01,就能夠肯定它表示的符號是「V」。

霍夫曼編碼的效率

爲了肯定霍夫曼編碼下降了多大容量的存儲空間,首先要計算每一個符號出現的次數與其編碼位數的乘積,而後將其求和。因此,上表中壓縮後的數據的大小爲:

12*3 + 18*2 + 7*3 + 15*2 +20*2 = 163位

假設不使用壓縮算法的72個字符均用8位表示,那麼其總共所佔的數據大小爲576位,因此其壓縮比計算以下:

1 - (163/576)=71.7%

再次強調的是,在實際中沒法用小數來表示霍夫曼編碼,因此在不少狀況下這個壓縮比並無數據的熵效果那麼好。但也很是接近於最佳壓縮比。

在一般狀況下,霍夫曼編碼並非最高效的壓縮方法,但它壓縮和解壓縮的速度很是快。通常來講,形成霍夫曼編碼比較耗時的緣由是它須要掃描兩次數據:一次用來計算頻率;另外一次纔是用來壓縮數據。而解壓縮數據很是高效,由於解碼每一個符號的序列只須要掃描一次霍夫曼樹。

霍夫曼編碼的接口定義

huffman_compress


int huffman_compress(const unsigned char *original, unsigned char **compressed, int size);

返回值:若是數據壓縮成功,返回壓縮後數據的字節數;不然返回-1。

描述:用霍夫曼編碼的方法壓縮緩衝區original中的數據,original包含size字節的空間。壓縮後的數據存入緩衝區compressed中。因爲函數調用者並不知道compressed須要多大的空間,所以須要經過函數調用malloc來動態分配存儲空間。當這塊存儲空間再也不使用時,由調用者調用函數free來釋放空間。

複雜度:O(n),其中n表明原始數據中符號的個數。

huffman_uncompress


int huffman_uncompress(const unsigned char *compressed, unsigned char **original);

返回值:若是解壓縮成功,返回恢復後數據的字節數;不然返回-1。

描述:用霍夫曼的方法解壓縮緩衝區compressed中的數據。假定緩衝區包含的數據是由Huffman_compress壓縮產生的。恢復後的數據存入緩衝區original中。因爲函數調用者並不知道original須要多大的空間,所以要經過函數調用malloc來動態分配存儲空間。當這塊存儲空間再也不使用時,由調用者調用free來釋放空間。

複雜度:O(n),其中n是原始數據中符號的個數。

 霍夫曼編碼的分析與實現

經過霍夫曼編碼,在壓縮過程當中,咱們將符號按照霍夫曼樹進行編碼從而壓縮數據。在解壓縮時,重建壓縮過程當中的霍夫曼樹,同時將編碼解碼成符號自己。在本節介紹的實現過程當中,一個原始符號都是用一個字節表示。

huffman_compress

huffman_compress操做使用霍夫曼編碼來壓縮數據。首先,它掃描數據,肯定每一個符號出現的頻率。將頻率存放到數組freqs中。完成對數據的掃描後,頻率獲得必定程度的縮放,所以它們能夠只用一個字節來表示。當肯定數據中某個符號的最大出現頻率,而且相應肯定其餘頻率後,這個掃描過程結束。因爲數據中沒有出現的符號,應該只是頻率值爲0的符號,因此執行一個簡單的測試來確保當任何非0頻率值其縮減爲小於1時,最終應該將其值設爲1而不是0。

一旦計算出了全部的頻率,就調用函數build_tree來創建霍夫曼樹。此函數首先將數據中至少出現過一次的符號插入優先隊列中(其實是一棵二叉樹)。樹中的結點由數據結構HuffNode定義。此結構包含兩個成員:symbol爲數據中的符號(僅在葉子結點中使用);freq爲頻率。每棵樹初始狀態下只包含一個結點,此結點存儲一個符號和它的縮放頻率(就像在數據freqs中記錄和縮放的同樣)。

要創建霍夫曼樹,經過優先隊列用一個循環對樹作size-1次合併。在每次迭代過程當中,兩次調用pqueue_extract來提取根結點頻率最小的兩棵二叉樹。而後,將兩棵樹合併到一棵新樹中,將兩棵樹的頻率和存放到新樹的根結點中,接着把新的樹保存回優先級隊列中。這個過程會一直持續下去,直到size-1次迭代完成,此時優先級隊列中只有一棵二叉樹,這就是霍夫曼樹。

利用上一步創建的霍夫曼樹,調用函數build_table來創建一個霍夫曼編碼表,此表指明每一個符號的編碼。表中每一個條目都是一個HuffCode結構。此結構包含3個成員:used是一個默認爲1的標誌位,它指示此條目是否已經存放一個代碼;code是存放在條目中的霍夫曼編碼;size是編碼包含的位數。每一個編碼都是一個短整數,由於能夠證實當全部的頻率調整到能夠用一個字節來表示時,沒有編碼會大於16位。

使用一個有序的遍歷方法來遍歷霍夫曼樹,從而構建這個表。在每次執行build_table的過程當中,code 記錄當前生成的編碼,size保存編碼的位數。在遍歷樹時,每當選擇左分支時,將0追加到編碼的末尾中;每當選擇右分支時,將1追加到編碼的末尾中。一旦到達一個葉子結點,就將霍夫曼編碼存放到編碼表合適的條目中。在存放每一個編碼的同時,調用函數htons,以確保編碼是以大端字節格式存放。這一步很是重要,由於在下一步生成壓縮數據時須要用大端格式,一樣在解壓縮過程當中也須要大端格式。

在產生壓縮數據的同時,使用ipos來保存原始數據中正在處理的當前字節,並用opos來保存向壓縮數據緩衝區寫入的當前位。首先,縮寫一個頭文件,這有助於在huffman_uncompress中重建霍夫曼樹。這個頭文件包含一個四字節的值,表示待編碼的符號個數,後面跟着的是全部256個可能的符號出現的縮放頻率,也包括0。最後對數據編碼,一次讀取一個符號,在表中查找到它的霍夫曼編碼,並將每一個編碼存放到壓縮緩衝區中。在壓縮緩衝區中爲每一個字節分配空間。

huffman_compress的時間複雜度爲O(n),其中n是原始數據中符號的數量。

huffman_uncompress

huffman_uncompress操做解壓縮由huffman_compress壓縮的數據。首先,此操做讀取追加到壓縮數據的頭。回想一下,頭的前4個字節包含編碼符號的數量。這個值存放在size中。接下來的256個字節包含全部符號的縮放頻率。

利用存放在頭中的信息,調用build_tree重建壓縮過程當中用到的霍夫曼樹。一旦重建了樹,接下來就要生成已恢復數據的緩衝區。要作到這一點,從壓縮數據中逐位讀取數據。從霍夫曼樹的根開始,只要遇到位數0,就選擇左分支;只要遇到位數1,就選擇右分支。一旦到達葉子結點,就獲取一個符號的霍夫曼編碼。解碼符號存儲在葉子中。因此, 將此符號寫入已恢復數據的緩衝區中。寫入數據以後,從新回到根部,而後重複以上過程。使用ipos來保存向壓縮數據緩衝區寫入的當前位,並用opos來保存寫入恢復緩衝區中的當前字節。一旦opos到達size,就從原始數據中生成了全部的符號。

huffman_uncompress的時間複雜度爲O(n)。其中n是原始數據中符號的數量。這是由於對每一個要解碼符號來講,在霍夫曼樹中向下尋找的深度是一個有界常量,這個常量依賴於數據中不一樣符號的數量。在本節的實現中,這個常量是256.創建霍夫曼樹的過程不影響huffman_uncompress的複雜度,由於這個過程只依賴於數據中不一樣符號的個數。

 示例:霍夫曼編碼的實現文件

/*huffman.c*/
#include <limit.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <string.h>

#include "bit.h"
#include "bitree.h"
#include "compress.h"
#include "pqueue.h"

/*compare_freq 比較樹中霍夫曼節點的頻率*/
static int compare_freq(const void *tree1,const void *tree2)
{
    HuffNode *root1,root2;

    /*比較兩棵二叉樹根結點中所存儲的頻率大小*/
    root1 = (HuffNode *)bitree_data(bitree_root((const BiTree *)tree1));
    root2 = (HuffNode *)bitree_data(bitree_root((const BiTree *)tree2));

    if(root1->freq < root2->freq)
        return 1;
    else if(root1->freq > root2->freq)
        return -1;
    else return 0;
}
/*destroy_tree  消毀二叉樹*/
static void destroy_tree(void *tree)
{
    /*從優先級隊列中消毀並釋放一棵二叉樹*/
    bitree_destroy(tree);
    free(tree);

    return;
}
/*buile_tree 構建霍夫曼樹,每棵樹初始只包含一個根結點*/
static int bulid_tree(int *freqs,BiTree **tree)
{
    BiTree *init,
           *merge,
           *left,
           *right;
    PQueue pqueue;
    HuffNode *data;
    int size,c;

    /*初始化二叉樹優先級隊列*/
    *tree = NULL;
     pqueue_init(&pqueue,compare_freq,destroy_tree);
     for(c=0; c<=UCHAR_MAX; c++)
     {
         if(freqs[c] != 0)
         {
             /*創建當前字符及頻率的二叉樹*/
             if((init = (BiTree *)malloc(sizeof(BiTree))) == NULL)
             {
                 pqueue_destroy(&pqueue);
                 return -1;
             }

             bitree_init(init,free);
             if((data = (HuffNode*)malloc(sizeof(HuffNode))) == NULL)
             {
                 pqueue_destroy(&pqueue);
                 return -1;
             }

             data->symbol = c;
             data->freq = freqs[c];

             if(bitree_ins_left(init,NULL,data) != 0)
             {
                 free(data);
                 bitree_destroy(init);
                 free(init);
                 pqueue_destroy(&pqueue);
                 return -1;
             }

             /*將二叉樹插入優先隊列*/
             if(pqueue_insert(&pqueue,init) != 0)
             {
                 bitree_destroy(init);
                 free(init);
                 pqueue_destroy(&pqueue);
                 return -1;
             }
         }
     }
     /*經過兩兩合併優先隊列中的二叉樹來構建霍夫曼樹*/
     for(c=1; c<=size-1; c++)
     {
         /*爲合併後的樹分配空間*/
         if((merge = (BiTree *)malloc(sizeof(BiTree))) == NULL)
         {
             pqueue_destroy(&pqueue);
             return -1;
         }

         /*提取隊列中擁有最小頻率的兩棵樹*/
         if(pqueue_extract(&pqueue,(void **)&left) != 0)
         {
             pqueue_destroy(&pqueue);
             free(merge);
             return -1;
         }
         if(pqueue_extract(&pqueue,(void **)right) !=0)
         {
             pqueue_destroy(&pqueue);
             free(merge);
             return -1;
         }
         /*分配新產生霍夫曼結點的空間*/
         if((data = (HuffNode *)malloc(sizeof(HuffNode))) == NULL)
         {
             pqueue_destroy(&pqueue);
             free(merge);
             return -1;
         }

         memset(data,0,sizeof(HuffNode));

         /*求和前面提取的兩棵樹的頻率*/
         data->freq = ((HuffNode *)bitree_data(bitree_root(left)))->freq +
                      ((HuffNode *)bitree_data(bitree_root(right)))->freq;

        /*合併left、right兩棵樹*/
        if(bitree_merge(merge,left,right,data) != 0)
        {
            pqueue_destroy(&pqueue);
            free(merge);
            return -1;
        }

        /*把合併後的樹插入到優先級隊列中,並釋放left、right棵樹*/
        if(pqueue_insert(&pqueue,merge) != 0)
        {
            pqueue_destroy(&pqueue);
            bitree_destroy(merge);
            free(merge);
            return -1;
        }

        free(left);
        free(right);
     }

     /*優先隊列中的最後一棵樹便是霍夫曼樹*/
     if(pqueue_extract(&pqueue,(void **)tree) != 0)
     {
         pqueue_destroy(&pqueue);
         return -1;
     }
     else
     {
         pqueue_destroy(&pqueue);
     }
     return 0;
}

/*build_table 創建霍夫曼編碼表*/
static void build_table(BiTreeNode *node, unsigned short code, unsigned char size, HuffCode *table)
{
    if(!bitree_is_eob(node))
    {
        if(!bitree_is_eob(bitree_left(node)))
        {
            /*向左移動,並將0追加到當前代碼中*/
            build_table(bitree_left(node),code<<1,size+1,table);
        }

        if(!bitree_is_eob(bitree_right(node)))
        {
            /*向右移動,並將1追加到當前代碼中*/
            build_table(bitee_right(node),(code<<1) | 0x0001,size+1,table);
        }

        if(bitree_is_eob(bitree_left(node)) && bitree_is_eob(bitree_right(node)))
        {
            /*確保當前代碼是大端格式*/
            code = htons(code);

            /*將當前代碼分配給葉子結點中的符號*/
            table[((HuffNode *)bitree_data(node))->symbol].used = 1;
            table[((HuffNode *)bitree_data(node))->symbol].code = code;
            table[((HuffNode *)bitree_data(node))->symbol].size = size;
        }
    }
    return;
}
/*huffman_compress 霍夫曼壓縮*/
int huffman_compress(const unsigned char *original, unsigned char **compressed, int size)
{
    BiTree        *tree;
    HuffCode      table[UCHAR_MAX + 1];
    int           freqs[UCHAR_MAX + 1],
                  max,
                  scale,
                  hsize,
                  ipos,opos,cpos,
                  c,i;
    unsigned      *comp,*temp;

    /*初始化,沒有壓縮數據的緩衝區*/
    *compressed = NULL;

    /*獲取原始數據中每一個符號的頻率*/
    for(c=0; c <= UCHAR_MAX; c++)
        freqs[c] = 0;

    ipos = 0;

    if(size > 0)
    {
        while(ipos < size)
        {
            freqs[original[ipos]]++;
            ipos++;
        }
    }

    /*將頻率縮放到一個字節*/
    max = UCHAR_MAX;

    for(c=0; c<=UCHAR_MAX; c++)
    {
        if(freqs[c] > max)
            max = freqs[c];
    }

    for(c=0; c <= UCHAR_MAX; c++)
    {
        scale = (int)(freqs[c] / ((double)max / (double)UCHAR_MAX));

        if(scale == 0 && freqs[c] != 0)
            freqs[c] = 1;
        else
            freqs[c] = scale;
    }

    /*創建霍夫曼樹和編碼表*/
    if(build_tree(freqs,&tree) != 0)
        return -1;

    for(c=0; c<=UCHAR_MAX; c++)
        memset(&table[c],0,sizeof(HuffCode));

    bulid_table(bitree_root(tree), 0x0000, 0, table);

    bitree_destroy(tree);
    free(tree);

    /*編寫一個頭代碼*/
    hsize = sizeof(int) + (UNCHAR_MAX + 1);

    if((comp = (unsigned char *)malloc(hsize)) == NULL)
        return -1;

    memcpy(comp,&size,sizeof(int));

    for(c=0; c<=UCHAR_MAX; c++)
        comp[sizeof(int) + c] = (unsigned char)freqs[c];

    /*壓縮數據*/
    ipos = 0;
    opos = hsize*8;

    while(ipos < size)
    {
        /*獲取原始數據中的下一個字符*/
        c = original[ipos];

        /*將字符對應的編碼寫入壓縮數據的緩存中*/
        for(i=0; i<table[c].size; i++)
        {
            if(opos % 8 == 0)
            {
                /*爲壓縮數據的緩存區分配另外一個字節*/
                if((temp = (unsigned char *)realloc(comp,(opos/8)+1)) == NULL)
                {
                    free(comp);
                    return -1;
                }
                comp = temp;
            }
            cpos = (sizeof(short)*8) - table[c].size + i;
            bit_set(comp, opos, bit_get((unsigned char *)&table[c].code,cpos));
            opos++;
        }
        ipos++;
    }
    /*指向壓縮數據的緩衝區*/
    *compressed = comp;

    /*返回壓縮緩衝區中的字節數*/
    return ((opos - 1) / 8) + 1;
}

/*huffman_uncompress  解壓縮霍夫曼數據*/
int huffman_uncompress(const unsigned char *compressed, unsigned char **original)
{
    BiTree      *tree;
    BiTreeNode  *node;
    int          freqs[UCHAR_MAX + 1],
                 hsize,
                 size,
                 ipos,opos,
                 state,
                 c;
    unsigned char *orig,*temp;
    
    /*初始化*/
    *original = orig = NULL;
    
    /*從壓縮數據緩衝區中獲取頭文件信息*/
    hize = sizeof(int) + (UCHAR_MAX + 1);
    memcpy(&size,compressed,sizeof(int));
    
    for(c=0; c<=UCHAR_MAX; c++)
        freqs[c] = compressed[sizeof(int) + c];
    
    /*重建前面壓縮數據時的霍夫曼樹*/
    if(bulid_tree(freqs,&tree) != 0)
        return -1;
    
    /*解壓縮數據*/
    ipos = hsize * 8;
    opos = 0;
    node = bitree_root(tree);
    
    while(opos < size)
    {
        /*從壓縮數據中獲取位狀態*/
        state = bit_get(compressed,ipos);
        ipos++;
        
        if(state == 0)
        {
            /*向左移動*/
            if(bitree_is_eob(node) || bitree_is_eob(bitree_left(node)))
            {
                bitree_destroy(tree);
                free(tree);
                return -1;
            }
            else node = bitree_left(node);
        }
        else 
        {
            /*向右移動*/
            if(bitree_is_eob(node) || bitree_is_eob(bitree_right(node)))
            {
                bitree_destroy(tree);
                free(tree);
                return -1;
            }
            else node = bitree_right(node);
        }
        
        if(bitree_is_eob(bitree_left(node)) && bitree_is_eob(bitree_right(node)))
        {
            /*將葉子結點中的符號寫入原始數據緩衝區*/
            if(opos > 0)
            {
                if((temp = (unsigned char *)realloc(orig,opos+1)) == NULL)
                {
                    bitree_destroy(tree);
                    free(tree);
                    free(orig);
                    return -1;
                }
                orig = temp;
            }
            else 
            {
                if((orig = (unsigned char *)malloc(1)) == NULL)
                {
                    bitree_destroy(tree);
                    free(tree);
                    return -1;
                }
            }
            
            orig[opos] = ((HuffNode *)bitree_data(node))->symbol;
            opos++;
            
            /*返回到霍夫曼樹的頂部*/
            node = bitree_root(tree);
        }
    }
    bitree_destroy(tree);
    free(tree);
    
    /*把向原始數據緩衝區*/
    *original = orig;
    
    /*返回原始數據中的字節數*/
    return opos;
}
相關文章
相關標籤/搜索