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