一、赫夫曼樹的定義
下面是一個例子
if (a < 60) {
printf("不及格");
} else if (a < 70){
printf("及格");
} else if (a < 90){
printf("良好");
} else {
printf("優秀");
}
這段代碼的樹結構如下
如圖所示,每個階段的學生所占比例不同,但我們的二叉樹并沒有對這方面進行優化,我們將樹結構改為如下,效率可能有明顯的改善
1.定義
我們將兩棵二叉樹簡化為葉子結點帶權的二叉樹(樹結點間的連線相關的數叫做權),這就是赫夫曼樹
下面是一些概念
結點的路徑長度
?從根節點到該結點的路徑上的連接數,如上圖中左邊樹中結點C的路徑長度為3(有3個連接)樹的路徑長度
?樹中每個葉子結點的路徑長度之和,如上圖中左邊樹的路徑長度為1+2+3+3=9結點帶權路徑長度
?結點的路徑長度與結點權值的乘積,如上圖中左邊樹中結點C的帶權路徑長度為3*70=210樹的帶權路徑長度
?WPL(Weighted Path Length)是樹中所有葉子結點的帶權路徑長度之和,如上圖中左邊樹中為1*5+2*15+3*70+3*10=275
WPL的值越小,說明構造出來的二叉樹性能越優。
2.赫夫曼樹的構造
如上面的所講,赫夫曼樹的構造算法如下:
① 根據給定的n個權值{W1,W2,...,Wn}構成n課二叉樹的集合F={T1,T2,...,Tn},其中每棵二叉樹Ti中只有一個帶權為Wi的結點,其左右子樹為空。
② 在F中選取兩棵根節點的權值最小的樹作為左右子樹構造一棵新的二叉樹,且將新的二叉樹的根節點的權值設置為其左右子樹上根節點的權值之和。
③ 在F中刪除這兩棵樹,同時將新得到的二叉樹加入F中。
④ 重復②和③步驟,直到F只含一棵樹為止,這棵樹便是赫夫曼樹。
二、赫夫曼編碼
赫夫曼編碼可以很有效的壓縮數據(通常可以節省20%~90%的空間,具體壓縮率依賴于數據的特性)
名詞解釋
- 定長編碼:像ASCII編碼,長度是一定的
- 變長編碼:單個編碼的長度不一致,可以根據整體出現頻率來調節
- 前綴碼:所謂的前綴碼,就是沒有任何碼字是其他碼字的前綴
赫夫曼編碼最早目的是為了解決當年遠距離通信(主要是電報)的數據傳送的最優化問題。
我們以網絡傳輸一段文字內容為“BADCADFEED”為例。如果用二進制的數字(0和1)來表示,
真正傳輸的數據是編碼后的“001 000 011 010 000 011 101 100 100 001”。
實際上,如果傳輸一篇很長的文章,這個二進制串就非常大,同時不同字母的出現頻率是不相同的。假設這六個字母的頻率為:
下圖左邊為赫夫曼樹的構造過程。右邊將權值左分支改為0,右分支改為1后的赫夫曼樹。
此時,我們對這六個字母用其從樹根到葉子所經過路徑的0或1來編碼,得到下表:
再次編碼為“1001 01 00 101 01 00 1000 11 11 00”。
對比結果節約了大約17%的數據空間。如果解碼呢,必須用到赫夫曼樹,即發送方和接收方必須約定好同樣的赫夫曼編碼規則。
以上面新編碼二進制串為例,從樹根結點出發,按二進制數表示的路徑到達葉子節點,解碼出字母。后面的二進制再從樹根結點出發,以此循環。1001走到葉子節點B,后面的01走到葉子節點A,以此類推。
三、赫夫曼編碼代碼實現
算法思路:
- 創建一個隊列queue,分別存儲字符的出現次數,這個隊列中每個結點的權值是按照從小到大的順序存儲的。
- 按照赫夫曼樹的構造方式,先取出兩個權值最小的結點a和b,將它們的權值相加,創建一個新結點,新節點的權值是兩個權值最小結點之和,left和right指向a和b,將新節點按照權值從小到大的順序插入隊列中,并在隊列中刪除a和b,循環第2步,最后隊列queue中只剩下一個結點時,這個結點就是赫夫曼樹的根節點,也就是說,這個結點指向一個赫夫曼樹
- 我們通過遍歷赫夫曼樹的結果,為左子樹加一個0,右子樹加一個1,一直到葉子結點,我們加一個'\0',這樣我們就得到了每個字符的編碼,并將結果存入一個表格中
- 解碼的過程,我們拿到編碼后,在赫夫曼樹上,為0則往左走一步,1往右走一步,這樣就找到了葉子,把它的字符寫出來
實現代碼有點長,我們分為兩個文件,分別為Huffman
文件和PriorityQueue
文件,代碼如下所示:
Huffman文件
Huffman.h
#ifndef Huffman_h
#define Huffman_h
#include <stdlib.h>
#include <stdio.h>
//赫夫曼樹結點
typedef struct HuffmanNode{
char symbol;//字符
struct HuffmanNode *left,*right;//左子樹,右子樹
}HuffmanNode;
//赫夫曼樹
typedef struct HuffmanTree{
HuffmanNode *root;//根節點
}HuffmanTree;
///編碼表結點
typedef struct HuffmanTableNode{
char symbol;//字符
char *code;//編碼
struct HuffmanTableNode *next;//指向
}HuffmanTableNode;
///編碼表,一個單鏈表結構
typedef struct HuffmanTable{
HuffmanTableNode *first;//首個結點
HuffmanTableNode *last;//最后一個結點
}HuffmanTable;
///創建赫夫曼樹
HuffmanTree *buildTree(char *inputString);
///創建編碼表
HuffmanTable *buildTable(HuffmanTree *tree);
///進行編碼
void encode(HuffmanTable *table,char *stringToEncode,char *stringEncoded);
///進行解碼
void decode(HuffmanTree *tree,char *stringToDecode,char *stringDecoded);
#endif /* Huffman_h */
Huffman.c
#include "Huffman.h"
#include "PriorityQueue.h"
#include <string.h>
///創建赫夫曼樹
HuffmanTree *buildTree(char *inputString){
//創建一個含有所有字符的數組,來記錄每個字符出現的次數
int *probability = (int *)malloc(sizeof(int)*256);
//初始化
for (int i = 0; i < 256; i++) {
probability[i] = 0;
}
//統計待編碼的字符串各個字符出現的次數
for (int i = 0; inputString[i] != '\0'; i++) {
//看到有出現,則將這個位置的出現次數加1
probability[(unsigned char)(inputString[i])]++;
}
//隊列
PQueue *queue;
//初始化隊列(分配頭結點空間與初始化)
initQueue(&queue);
for (int i = 0; i < 256; i++) {
//如果這個字符出現過
if (probability[i] != 0) {
//初始化一個樹結點
HuffmanNode *node = (HuffmanNode *)malloc(sizeof(HuffmanNode));
//左右子樹置空
node->left = NULL;
node->right = NULL;
//將int轉化為ASCII碼
node->symbol = (char)i;
//插入隊列
addQueue(&queue, node, probability[i]);
}
}
//釋放可能性數組
free(probability);
//隊列元素只剩1,則構建完成了赫夫曼樹
while (queue->size > 1) {
int priority = queue->first->priotity;
priority += queue->first->next->priotity;
HuffmanNode *left = getQueue(&queue);
HuffmanNode *right = getQueue(&queue);
//創建新的結點,加入到隊列中
HuffmanNode *newNode = (HuffmanNode *)malloc(sizeof(HuffmanNode));
newNode->left = left;
newNode->right = right;
addQueue(&queue, newNode, priority);
}
//赫夫曼樹的根節點就得到了,而隊列也沒有用了
HuffmanTree *tree = (HuffmanTree *)malloc(sizeof(HuffmanTree));
tree->root = getQueue(&queue);
//銷毀隊列
destroyQueue(&queue);
return tree;
}
void travalTree(HuffmanNode *treeNode,HuffmanTable **table,int k,char code[256]);
///創建編碼表
HuffmanTable *buildTable(HuffmanTree *tree){
//初始化
HuffmanTable *table = (HuffmanTable *)malloc(sizeof(HuffmanTable));
table->first = NULL;
table->last = NULL;
//編碼數組
char code[256];
int k = 0;//下標
travalTree(tree->root, &table, k, code);
return table;
}
void travalTree(HuffmanNode *treeNode,HuffmanTable **table,int k,char code[256]){
if (treeNode->left == NULL && treeNode->right == NULL) {
//遍歷到了葉子結點
code[k] = '\0';//添加\0作為終止符
HuffmanTableNode *node = (HuffmanTableNode *)malloc(sizeof(HuffmanTableNode));
node->code = (char *)malloc(sizeof(char) * (strlen(code) + 1));
strcpy(node->code, code);//賦值
node->symbol = treeNode->symbol;//賦值字符
node->next = NULL;
if ((*table)->first == NULL) {
(*table)->first = node;
(*table)->last = node;
} else {
(*table)->last->next = node;
(*table)->last = node;
}
}
if (treeNode->left) {
//遍歷到左邊,則添加0
code[k] = '0';
travalTree(treeNode->left, table, k+1, code);
}
if (treeNode->right){
//遍歷到右邊,則添加1
code[k] = '1';
travalTree(treeNode->right, table, k+1, code);
}
}
HuffmanTableNode *visitHuffmanTableNode(HuffmanTableNode *node,char code);
///進行編碼
void encode(HuffmanTable *table,char *stringToEncode,char *stringEncoded){
char result[512];
memset(result, 0, sizeof(result));
//因為得到了這個字符串的赫夫曼編碼表,那么我們只需要遍歷這個字符串的每個字符即可進行編碼
for (int i = 0; stringToEncode[i] != '\0'; i++) {
char code = stringToEncode[i];
//找到對應的編碼表結點
HuffmanTableNode *traversal = visitHuffmanTableNode(table->first, code);
//拼接到字符串中
strcat(result, traversal->code);
}
strcpy(stringEncoded, result);
}
HuffmanTableNode *visitHuffmanTableNode(HuffmanTableNode *node,char code){
if (node->symbol == code) {
return node;
}
return visitHuffmanTableNode(node->next, code);
}
///進行解碼
void decode(HuffmanTree *tree,char *stringToDecode,char *stringDecoded){
HuffmanNode *traversal = tree->root;
char result[512];
memset(result, 0, sizeof(result));
for (int i = 0; stringToDecode[i] != '\0'; i++) {
//為0則往左走一步,為1則往右走一步
if (traversal->left == NULL && traversal->right == NULL) {
//如果到了葉子結點,則得到一個字符
strcat(result, &(traversal->symbol));
traversal = tree->root;
}
if (stringToDecode[i] == '0') {
traversal = traversal->left;
}
if (stringToDecode[i] == '1') {
traversal = traversal->right;
}
if (stringToDecode[i] != '0' && stringToDecode[i] != '1') {
continue;
}
}
if (traversal->left == NULL && traversal->right == NULL) {
strcat(result, &(traversal->symbol));
traversal = tree->root;
}
strcpy(stringDecoded, result);
}
PriorityQueue文件
PriorityQueue.h
#ifndef PriorityQueue_h
#define PriorityQueue_h
#include <stdlib.h>
#include <stdio.h>
#include "Huffman.h"
//隊列結點
typedef struct PQueueNode{
HuffmanNode *val;//指向赫夫曼樹結點的指針
unsigned int priotity;//權值
struct PQueueNode *next;//指向下一個結點
}PQueueNode;
//隊列,當隊列只剩下1個元素時,則是我們的赫夫曼樹
typedef struct PQueue{
unsigned int size;//隊列元素個數
PQueueNode *first;//指向真正的結點
}PQueue;
///初始化隊列
void initQueue(PQueue **queue);
///往隊列中添加結點
void addQueue(PQueue **queue,HuffmanNode *val,unsigned int priority);
///獲取隊列的
HuffmanNode *getQueue(PQueue **queue);
///銷毀對壘
void destroyQueue(PQueue **queue);
#endif /* PriorityQueue_h */
PriorityQueue.c
#define MAX_SIZE 256
#include "PriorityQueue.h"
///初始化隊列
void initQueue(PQueue **queue){
*queue = (PQueue *)malloc(sizeof(PQueue));
(*queue)->first = NULL;
(*queue)->size = 0;
}
///往隊列中添加結點
void addQueue(PQueue **queue,HuffmanNode *val,unsigned int priority){
//判斷是否已滿
if ((*queue)->size == MAX_SIZE) {
printf("\n queue is full.\n");
return;
}
//生成一個隊列結點
PQueueNode *node = (PQueueNode *)malloc(sizeof(PQueueNode));
node->next = NULL;
node->priotity = priority;
node->val = val;
if ((*queue)->size == 0 || (*queue)->first == NULL) {
//如果空隊列,直接把新來的放在首結點位置
(*queue)->first = node;
(*queue)->size = 1;
} else {
if (priority <= (*queue)->first->priotity) {
//如果權值小于等于首結點的權值,那么直接替換掉首節點(由于隊列的權值是從小到大)
node->next = (*queue)->first;
(*queue)->first = node;
(*queue)->size++;
} else {
//如果權值比首節點大,則需要查找到應該插入的位置,再進行插入
PQueueNode *iterator = (*queue)->first;
while (iterator->next) {
if (priority <= iterator->next->priotity) {
//如果比當前結點的下一個結點的權值小,則插入在這個結點之后
node->next = iterator->next;
iterator->next = node;
(*queue)->size++;
break;
}
iterator = iterator->next;
}
//找到最后,直接插在最后面
if (iterator->next == NULL) {
iterator->next = node;
(*queue)->size++;
}
}
}
}
HuffmanNode *getQueue(PQueue **queue){
HuffmanNode *returnNode = NULL;
if ((*queue)->size > 0) {
//依次從頭取出樹結點 并釋放取出的隊列結點
PQueueNode *firstNode = (*queue)->first;
returnNode = firstNode->val;
(*queue)->first = (*queue)->first->next;
(*queue)->size--;
free(firstNode);
} else {
printf("\n queue is empty.\n");
}
return returnNode;
}
void destroyQueue(PQueue **queue){
free(*queue);
*queue = NULL;
}
代碼驗證
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "Huffman.h"
int main(int argc, const char * argv[]) {
char *inputString = "i am student";
HuffmanTree *tree = buildTree(inputString);
HuffmanTable *table = buildTable(tree);
printf("inputString is %s\n",inputString);
//進行編碼
char encodedString[512];
memset(encodedString, 0, sizeof(encodedString));
encode(table, inputString, encodedString);
printf("encodedString is %s\n",encodedString);//結果:0101010011101101111110011100000111100100
//進行解碼
char decodedString[512];
memset(decodedString, 0, sizeof(decodedString));
decode(tree, encodedString, decodedString);
printf("decodedString is %s\n",decodedString);//結果:i am student
return 0;
}
如上所示,我們輸入字符串i am student
,通過編碼再解碼,輸出的字符串也是i am student
,輸入其它的字符串結果也是一致的,這說明我們的算法實現是正確的