實驗原理:
1.Huffman編碼
Huffman編碼是一種無失真的編碼方式,是可變字長編碼(VLC)的一種。
Huffman編碼基於信源的概率統計模型,它的基本思路是:
出現概率大的信源符號編長碼,出現概率小的信源符號編短碼,從而使平均碼長最小。
在程序實現時常使用一種叫樹的數據結構實現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;//左右子節點指針,編碼時左爲0右爲1
};
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;
(3)Huffman編碼的統計結果
typedef struct huffman_statistics_result
{
float freq[256];//每個信源符號出現頻率
unsigned long numbits[256];//碼長
unsigned char bits[256][100];//碼字
}huffman_stat;
Ps:上述三個結構體的創建均在下圖(Huffman編碼工程目錄)的huffman.c文件中實現。
實驗流程分析:
Huffman編碼流程框圖:
關鍵代碼分析:
主函數(huffcode.c)中主要步驟分析:
/*step1: 創建和初始化文件讀入、輸出、表格文件輸出指針及其他參數*/
main(int argc, char** argv)
{
char memory = 0;
char compress = 1;//編碼爲1解碼爲0
int opt;//將從命令行讀取的數據賦值給opt
const char *file_in = NULL, *file_out = NULL;//輸入輸出文件指針
//step1:add by yzhang for huffman statistics
const char *file_out_table = NULL;//表格文件
//end by yzhang
FILE *in = stdin;//定義指向輸入緩衝區的文件指針
FILE *out = stdout;//定義指向輸出緩衝區的文件指針
//step1:add by yzhang for huffman statistics
FILE * outTable = NULL;//輸出統計數據文件
//end by yzhang
/* step2:調用getopt函數獲取和解析命令行參數 */
while((opt = getopt(argc, argv, "i:o:cdhvmt:")) != -1) //演示如何跳出循環,及查找括號對;當讀取非空
{
switch(opt)
{
case 'i':
file_in = optarg;//optarg指向i額外的參數
break;
case 'o':
file_out = optarg;
break;
case 'c':
compress = 1;//設置爲編碼
break;
case 'd':
compress = 0;//設置爲解碼
break;
case 'h':
usage(stdout);//幫助顯示使用方法
return 0;
case 'v':
version(stdout);//輸出版本信息
return 0;
case 'm':
memory = 1;//改變內存中的值
break;
// by yzhang for huffman statistics
case 't'://輸出數據統計文件
file_out_table = optarg;
break;
//end by yzhang
default:
usage(stderr);//如果是其他情況,則將使用方法信息送到標準錯誤文件
return 1;
}
}
/* step3:打開輸入輸出文件 */
if(file_in)
{
in = fopen(file_in, "rb");
if(!in)
{
fprintf(stderr,
"Can't open input file '%s': %s\n",
file_in, strerror(errno));
return 1;
}
}
if(file_out)
{
out = fopen(file_out, "wb");
if(!out)
{
fprintf(stderr,
"Can't open output file '%s': %s\n",
file_out, strerror(errno));
return 1;
}
}
//by yzhang for huffman statistics
if(file_out_table)
{
outTable = fopen(file_out_table, "w");/*以寫的方式打開一個文件,並賦值給文件指針outTable*/
if(!outTable)
{
fprintf(stderr,
"Can't open output file '%s': %s\n",
file_out_table, strerror(errno));
return 1;
}
}
//end by yzhang
/*step4:關鍵部分!Huffman編碼部分*/
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);//否則只進行解碼
/*step5:收尾工作:關閉文件*/
if(in)//如果指向輸入文件的這塊內存還存在,關閉
fclose(in);
if(out)
fclose(out);//關閉
if(outTable)
fclose(outTable);//關閉
return 0;
}
子函數文件(huffman.c)中的編碼步驟分析:
第一次掃描,統計文件中各個字符出現頻率:(8bit,共256個符號)
1.創建一個256個元素的指針數組,用來保存256個信源符號的頻率,其下標對應相應字符的ASCII碼值。
2.數組中的非空元素爲當前待編碼文件中實際出現的信源符號。
/*保存頻率的指針數組*/
typedef huffman_node* SymbolFrequencies[MAX_SYMBOLS];//MAX_SYMBOLS=256
SymbolFrequencies sf;
/*獲得各符號頻數*/
static unsigned int get_symbol_frequencies(SymbolFrequencies *pSF, FILE *in)
{
int c;
unsigned int total_count = 0;//用於計算符號出現次數
init_frequencies(pSF);//初始化這些符號的頻率全部爲0
/*第一次掃描*/
while((c = fgetc(in)) != EOF)//從文件中讀取一字節字符,並賦值給c
{
unsigned char uc = c;//重置爲字符
/*新建了一個節點.。uc又指字符,又指頻率數組的第uc個值,這樣可以滿足排序問題; 如果uc的結構體不存在,則新建*/
if(!(*pSF)[uc])
(*pSF)[uc] = new_leaf_node(uc);
++(*pSF)[uc]->count;//讀了一個uc,個數加1
++total_count;//總個數加1
}
return total_count;
}
/*獲得各符號頻率:統計頻率,並賦值給統計數據結構體的頻率變量*/
int huffST_getSymFrequencies(SymbolFrequencies *SF, huffman_stat *st,int total_count)
{
int i,count =0;
for(i = 0; i < MAX_SYMBOLS; ++i)
{
if((*SF)[i])//如果i符號存在
{
st->freq[i]=(float)(*SF)[i]->count/total_count;//這個符號的頻率設置爲符號個數除以總頻率
count+=(*SF)[i]->count;
}
else
{
st->freq[i]= 0;//不存在則頻率爲0
}
}
if(count==total_count)
return 1;
else
return 0;
}
建立Huffman樹,並計算符號對應的Huffman碼字
1.按頻率從小到大進行排序並建立Huffman樹
static SymbolEncoder*
calculate_huffman_codes(SymbolFrequencies * pSF)//返回爲編碼結構體指針
{
unsigned int i = 0;
unsigned int n = 0;
huffman_node *m1 = NULL, *m2 = NULL;//定義兩個節點指針,置爲空
SymbolEncoder *pSE = NULL;//定義一個編碼指針爲空
#if 1
printf("BEFORE SORT\n");
print_freqs(pSF); //演示堆棧的使用,打印符號和出現次數
#endif
/*1對256個節點進行排序*/
qsort((*pSF), MAX_SYMBOLS, sizeof((*pSF)[0]), SFComp); //qsort爲排序函數,按頻率從小到大排。排序順序由SFcomp函數給出,該函數在後面給出其具體實現
#if 1
printf("AFTER SORT\n");
print_freqs(pSF);//再次顯示符號和次數(排序後)
#endif
for(n = 0; n < MAX_SYMBOLS && (*pSF)[n]; ++n)//節點存在且小於256時,即獲得頻率爲非零節點的個數
/*2對排好序的節點計算碼字*/
for(i = 0; i < n - 1; ++i)
{
//處理頻率最小的兩個節點
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);//再次進行排序
}
/* 3由建立的huffman樹對每個符號生成碼字 */
pSE = (SymbolEncoder*)malloc(sizeof(SymbolEncoder));
memset(pSE, 0, sizeof(SymbolEncoder));
build_symbol_encoder((*pSF)[0], pSE);
return pSE;
}
/*遍歷整個樹,對存在的碼字計算碼字*/
static void
build_symbol_encoder(huffman_node *subtree, SymbolEncoder *pSF)//對符號進行編碼,前一個是指樹,後一個是指編碼結構體指針
{
if(subtree == NULL)//樹爲空,不編碼
return;
if(subtree->isLeaf)//如果爲樹葉節點,則進行編碼
(*pSF)[subtree->symbol] = new_code(subtree);//編碼函數
else//不是樹葉節點的話
{
build_symbol_encoder(subtree->zero, pSF);//先對其左子節點編碼
build_symbol_encoder(subtree->one, pSF);
}
}
ps:該二叉樹的遍歷方式爲先序遍歷,先序遍歷即是:首先訪問根結點然後遍歷左子樹,最後遍歷右子樹。其遍歷順序如下圖:/*new_code函數*/
static huffman_code*
new_code(const huffman_node* leaf)//爲該節點編碼
{
/* Build the huffman code by walking up to
* the root node and then reversing the bits,
* since the Huffman code is calculated by
* walking down the tree. */
unsigned long numbits = 0;//碼長初始化爲0
unsigned char* bits = NULL;//指向碼字的指針
huffman_code *p;//
while(leaf && leaf->parent)//當葉子節點存在且其父節點存在時:父節點存在說明該碼還沒有編完,繼續編
{
huffman_node *parent = leaf->parent;//父指針指向父節點
unsigned char cur_bit = (unsigned char)(numbits % 8);//因爲一字節8比特,用cur_bit判斷是否湊足一字節
unsigned long cur_byte = numbits / 8;
if(cur_bit == 0)//當湊足一字節
{
size_t newSize = cur_byte + 1;//再多分配一字節
bits = (char*)realloc(bits, newSize);/*注意realloc函數與malloc不同,它在保持原有數據不變的情況下重新分配新的空間,原有數據保存在新 空間的的前面部分(空間的地址可能有變化)*/
bits[newSize - 1] = 0;//且將多分配的字節初始化爲0
}
if(leaf == parent->one)//如果該節點是其父節點的右子節點,則,左子節點不處理,因爲初始化爲0,右子節點賦爲1
bits[cur_byte] |= 1 << cur_bit;//左移到當前比特的當前位,繼續編碼
++numbits;//編了一位碼,碼長+1
leaf = parent;//將其父節點賦給當前節點,再循環
}
if(bits)//編完碼,且碼字存在,按照Huffman編碼規則,碼字需要翻轉
reverse_bits(bits, numbits);
p = (huffman_code*)malloc(sizeof(huffman_code));
p->numbits = numbits;
p->bits = bits;
return p;
}
/*reverse_bits函數的具體實現*/
static void reverse_bits(unsigned char* bits, unsigned long numbits)//碼字翻轉
{
unsigned long numbytes = numbytes_from_numbits(numbits);//獲得該碼字所佔字節數
unsigned char *tmp =(unsigned char*)alloca(numbytes);//alloca是內存分配函數,在棧上申請空間,用完立即釋放,用於存放翻轉後的碼字
unsigned long curbit;//對碼字進行未操作
long curbyte = 0;//對碼字進行字節操作
memset(tmp, 0, numbytes);//將tmp中的前numbytes個字節全部換成0
for(curbit = 0; curbit < numbits; ++curbit)//當前的比特
{
unsigned int bitpos = curbit % 8;//定位當前位在一個字節的第幾位
if(curbit > 0 && curbit % 8 == 0)//當超過一字節,將當前字節定位到下一字節
++curbyte;
tmp[curbyte] |= (get_bit(bits, numbits - curbit - 1) << bitpos);/*依次獲取相應bit位並將其移到逆序後對應位置,再與前一bit位逆序後所得字節或運算,得到逆序後碼字:如curbit爲0,則對應與最高位交換,get_bit獲取最高位後返回0000000i,i爲最高位的碼,再與00000000或之後返回給tmp.*/
}
memcpy(bits, tmp, numbytes);//從tmp中拷貝numbytes個字節到bits中
}
/*numbytes_from_numbits函數的實現*/
static unsigned long numbytes_from_numbits(unsigned long numbits)//比特轉字節函數
{
return numbits / 8 + (numbits % 8 ? 1 : 0);//一字節八比特,返回爲字節,上取整的方法
}
/*SFComp函數的具體實現*/
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;
/* Sort all NULLs to the end. */
if(hn1 == NULL && hn2 == NULL)//兩節點都爲空,返回0
return 0;//前=後
if(hn1 == NULL)
return 1;//hn1放在hn2後
if(hn2 == NULL)
return -1;//hn1放在hn2前
if(hn1->count > hn2->count)
return 1;//hn1放在hn2後
else if(hn1->count < hn2->count)
return -1;//hn1放在hn2前
return 0;
}
/*get_bit函數的實現*/
static unsigned char get_bit(unsigned char* bits, unsigned long i)//寫比特
{
return (bits[i / 8] >> i % 8) & 1;//獲取第i位,如獲取碼字101010001的第7位“0”,則返回0000 0000和1的與
}
/*new_nonleaf_node函數的具體實現*/
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;//沒有父節點 ----ps:都是通過節點連接其子節點來說明節點間的相互關係
return p;
}
將每個符號的碼長和碼字賦給編碼結果統計數據結構體:
int huffST_getcodeword(SymbolEncoder *se, huffman_stat *st)
{
unsigned long i,j;
for(i = 0; i < MAX_SYMBOLS; ++i)
{
huffman_code *p = (*se)[i];
if(p)//編碼結構體存在時
{
unsigned int numbytes;
st->numbits[i] = p->numbits;//將編完碼的碼長賦給統計數據結構體
numbytes = numbytes_from_numbits(p->numbits);
for (j=0;j<numbytes;j++)
st->bits[i][j] = p->bits[j];//將編完碼的碼字賦給統計數據結構體
}
else
st->numbits[i] =0;//不存在則該符號出現頻率爲0,沒有進行編碼
}
return 0;
}
將Huffman碼錶寫入文件
for(i = 0; i < MAX_SYMBOLS; ++i)
{
huffman_code *p = (*se)[i];
if(p)
{
unsigned int numbytes;
fputc((unsigned char)i, out);//將信源符號寫進輸出文件
fputc(p->numbits, out);//將信源符號對應的碼長寫入文件
numbytes = numbytes_from_numbits(p->numbits);//將信源符號編成的碼字寫入文件
if(fwrite(p->bits, 1, numbytes, out) != numbytes)
return 1;
}
}
將統計數據寫入輸出統計文件
void output_huffman_statistics(huffman_stat *st,FILE *out_Table)
{
int i,j;
unsigned char c;
fprintf(out_Table,"symbol\t freq\t codelength\t code\n");//輸出文件表頭
for(i = 0; i < MAX_SYMBOLS; ++i)
{
fprintf(out_Table,"%d\t ",i);//輸出信源符號
fprintf(out_Table,"%f\t ",st->freq[i]);//輸出信源符號的頻率
fprintf(out_Table,"%d\t ",st->numbits[i]);//輸出信源符號編碼後的碼長
if(st->numbits[i])//如果碼長大於1,即進行了編碼
{
for(j = 0; j < st->numbits[i]; ++j)
{
c =get_bit(st->bits[i], j);
fprintf(out_Table,"%d",c);//輸出信源符號編碼後的碼字
}
}
fprintf(out_Table,"\n");
}
}
第二次掃描文件,對文件查表進行huffman編碼,並寫入輸出文件
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)//遍歷文件
{
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)//當湊足一個字節,則將這一字節寫入文件,將當前bit和byte都置零,重新寫入一個字節
{
fputc(curbyte, out);
curbyte = 0;
curbit = 0;
}
}
}
//若多出不夠湊出一字節,則作爲一字節輸出
if(curbit > 0)
fputc(curbyte, out);
return 0;
}
實驗結果分析:
文件類型 |
平均碼長 |
信源熵 |
原文件大小(kb) |
壓縮後文件大小(kb) |
壓縮比 |
ppt |
6.337851 |
6.309532 |
182 |
146 |
1.247 |
|
7.630489 |
7.587450 |
300 |
287 |
1.045 |
png |
7.999920 |
7.997800 |
328 |
329 |
0.996 |
bmp |
7.835411 |
7.799278 |
554 |
544 |
1.018 |
docx |
7.999294 |
7.995741 |
264 |
265 |
0.996 |
jpg |
7.868257 |
7.842246 |
1062 |
1045 |
1.016 |
html |
5.015010 |
6.260903 |
31 |
25 |
1.240 |
txt |
5.896542 |
5.866860 |
1 |
1 |
1.000 |
rar |
7.999984 |
7.998295 |
4386 |
4386 |
1.000 |
gif |
7.939629 |
7.909703 |
59 |
58 |
1.017 |
分析上表和上圖:可知Huffman編碼對於不同格式的文件,其壓縮效率不同。具體來說,當概率較集中在某幾個符號時,用Huffman編碼可以得到較大壓縮比(如上面的ppt、pdf、html文件),而當符號分佈較均勻時,則得到較小壓縮比甚至壓縮比<1(如png和docx,這兩個文件概率分佈較均勻,碼錶所佔的內存大導致壓縮比甚至小於1,其他接近於1的文件也主要是因爲符號分佈較均勻而不能用較少的碼字來編碼導致壓縮效率不高)
實驗結論:
1、Huffman編碼算法是一種無失真編碼,雖然在編碼原理較爲容易,但在工程應用上應用不多,因爲該編碼方法適用於信源符號單一且集中分佈的文件,而實際工程應用上的文件卻是十分複雜的。這會導致其傳輸的碼錶佔據很大內存,從而降低壓縮效率。
2、該程序實現的過程用到了二叉樹這種數據結構。展開來講,對於一種數據結構的分析,應該從其邏輯結構和存儲結構兩方面分析。邏輯結構即其內部元素之間的相關性;存儲結構即其元素存儲位置的相關性。