霍夫曼編碼是一種基於最小冗余編碼的壓縮算法。最小冗余編碼是指,如果知道一組數據中符號出現的頻率,就可以用一種特殊的方式來表示符號從而減少數據需要的存儲空間。一種方法是使用較少的位對出現頻率高的符號編碼,用較多的位對出現頻率低的符號編碼。我們要意識到,一個符號不一定必須是文本字符,它可以是任何大小的數據,但往往它只占一個字節。
熵和最小冗余
每個數據集都有一定的信息量,這就是所謂的熵。一組數據的熵是數據中每個符號熵的總和。符號z的熵S定義為:
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次,因此整個數據的熵為:
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; }