七、二叉樹(七)、赫夫曼樹&赫夫曼編碼

數據結構目錄

一、赫夫曼樹的定義

下面是一個例子

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的赫夫曼樹

此時,我們對這六個字母用其從樹根到葉子所經過路徑的0或1來編碼,得到下表:


赫夫曼編碼

再次編碼為“1001 01 00 101 01 00 1000 11 11 00”。


編碼壓縮比較

對比結果節約了大約17%的數據空間。如果解碼呢,必須用到赫夫曼樹,即發送方和接收方必須約定好同樣的赫夫曼編碼規則。

以上面新編碼二進制串為例,從樹根結點出發,按二進制數表示的路徑到達葉子節點,解碼出字母。后面的二進制再從樹根結點出發,以此循環。1001走到葉子節點B,后面的01走到葉子節點A,以此類推。

三、赫夫曼編碼代碼實現

算法思路:

  1. 創建一個隊列queue,分別存儲字符的出現次數,這個隊列中每個結點的權值是按照從小到大的順序存儲的。
  2. 按照赫夫曼樹的構造方式,先取出兩個權值最小的結點a和b,將它們的權值相加,創建一個新結點,新節點的權值是兩個權值最小結點之和,left和right指向a和b,將新節點按照權值從小到大的順序插入隊列中,并在隊列中刪除a和b,循環第2步,最后隊列queue中只剩下一個結點時,這個結點就是赫夫曼樹的根節點,也就是說,這個結點指向一個赫夫曼樹
  3. 我們通過遍歷赫夫曼樹的結果,為左子樹加一個0,右子樹加一個1,一直到葉子結點,我們加一個'\0',這樣我們就得到了每個字符的編碼,并將結果存入一個表格中
  4. 解碼的過程,我們拿到編碼后,在赫夫曼樹上,為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,輸入其它的字符串結果也是一致的,這說明我們的算法實現是正確的

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380