哈夫曼樹
哈夫曼樹
簡介
哈夫曼樹(Huffman Tree),又名:最優二叉樹,赫夫曼樹
其標準含義是:給定N個權值作為N個葉子結點,構造一棵二叉樹,若該樹的帶權路徑長度達到最小,稱這樣的二叉樹為最優二叉樹,也稱為哈夫曼樹。哈夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近。
相關名詞
由於本篇存在一定的難度,因此在開始相關的學習之前,請讓我們來鞏固以下本文所涉及的名詞知識點。
a) 路徑:在一棵樹中,一個結點到另一個結點之間的通路,稱為路徑。
b) 路徑長度:在一條路徑中,每經過一個結點,路徑長度都要加 1 。例如在一棵樹中,規定根結點所在層數為1層,那麼從根結點到第 i 層結點的路徑長度為 i - 1 。
c) 結點的權:給每一個結點賦予一個新的數值,被稱為這個結點的權。
d) 結點的帶權路徑長度:指的是從根結點到該結點之間的路徑長度與該結點的權的乘積。
e) 樹的帶權路徑長度為樹中所有葉子結點的帶權路徑長度之和。通常記作 “WPL”。
構建哈夫曼樹
在構建哈夫曼樹時,只需要遵循一個原則,那就是權重越大的結點距離樹根越近。
因此,在構建過程中,有如下的規律:
首先,選出我們資料中最小的兩個資料,構建成二叉樹的左孩子和右孩子,而根的資料為兩者之和
其次,將剛才合成的資料作為右孩子,左孩子從未處理的資料中選出最小的一個,作為左孩子,他們的根同樣為左右孩子的權值和
不斷重複上述的步驟,直到將所有的資料全部處理完並構建出二叉樹,這棵二叉樹就是我們的哈夫曼樹。
如圖這顆哈夫曼樹的WPL值為:WPL= 8*1+ 6*2 + 1*3 + 4*3 = 273
哈弗曼樹中結點結構
構建哈夫曼樹時,首先需要確定樹中結點的構成。由於哈夫曼樹的構建是從葉子結點開始,不斷地構建新的父結點,直至樹根,所以結點中應包含指向父結點的指標。但是在使用哈夫曼樹時是從樹根開始,根據需求遍歷樹中的結點,因此每個結點需要有指向其左孩子和右孩子的指標。
所以,哈夫曼樹中結點構成用程式碼表示為:
//哈夫曼樹結點結構 typedef struct { int weight;//結點權重 intparent, left, right;//父結點、左孩子、右孩子在陣列中的位置下標 }HTNode, *HuffmanTree;
哈弗曼樹中的查詢演算法
構建哈夫曼樹時,需要每次根據各個結點的權重值,篩選出其中值最小的兩個結點,然後構建二叉樹。
查詢權重值最小的兩個結點的思想是:從樹組起始位置開始,首先找到兩個無父結點的結點(說明還未使用其構建成樹),然後和後續無父結點的結點依次做比較,有兩種情況需要考慮:
- 如果比兩個結點中較小的那個還小,就保留這個結點,刪除原來較大的結點;
- 如果介於兩個結點權重值之間,替換原來較大的結點;
//HT陣列中存放的哈夫曼樹,end表示HT陣列中存放結點的最終位置,s1和s2傳遞的是HT陣列中權重值最小的兩個結點在陣列中的位置 void Select(HuffmanTree HT, int end, int *s1, int *s2) { int min1, min2; //遍歷陣列初始下標為 1 int i = 1; //找到還沒構建樹的結點 while(HT[i].parent != 0 && i <= end){ i++; } min1 = HT[i].weight; *s1 = i; i++; while(HT[i].parent != 0 && i <= end){ i++; } //對找到的兩個結點比較大小,min2為大的,min1為小的 if(HT[i].weight < min1){ min2 = min1; *s2 = *s1; min1 = HT[i].weight; *s1 = i; }else{ min2 = HT[i].weight; *s2 = i; } //兩個結點和後續的所有未構建成樹的結點做比較 for(int j=i+1; j <= end; j++) { //如果有父結點,直接跳過,進行下一個 if(HT[j].parent != 0){ continue; } //如果比最小的還小,將min2=min1,min1賦值新的結點的下標 if(HT[j].weight < min1){ min2 = min1; min1 = HT[j].weight; *s2 = *s1; *s1 = j; } //如果介於兩者之間,min2賦值為新的結點的位置下標 else if(HT[j].weight >= min1 && HT[j].weight < min2){ min2 = HT[j].weight; *s2 = j; } } }
構建哈弗曼樹的程式碼實現
//HT為地址傳遞的儲存哈夫曼樹的陣列,w為儲存結點權重值的陣列,n為結點個數 void CreateHuffmanTree(HuffmanTree *HT, int *w, int n) { if(n<=1) return; // 如果只有一個編碼就相當於0 int m = 2*n-1; // 哈夫曼樹總節點數,n就是葉子結點 *HT = (HuffmanTree) malloc((m+1) * sizeof(HTNode)); // 0號位置不用 HuffmanTree p = *HT; // 初始化哈夫曼樹中的所有結點 for(int i = 1; i <= n; i++) { (p+i)->weight = *(w+i-1); (p+i)->parent = 0; (p+i)->left = 0; (p+i)->right = 0; } //從樹組的下標 n+1 開始初始化哈夫曼樹中除葉子結點外的結點 for(int i = n+1; i <= m; i++) { (p+i)->weight = 0; (p+i)->parent = 0; (p+i)->left = 0; (p+i)->right = 0; } //構建哈夫曼樹 for(int i = n+1; i <= m; i++) { int s1, s2; Select(*HT, i-1, &s1, &s2); (*HT)[s1].parent = (*HT)[s2].parent = i; (*HT)[i].left = s1; (*HT)[i].right = s2; (*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight; } }
哈夫曼編碼
哈夫曼編碼就是在哈夫曼樹的基礎上構建的,這種編碼方式最大的優點就是用最少的字元包含最多的資訊內容。
根據傳送資訊的內容,通過統計文字中相同字元的個數作為每個字元的權值,建立哈夫曼樹。對於樹中的每一個子樹,統一規定其左孩子標記為 0 ,右孩子標記為 1 。這樣,用到哪個字元時,從哈夫曼樹的根結點開始,依次寫出經過結點的標記,最終得到的就是該結點的哈夫曼編碼。
文字中字元出現的次數越多,在哈夫曼樹中的體現就是越接近樹根。編碼的長度越短。
圖 3 哈夫曼編碼
如圖 3 所示,字元 a 用到的次數最多,其次是字元 b 。字元 a 在哈夫曼編碼是0
,字元 b 編碼為10
,字元 c 的編碼為110
,字元 d 的編碼為111
。
使用程式求哈夫曼編碼有兩種方法:
- 從葉子結點一直找到根結點,逆向記錄途中經過的標記。例如,圖 3 中字元 c 的哈夫曼編碼從結點 c 開始一直找到根結點,結果為:0 1 1 ,所以字元 c 的哈夫曼編碼為:1 1 0(逆序輸出)。
- 從根結點出發,一直到葉子結點,記錄途中經過的標記。例如,求圖 3 中字元 c 的哈夫曼編碼,就從根結點開始,依次為:1 1 0。
採用方法 1 的實現程式碼為:
//HT為哈夫曼樹,HC為儲存結點哈夫曼編碼的二維動態陣列,n為結點的個數 void HuffmanCoding(HuffmanTree HT, HuffmanCode *HC,int n){ *HC = (HuffmanCode) malloc((n+1) * sizeof(char *)); char *cd = (char *)malloc(n*sizeof(char)); //存放結點哈夫曼編碼的字串陣列 cd[n-1] = '\0';//字串結束符 for(int i=1; i<=n; i++){ //從葉子結點出發,得到的哈夫曼編碼是逆序的,需要在字串陣列中逆序存放 int start = n-1; //當前結點在陣列中的位置 int c = i; //當前結點的父結點在陣列中的位置 int j = HT[i].parent; // 一直尋找到根結點 while(j != 0){ // 如果該結點是父結點的左孩子則對應路徑編碼為0,否則為右孩子編碼為1 if(HT[j].left == c) cd[--start] = '0'; else cd[--start] = '1'; //以父結點為孩子結點,繼續朝樹根的方向遍歷 c = j; j = HT[j].parent; } //跳出迴圈後,cd陣列中從下標 start 開始,存放的就是該結點的哈夫曼編碼 (*HC)[i] = (char *)malloc((n-start)*sizeof(char)); strcpy((*HC)[i], &cd[start]); } //使用malloc申請的cd動態陣列需要手動釋放 free(cd); }
採用第二種演算法的實現程式碼為:
//HT為哈夫曼樹,HC為儲存結點哈夫曼編碼的二維動態陣列,n為結點的個數 void HuffmanCoding(HuffmanTree HT, HuffmanCode *HC,int n){ *HC = (HuffmanCode) malloc((n+1) * sizeof(char *)); int m=2*n-1; int p=m; int cdlen=0; char *cd = (char *)malloc(n*sizeof(char)); //將各個結點的權重用於記錄訪問結點的次數,首先初始化為0 for (int i=1; i<=m; i++) { HT[i].weight=0; } //一開始 p 初始化為 m,也就是從樹根開始。一直到p為0 while (p) { //如果當前結點一次沒有訪問,進入這個if語句 if (HT[p].weight==0) { HT[p].weight=1;//重置訪問次數為1 //如果有左孩子,則訪問左孩子,並且儲存走過的標記為0 if (HT[p].left!=0) { p=HT[p].left; cd[cdlen++]='0'; } //當前結點沒有左孩子,也沒有右孩子,說明為葉子結點,直接記錄哈夫曼編碼 else if(HT[p].right==0){ (*HC)[p]=(char*)malloc((cdlen+1)*sizeof(char)); cd[cdlen]='\0'; strcpy((*HC)[p], cd); } } //如果weight為1,說明訪問過一次,即是從其左孩子返回的 else if(HT[p].weight==1){ HT[p].weight=2;//設定訪問次數為2 //如果有右孩子,遍歷右孩子,記錄標記值 1 if (HT[p].right!=0) { p=HT[p].right; cd[cdlen++]='1'; } } //如果訪問次數為 2,說明左右孩子都遍歷完了,返回父結點 else{ HT[p].weight=0; p=HT[p].parent; --cdlen; } } }
完整程式碼
#include<stdlib.h> #include<stdio.h> #include<string.h> //哈夫曼樹結點結構 typedef struct { int weight;//結點權重 int parent, left, right;//父結點、左孩子、右孩子在陣列中的位置下標 }HTNode, *HuffmanTree; //動態二維陣列,儲存哈夫曼編碼 typedef char ** HuffmanCode; //HT陣列中存放的哈夫曼樹,end表示HT陣列中存放結點的最終位置,s1和s2傳遞的是HT陣列中權重值最小的兩個結點在陣列中的位置 void Select(HuffmanTree HT, int end, int *s1, int *s2) { int min1, min2; //遍歷陣列初始下標為 1 int i = 1; //找到還沒構建樹的結點 while(HT[i].parent != 0 && i <= end){ i++; } min1 = HT[i].weight; *s1 = i; i++; while(HT[i].parent != 0 && i <= end){ i++; } //對找到的兩個結點比較大小,min2為大的,min1為小的 if(HT[i].weight < min1){ min2 = min1; *s2 = *s1; min1 = HT[i].weight; *s1 = i; }else{ min2 = HT[i].weight; *s2 = i; } //兩個結點和後續的所有未構建成樹的結點做比較 for(int j=i+1; j <= end; j++) { //如果有父結點,直接跳過,進行下一個 if(HT[j].parent != 0){ continue; } //如果比最小的還小,將min2=min1,min1賦值新的結點的下標 if(HT[j].weight < min1){ min2 = min1; min1 = HT[j].weight; *s2 = *s1; *s1 = j; } //如果介於兩者之間,min2賦值為新的結點的位置下標 else if(HT[j].weight >= min1 && HT[j].weight < min2){ min2 = HT[j].weight; *s2 = j; } } } //HT為地址傳遞的儲存哈夫曼樹的陣列,w為儲存結點權重值的陣列,n為結點個數 void CreateHuffmanTree(HuffmanTree *HT, int *w, int n) { if(n<=1) return; // 如果只有一個編碼就相當於0 int m = 2*n-1; // 哈夫曼樹總節點數,n就是葉子結點 *HT = (HuffmanTree) malloc((m+1) * sizeof(HTNode)); // 0號位置不用 HuffmanTree p = *HT; // 初始化哈夫曼樹中的所有結點 for(int i = 1; i <= n; i++) { (p+i)->weight = *(w+i-1); (p+i)->parent = 0; (p+i)->left = 0; (p+i)->right = 0; } //從樹組的下標 n+1 開始初始化哈夫曼樹中除葉子結點外的結點 for(int i = n+1; i <= m; i++) { (p+i)->weight = 0; (p+i)->parent = 0; (p+i)->left = 0; (p+i)->right = 0; } //構建哈夫曼樹 for(int i = n+1; i <= m; i++) { int s1, s2; Select(*HT, i-1, &s1, &s2); (*HT)[s1].parent = (*HT)[s2].parent = i; (*HT)[i].left = s1; (*HT)[i].right = s2; (*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight; } } //HT為哈夫曼樹,HC為儲存結點哈夫曼編碼的二維動態陣列,n為結點的個數 void HuffmanCoding(HuffmanTree HT, HuffmanCode *HC,int n){ *HC = (HuffmanCode) malloc((n+1) * sizeof(char *)); char *cd = (char *)malloc(n*sizeof(char)); //存放結點哈夫曼編碼的字串陣列 cd[n-1] = '\0';//字串結束符 for(int i=1; i<=n; i++){ //從葉子結點出發,得到的哈夫曼編碼是逆序的,需要在字串陣列中逆序存放 int start = n-1; //當前結點在陣列中的位置 int c = i; //當前結點的父結點在陣列中的位置 int j = HT[i].parent; // 一直尋找到根結點 while(j != 0){ // 如果該結點是父結點的左孩子則對應路徑編碼為0,否則為右孩子編碼為1 if(HT[j].left == c) cd[--start] = '0'; else cd[--start] = '1'; //以父結點為孩子結點,繼續朝樹根的方向遍歷 c = j; j = HT[j].parent; } //跳出迴圈後,cd陣列中從下標 start 開始,存放的就是該結點的哈夫曼編碼 (*HC)[i] = (char *)malloc((n-start)*sizeof(char)); strcpy((*HC)[i], &cd[start]); } //使用malloc申請的cd動態陣列需要手動釋放 free(cd); } //列印哈夫曼編碼的函式 void PrintHuffmanCode(HuffmanCode htable,int *w,int n) { printf("Huffman code : \n"); for(int i = 1; i <= n; i++) printf("%d code = %s\n",w[i-1], htable[i]); } int main(void) { int w[5] = {2, 8, 7, 6, 5}; int n = 5; HuffmanTree htree; HuffmanCode htable; CreateHuffmanTree(&htree, w, n); HuffmanCoding(htree, &htable, n); PrintHuffmanCode(htable,w, n); return 0; }View Code
總結
圖 4 程式執行效果圖
本節的程式中對權重值分別為 2,8,7,6,5 的結點構建的哈夫曼樹如圖 4(A)所示。圖 4(B)是另一個哈夫曼樹,兩棵樹的帶權路徑長度相同。
程式執行效果圖之所以是(A)而不是(B),原因是在構建哈夫曼樹時,結點 2 和結點 5 構建的新的結點 7 儲存在動態樹組的最後面,所以,在程式繼續選擇兩個權值最小的結點時,直接選擇了的葉子結點 6 和 7 。