一、實驗目的
掌握Huffman編解碼實現的數據結構和實現框架, 進一步熟練使用C編程語言, 並完成壓縮效率的分析。
二、實驗原理
1. Huffman編碼
(1)Huffman Coding (霍夫曼編碼)是一種無失真編碼的編碼方式, Huffman 編碼是可變字長編碼(VLC)的一種。
(2)Huffman 編碼基於信源的概率統計模型,它的基本思路是,出現概率大的信源符號編長碼,出現概率小的信源符號編短碼,從而使平均碼長最小。
(3)在程序實現中常使用一種叫做樹的數據結構實現 Huffman 編碼,由它編出的碼是即時碼。
2. Huffman編碼的方法
(1) 將文件以ASCII字符流的形式讀入, 統計每個符號的發生頻率;
(2)將所有文件中出現過的字符按照頻率從小到大的順序排列;
(3) 每一次選出最小的兩個值,作爲二叉樹的兩個葉子節點,將和作爲它們的根節點,這兩個葉子節點不再參與比較,新的根節點參與比較;
(4)重複3, 直到最後得到和爲1的根節點;
(5)將形成的二叉樹的左節點標0,右節點標1, 把從最上面的根節點到最下面的葉子節點途中遇到的0、 1序列串起來,得到了各個字符的編碼表示。
3. Huffman編碼的數據結構設計
在程序實現中使用一種叫做二叉樹的數據結構實現Huffman編碼。
(1)Huffman節點結構
typedef struct huffman_node_tag
{
unsigned char isLeaf; /* 是否爲葉結點 */
unsigned long count; /* 信源中出現頻數 */
struct huffman_node_tag *parent; /* 父節點指針 */
union /* 如果不是樹葉,則此項爲該結點左右孩子的指針;否則爲某個信源符號 */
{
struct
{
struct huffman_node_tag *zero, *one;
};
unsigned char symbol;
};
} huffman_node;
(2)Huffman碼字結點
typedef struct huffman_code_tag
{
/* 碼字的長度(單位:位) */
unsigned long numbits;
/* 碼字, 碼字的第 1 位存於 bits[0]的第 1 位,
碼字的第 2 位存於 bits[0]的第的第 2 位,
碼字的第 8 位存於 bits[0]的第的第 8 位,
碼字的第 9 位存於 bits[1]的第的第 1 位 */
unsigned char *bits;
} huffman_code;
4. 靜態鏈接庫的使用
本實驗由兩個項目組成,第一個項目爲 Huffman 編碼的具體實現,名爲 huff_code,創建項目時選擇的是靜態庫,生成一個 .lib 文件。第二個項目 huff_run 只需要包含這個庫即可調用其中的編碼函數。項目屬性需要配置庫目錄屬性,也就是第一個項目生成文件的路徑,和附加依賴性屬性,也就是庫的名稱,如圖所示。由於代碼中用到了字節序轉換的函數 htonl、ntohl,附加依賴項還需包含 ws2_32.lib。
三、實驗步驟及代碼分析
1.調試
首先調試Huffman的編碼程序, 對照編碼算法步驟對關鍵語句加上註釋,並說明進行何操作。
Huffman編碼流程
(1)讀取文件
使用庫函數中的getopt解析命令行參數,這個函數的前兩個參數爲main中的argc和argv,第三個參數爲單個字符組成的字符串,每個字符表示不同的選項,單個字符後接一個冒號,表示該選項後必須跟一個參數。
//--------huffcode.c--------
...
static void
usage(FILE* out)//命令行參數格式
{
fputs("Usage: huffcode [-i<input file>] [-o<output file>] [-d|-c]\n"
"-i - input file (default is standard input)\n"
"-o - output file (default is standard output)\n"
"-d - decompress\n"
"-c - compress (default)\n"
"-m - read file into memory, compress, then write to file (not default)\n",
// step1: by yzhang, for huffman statistics
"-t - output huffman statistics\n",
//step1:end by yzhang
out);
}
//————————————————————————————————————————————————————————
int main(int argc, char** argv)
{
char memory = 0; //memory表示是否對內存數據進行操作
char compress = 1; //compress爲1表示編碼,0表示解碼
const char *file_in = NULL, *file_out = NULL;
FILE *in = stdin, *out = stdout;
while((opt = getopt(argc, argv, "i:o:cdhvm")) != -1){ //讀取命令行參數的選項
switch(opt){
case 'i': file_in = optarg; break; // i 爲輸入文件
case 'o': file_out = optarg; break; // o 爲輸出文件
case 'c': compress = 1; break; // c 爲壓縮操作
case 'd': compress = 0; break; // d 爲解壓縮操作
case 'h': usage(stdout); system("pause"); return 0; // h 爲輸出參數用法說明
case 'v': version(stdout); system("pause"); return 0; // v 爲輸出版本號信息
case 'm': memory = 1; break; // m 爲對內存數據進行編碼
default: usage(stderr); system("pause"); return 1;
}
}
if(file_in){ //讀取輸入輸出文件等
in = fopen(file_in, "rb"); if(!in)...
}
...
if(memory)
{//對內存數據進行編碼或解碼操作
return compress ?
memory_encode_file(in, out) : memory_decode_file(in, out);
}
if(compress) //change by yzhang
huffman_encode_file(in, out,outTable);//step1:changed by yzhang from huffman_encode_file(in, out) to huffman_encode_file(in, out,outTable)
else
huffman_decode_file(in, out);
}
分析對文件的編碼流程
//--------huffman.c--------
...
//最大符號數目,由於一個字節一個字節進行編碼,因此爲256
#define MAX_SYMBOLS 256
typedef huffman_node* SymbolFrequencies[MAX_SYMBOLS]; //信源符號數組,數據類型爲之前定義過的樹節點類型
typedef huffman_code* SymbolEncoder[MAX_SYMBOLS]; //編碼後的碼字數組,數據類型爲之前定義過的碼字類型
//————————————————————————————————————————————————————————
int huffman_encode_file(FILE *in, FILE *out) //對文件進行Huffman編碼的函數
{
SymbolFrequencies sf;
SymbolEncoder *se;
huffman_node *root = NULL;
unsigned int symbol_count = get_symbol_frequencies(&sf, in); //第一遍掃描文件,得到文件中各字節的出現頻率
se = calculate_huffman_codes(&sf); //再根據得到的符號頻率建立一棵Huffman樹,還有Huffman碼錶
root = sf[0]; //編完碼錶後,Huffman樹的根節點爲 sf[0],具體原因在後面的分析
rewind(in); //回到文件開頭,準備第二遍掃描文件
int rc = write_code_table(out, se, symbol_count); //先在輸出文件中寫入碼錶
if(rc == 0) rc = do_file_encode(in, out, se); //寫完碼錶後對文件字節按照碼錶進行編碼
free_huffman_tree(root); free_encoder(se);
return rc;
}
(2)統計文件中各字節出現的頻率
static unsigned int get_symbol_frequencies(SymbolFrequencies *pSF, FILE *in) //統計中各字節出現頻率的函數
{
int c; unsigned int total_count = 0;
memset(*pSF, 0, sizeof(SymbolFrequencies)); //首先把所有符號的頻率設爲0
while((c = fgetc(in)) != EOF) //然後讀每一個字節,把一個字節看成一個信源符號,直到文件結束
{
unsigned char uc = c;
if(!(*pSF)[uc]) //如果還沒有在數組裏建立當前符號的信息
(*pSF)[uc] = new_leaf_node(uc); //那麼把這個符號設爲一個葉節點
++(*pSF)[uc]->count; //如果已經是一個葉節點了或者葉節點剛剛建立,符號數目都+1
++total_count; //總字節數+1
}
return total_count;
}
//————————————————————————————————————————————————————————
static huffman_node* new_leaf_node(unsigned char symbol) //建立一個葉節點的函數
{
huffman_node *p = (huffman_node*)malloc(sizeof(huffman_node)); //分配一個葉節點的存儲空間
p->isLeaf = 1; //表明當前節點爲葉節點
p->symbol = symbol; //節點存儲的信源符號
p->count = 0; //信源符號數目設爲0
p->parent = 0; //父節點爲空
return p;
}
(3)建立Huffman樹
static SymbolEncoder* calculate_huffman_codes(SymbolFrequencies * pSF) //創建一棵Huffman樹的函數
{
unsigned int i = 0, n = 0;
huffman_node *m1 = NULL, *m2 = NULL;
SymbolEncoder *pSE = NULL;
qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp); //先使用自定義的順序對出現次數進行排序,使得下標爲0的元素的count最小
for(n = 0; n < MAX_SYMBOLS && (*pSF)[n]; ++n); //統計下信源符號的真實種類數,因爲一個文件中不一定256種字節都會出現
for(i = 0; i < n - 1; ++i)
{
//把出現次數最少的兩個信源符號節點設爲 m1,m2
m1 = (*pSF)[0];
m2 = (*pSF)[1];
//然後合併這兩個符號,把合併後的新節點設爲這兩個節點的父節點
(*pSF)[0] = m1->parent = m2->parent = new_nonleaf_node(m1->count + m2->count, m1, m2);
(*pSF)[1] = NULL; //合併之後,第二個節點爲空
qsort((*pSF), n, sizeof((*pSF)[0]), SFComp); //然後再排一遍序
}
//樹構造完成後,爲碼字數組分配內存空間並初始化
pSE = (SymbolEncoder*)malloc(sizeof(SymbolEncoder));
memset(pSE, 0, sizeof(SymbolEncoder));
build_symbol_encoder((*pSF)[0], pSE); //從樹根開始,爲每個符號構建碼字
return pSE;
}
qsort爲標準庫中自帶的快速排序函數,參數爲 <待排序數組> <數組元素個數> <元素的大小> <自定義比較數組元素的函數>。
臨時變量 m1,m2 不斷地設爲信源符號數組中出現次數最少的兩個元素,數組第一個元素一直是出現次數最小的兩個符號的合併,這樣循環結束後,pSF 數組中所有元素除第一個 pSF[0] 以外都空,而這些新建立的節點分佈在內存中各個角落,通過節點屬性中的左右兩個子節點指針指向各自的子節點,構建出一棵二叉樹結構,把這些節點連在一起,pSF[0] 就是這棵樹的根節點。因此如果要遍歷這棵樹,只要 pSF[0] 就夠了。
static int SFComp(const void *p1, const void *p2) //自定義的排序順序函數,把節點數組由小到大排序
{
//把兩個排序元素設爲自定義的樹節點類型
const huffman_node *hn1 = *(const huffman_node**)p1;
const huffman_node *hn2 = *(const huffman_node**)p2;
if(hn1 == NULL && hn2 == NULL) return 0; //如果兩個節點都空,返回相等
if(hn1 == NULL) return 1; //如果第一個節點爲空,則第二個節點大
if(hn2 == NULL) return -1; //反之第二個節點小
//如果都不空,則比較兩個節點中的計數屬性值,然後同上返回比較結果
if(hn1->count > hn2->count) return 1;
else if(hn1->count < hn2->count) return -1;
return 0;
}
static huffman_node* new_nonleaf_node(unsigned long count, huffman_node *zero, huffman_node *one) //建立一個內部節點的函數
{
huffman_node *p = (huffman_node*)malloc(sizeof(huffman_node)); //分配一個節點的存儲空間
p->isLeaf = 0; //內部節點,不是葉節點
//根據參數,設置這個節點的符號數和左右子節點
p->count = count; p->zero = zero; p->one = one;
p->parent = 0; //父節點設爲空
return p;
}
(4)生成碼字
實驗中碼字數組中的一個元素爲 unsigned char 類型,一個元素保存了 8 位的碼字,一個碼字中的一位(0或1)在存儲時確實只佔用了 1 bit。 假如有一個葉節點在樹中的位置按照編碼規則得到從根到葉的編碼爲 111100011。這個碼字一共有 9 位,那麼需要佔用 unsigned char 類型數組中的兩個元素的位置。
生成碼字時,首先遍歷二叉樹找到葉節點,然後逐層向上回到根部,先從葉到根編碼。設這個數組爲 bits,則 bits[0] 保存了碼字的 1 到 8 位,bits[1] 保存了碼字的第 9 位,而且一個字節的低位保存碼字的低位。即bits[0]:11100011,bits[1]:00000001
這是從葉到根的編碼(110001111),真正的碼字要從根到葉讀(111100011),因此在 new_code 函數最後使用了一個對碼字進行倒序的函數 reverse_bits,執行倒序之後 bits 數組變爲bits[0]:10001111,bits[1]:00000001
讀碼字的時候,先從 bits[0] 的低位向高位讀,讀完 bits[0] 讀 bits[1],也是低位往高位讀,這樣就讀出了正確的碼字(111100011)。
具體代碼說明
static void build_symbol_encoder(huffman_node *subtree, SymbolEncoder *pSE) //遍歷碼樹的函數
{
if(subtree == NULL) return; //如果是空樹,返回
if(subtree->isLeaf) //如果是葉節點,則對葉節點進行編碼
(*pSE)[subtree->symbol] = new_code(subtree);
else //如果都不是,那麼先訪問左節點,到了葉節點之後再訪問右節點
{
build_symbol_encoder(subtree->zero, pSE);
build_symbol_encoder(subtree->one, pSE);
}
}
遍歷碼樹時,先一直向下訪問左子節點到葉節點,再回到根,再訪問右子節點,pSE 的下標就是待編碼的信源符號。
static huffman_code* new_code(const huffman_node* leaf) //生成碼字的函數
{
//碼字的位數 numbits,也就是樹從下到上的第幾層,還有保存碼字的數組 bits
unsigned long numbits = 0;
unsigned char* bits = NULL;
while(leaf && leaf->parent) //如果還沒到根節點
{
//那麼得到當前節點的父節點,由碼字位數得到碼字在字節中的位置和碼字的字節數
huffman_node *parent = leaf->parent;
unsigned char cur_bit = (unsigned char)(numbits % 8);
unsigned long cur_byte = numbits / 8;
if(cur_bit == 0) //如果比特位數爲0,說明到了下一個字節,新建一個字節保存後面的碼字
{
size_t newSize = cur_byte + 1; //新的字節數爲當前字節數+1,size_t 即爲 unsigned int 類型
bits = (char*)realloc(bits, newSize); //數組按照新的字節數重新分配空間
bits[newSize - 1] = 0; //並把新增加的字節設爲0
}
if(leaf == parent->one) //如果是右子節點,按照Huffman樹左0右1的原則,應當把當前字節中當前位置1
//先把1右移到當前位(cur_bit)位置,再把當前字節(bits[cur_byte])與移位後的1做或操作
bits[cur_byte] |= 1 << cur_bit;
++numbits; //然後碼字的位數加1
leaf = parent; //下一位碼字在父節點所在的那一層
}
//回到根之後編碼完畢,對碼字進行倒序
if(bits)
reverse_bits(bits, numbits);
//倒序後,輸出碼字數組
huffman_code *p = (huffman_code*)malloc(sizeof(huffman_code));
p->numbits = numbits; p->bits = bits;
return p;
}
static void reverse_bits(unsigned char* bits, unsigned long numbits) //對碼字進行倒序的函數
{
//先判斷碼字最多需要多少個字節存儲
unsigned long numbytes = numbytes_from_numbits(numbits);
//分配字節數所需的存儲空間,還有當前字節數和比特位數
unsigned char *tmp = (unsigned char*)alloca(numbytes);
unsigned long curbit;
long curbyte = 0;
memset(tmp, 0, numbytes);
for(curbit = 0; curbit < numbits; ++curbit)
{
//判斷當前位是字節裏的哪一位,到了下一個字節,字節數+1
unsigned int bitpos = curbit % 8;
if(curbit > 0 && curbit % 8 == 0) ++curbyte;
//從後往前取碼字中的每一位,再移位到所在字節的正確位置
tmp[curbyte] |= (get_bit(bits, numbits - curbit - 1) << bitpos);
}
memcpy(bits, tmp, numbytes);
}
//由比特位長度得到字節數。除以8取整,如果還有餘數說明要再加一個字節
static unsigned long numbytes_from_numbits(unsigned long numbits)
{ return numbits / 8 + (numbits % 8 ? 1 : 0); }
/* 取出碼字 bits 中的第 i 位
第 i 位在第 i/8 字節的第 i%8 位,把這一位移到字節最低位處,和 0000 0001 做與操作,從而只留下這一位,返回*/
static unsigned char get_bit(unsigned char* bits, unsigned long i)
{ return (bits[i / 8] >> i % 8) & 1; }
碼字由數組 bits 存儲,數組的一個元素有 8 位(一個字節),因此定義了 cur_bit 和 cur_byte 兩個變量,用於標識當前的一位碼字在 bits 中的字節位置和字節裏的位位置。默認情況下碼字數組 bits 全爲 0,需要置 1 的情況就和 1 進行或操作把某些比特位置 1。
(5)寫入碼錶,對文件進行編碼
static int write_code_table(FILE* out, SymbolEncoder *se, unsigned int symbol_count) //寫入碼錶的函數
{
unsigned long i, count = 0;
//還是要先統計下真實的碼字種類,不一定256種都有
for(i = 0; i < MAX_SYMBOLS; ++i)
if((*se)[i]) ++count;
//把字節種類數和字節總數變成大端保存的形式,寫入文件中
i = htonl(count);
if(fwrite(&i, sizeof(i), 1, out) != 1) return 1;
symbol_count = htonl(symbol_count);
if(fwrite(&symbol_count, sizeof(symbol_count), 1, out) != 1) return 1;
//然後開始寫入碼錶
for(i = 0; i < MAX_SYMBOLS; ++i)
{
huffman_code *p = (*se)[i];
if(p)
{
fputc((unsigned char)i, out); //碼錶中有三種數據,先寫入字節符號
fputc(p->numbits, out); //再寫入碼長
//最後得到字節數,寫入碼字
unsigned int numbytes = numbytes_from_numbits(p->numbits);
if(fwrite(p->bits, 1, numbytes, out) != numbytes) return 1;
}
}
return 0;
}
在文件中寫入字節種類數和字節數時,系統按照小端方式寫入,比如 256(100H) 寫入後變爲 00 01 00 00。爲了在文件中能從左到右直接讀出真實數據,這裏先把它們變成了大端方式保存再寫入文件,在解碼時還要做一次轉換。
static int do_file_encode(FILE* in, FILE* out, SymbolEncoder *se) //對文件符號進行編碼的函數
{
unsigned char curbyte = 0;
unsigned char curbit = 0;
int c;
while((c = fgetc(in)) != EOF)
{
//逐字節讀取待編碼的文件,要找到當前符號(字節)uc對應的碼字code,只需要把uc作爲碼字數組se的下標即可
unsigned char uc = (unsigned char)c;
huffman_code *code = (*se)[uc];
unsigned long i;
for(i = 0; i < code->numbits; ++i)
{
//把碼字中的一個比特位放到編碼字節的相應位置
curbyte |= get_bit(code->bits, i) << curbit;
//每次寫入一個字節
if(++curbit == 8){
fputc(curbyte, out);
curbyte = 0; curbit = 0;
}
}
}
//處理一下最後一個字節的編碼不足一字節的情況
if(curbit > 0) fputc(curbyte, out);
return 0;
}
對文件進行編碼時,一個字節一個字節地讀文件,把字節作爲信源符號,查找碼字數組得到碼字。寫文件也是一個字節一個字節寫,有一些碼字可能不足一個字節或超過一個字節(8位碼字),那麼就等到下一個符號的編碼,直到湊足一個字節的長度再寫入文件。因此編碼後的數據中一個字節可能包含有原來文件的多個符號(字節),從而達到了數據壓縮的目的。
Huffman解碼流程
讀取壓縮後文件首部Huffman碼錶:
/*
* read_code_table builds a Huffman tree from the code
* in the in file. This function returns NULL on error.
* The returned value should be freed with free_huffman_tree.
*/
static huffman_node*//讀碼錶
read_code_table(FILE* in, unsigned int *pDataBytes)
{
huffman_node *root = new_nonleaf_node(0, NULL, NULL);
unsigned int count;
/* Read the number of entries.
(it is stored in network byte order). */
//讀文件和寫文件一樣,按照小端方式讀
if (fread(&count, sizeof(count), 1, in) != 1)
{
free_huffman_tree(root);
return NULL;
}
//所以按照大端方式存放的數據count,再轉換一次就得到了正確結果
count = ntohl(count);
//ntohl()是將一個無符號長整形數從網絡字節順序轉換爲主機字節順序
//從big-endian轉化爲little-endian
/* Read the number of data bytes this encoding represents. */
if (fread(pDataBytes, sizeof(*pDataBytes), 1, in) != 1)
{
free_huffman_tree(root);
return NULL;
}
*pDataBytes = ntohl(*pDataBytes);
// 原文件的總字節數pDataBytes也由littleendian轉化爲bigendian
/* Read the entries. */
//讀完這些後,文件指針指向了碼錶開頭,依次讀取碼錶中的每一項,每一項由符號,碼長,碼字三種數據組成
while (count-- > 0)
{
int c;
unsigned int curbit;
unsigned char symbol;
unsigned char numbits;
unsigned char numbytes;
unsigned char *bytes;
huffman_node *p = root;
if ((c = fgetc(in)) == EOF)//一次讀一個字節,第一個字節是信源符號symbol
{
free_huffman_tree(root);
return NULL;
}
symbol = (unsigned char)c;
if ((c = fgetc(in)) == EOF)//第二個字節是碼長數據numbits
{
free_huffman_tree(root);
return NULL;
}
numbits = (unsigned char)c;
//計算出這樣一個碼長需要多少個字節(numbytes個)保存,開闢與字節數對應的空間
numbytes = (unsigned char)numbytes_from_numbits(numbits);
bytes = (unsigned char*)malloc(numbytes);
if (fread(bytes, 1, numbytes, in) != numbytes)//然後讀取numbytes個字節得到碼字bytes
{
free(bytes);
free_huffman_tree(root);
return NULL;
}
/*
* Add the entry to the Huffman tree. The value
* of the current bit is used switch between
* zero and one child nodes in the tree. New nodes
* are added as needed in the tree.
*/
for (curbit = 0; curbit < numbits; ++curbit)//讀完碼錶碼符號三種數據後,開始由碼字建立Huffman樹
{
if (get_bit(bytes, curbit))//如果碼字中的當前位爲1
{
if (p->one == NULL)//那麼沒有右子節點則新建一個右子節點
{//如果是最後一位,就建立樹葉節點,否則就當做一個父節點,後續建立他的子節點
p->one = curbit == (unsigned char)(numbits - 1)
? new_leaf_node (symbol)
: new_nonleaf_node (0, NULL, NULL);
p->one->parent = p;//設置好新建節點的父節點
}
p = p->one;//不管右子節點是不是新建的,都要把這個節點當成父節點,以便建立它後續的子節點
}
else//如果碼字中的當前位爲0
{
if (p->zero == NULL)//那麼應該建立一個左子節點(如果沒有的話)
{
p->zero = curbit == (unsigned char)(numbits - 1)
//同理,選擇節點類型並確定節點之間的關係
? new_leaf_node(symbol)
: new_nonleaf_node(0, NULL, NULL);
p->zero->parent = p;
}
p = p->zero;
}
}
free(bytes);//和編碼一樣,只要有最上面的根節點就能遍歷整棵樹
}
return root;
}
對文件逐字符進行Huffman解碼:
int huffman_decode_file(FILE *in, FILE *out)//Huffman解碼
{
huffman_node *root, *p;
int c;
unsigned int data_count;
/* Read the Huffman code table. */
root = read_code_table(in, &data_count);//打開文件後首先讀入碼錶,建立Huffman樹,並且獲取原文件的字節數
if (!root)
return 1;
/* Decode the file. */
p = root;
while (data_count > 0 && (c = fgetc(in)) != EOF)//準備好碼樹之後,一次讀一個字節進行解碼
{
unsigned char byte = (unsigned char)c;
unsigned char mask = 1;
//mask負責提取字節中的每一位,提取完之後向左移動一位來提取下一位。因此移動8位之後變成0,循環退出,讀下一個字節
while (data_count > 0 && mask)
{
//如果當前字節爲0,就轉到左子樹,否則轉到右子樹
p = byte & mask ? p->one : p->zero;
mask <<= 1;//準備讀下一個字節
if (p->isLeaf)//如果走到了葉節點
{
fputc(p->symbol, out);//就輸出葉節點中存儲的符號
p = root;//然後轉到根節點,再從頭讀下一個碼字
--data_count;//而且剩下沒解碼的符號數-1
}
}
}
free_huffman_tree(root);
return 0;
}
2.輸出
在程序中添加代碼,輸出編碼結果文件(以列表方式顯示字符、字符發生概率、字符對應編碼碼字長度、字符對應編碼碼字)。在命令行參數中需要添多加一個參數‘t’來指定輸出的文本文件,添加命令行參數方法在讀入編碼文件部分分析過。
定義信源符號的統計數據類型:
typedef struct huffman_stat_tag //信源符號的統計數據類型
{
unsigned long numbits; //碼字長度
unsigned char *bits; //碼字
double freq; //信源符號出現的頻率
}huffman_stat;
typedef huffman_stat* SymbolStatices[MAX_SYMBOLS];
void getStatFreq(SymbolStatices* stat, SymbolFrequencies* sf, unsigned int symbol_count) //由信源符號數組得到出現頻率的函數
{
unsigned int i;
for (i = 0; i < MAX_SYMBOLS; i++)
(*stat)[i] = (huffman_stat*)malloc(sizeof(huffman_stat)); //把統計數組信源符號的每個位置分配一塊空間
for (i = 0; i < MAX_SYMBOLS; i++)
{
if ((*sf)[i]) //如果符號數組當前元素不爲空
{
unsigned int j = (*sf)[i]->symbol; //那麼得到當前元素保存的信源符號
(*stat)[j]->freq = (double)(*sf)[i]->count / symbol_count; //把符號作爲下標,對 freq 賦值
}
}
for (i = 0; i < MAX_SYMBOLS; i++)
{
if (!(*sf)[i]) //找到那些信源符號爲空的數組
(*stat)[i]->freq = 0; //信源符號頻率爲0
}
}
void getStatCode(SymbolStatices* stat, SymbolEncoder *se) //由碼字數組得到統計數組中其它兩項信息的函數
{
unsigned int i;
for (i = 0; i < MAX_SYMBOLS; i++)
{
//之前已經分配過存儲空間了,如果當前符號存在,得到符號的碼長和碼字
if ((*se)[i])
{
(*stat)[i]->numbits = (*se)[i]->numbits;
(*stat)[i]->bits = (*se)[i]->bits;
}
}
}
void output_statistics(FILE* table, SymbolStatices stat) //將碼字列表寫入文件的函數
{
unsigned long i,j, count = 0;
for (i = 0; i < MAX_SYMBOLS; ++i)
if (stat[i]) ++count;
fprintf(table, "Symbol\t Frequency\t Length\t Code\n"); //表頭每一項的說明
for (i = 0; i < count; i++)
{
fprintf(table, "%d\t", i); //第一列爲信源符號
fprintf(table, "%f\t", stat[i]->freq); //第二列爲符號的出現頻率
//如果信源符號頻率爲零,碼字爲空指針,因此輸出一個 0 作爲碼字長度然後輸出下一個符號
if (stat[i]->freq == 0)
{
fprintf(table,"0\n");
continue;
}
fprintf(table, "%d\t", stat[i]->numbits); //第三列爲碼字長度
for (j = 0; j < numbytes_from_numbits(stat[i]->numbits); j++)
{
fprintf(table, "%x", stat[i]->bits[j]); //第四列爲用十六進制表示的碼字,可以與編碼後文件中的碼錶對應
}
fprintf(table, "\t");
for (j = 0; j < stat[i]->numbits; j++)
{
//還有用二進制方式表示的碼字,每次取出碼字的一位,輸出到文件中
unsigned char c = get_bit(stat[i]->bits, j);
fprintf(table, "%d", c);
}
fprintf(table, "\n");
}
}
三、實驗結果比較分析
選擇不同格式類型的文件,使用Huffman編碼器進行壓縮得到輸出的壓縮比特流文件。根據第2步得到的編碼結果文件,對各種不同格式的文件進行壓縮效率的分析。
編碼結果示例
與上述代碼對應,編碼結果分爲符號、符號出現頻率、碼長、碼字四部分。可以看出,頻率高的符號爲短碼,低的符號編長碼。
總共選擇了九種不同格式的文件對它們進行壓縮,如圖所示
實驗用文件
文件類型 | 平均碼長 | 信息熵(bit/sym) | 源文件大小(KB) | 壓縮後文件大小(KB) | 壓縮比 |
---|---|---|---|---|---|
docx | 7.495 | 7.468 | 18 | 9 | 2.000 |
avi | 4.997 | 4.956 | 2194 | 1372 | 1.599 |
jpg | 7.964 | 7.940 | 31 | 32 | 0.969 |
mp4 | 6.666 | 6.624 | 386 | 322 | 1.199 |
png | 8.000 | 7.996 | 461 | 461 | 1.000 |
7.921 | 7.907 | 473 | 469 | 1.009 | |
ppt | 5.581 | 5.548 | 185 | 131 | 1.412 |
rar | 7.999 | 7.992 | 39 | 39 | 1.000 |
m | 3.869 | 3.811 | 24 | 12 | 2.000 |
表1實驗結果
表2各文件的概率分佈圖
結合表 1 和表 2 可以看出Huffman編碼的平均碼長基本接近於信源熵,符號有 256 種,信源熵最大約爲 8 bit/sym。
Huffman編碼對於字節分佈不均勻的文件壓縮效果好,而對於每個字節出現概率接近等概的文件就不能按照大概率短碼,小概率長碼的原則進行編碼,最終使得文件基本沒有壓縮,特別是第 3,5,8 測試的 jpg,png,rar 文件,編碼結果顯示每個符號的碼長都是 8 位,再加上存儲的碼錶使得文件反而比壓縮前更大了。
四、注意事項
1.程序運行前的命令參數設置
2.多項目工程下設置啓動項目
有可能會遇到這樣的問題,在於沒把含有.exe的項目設爲啓動項