赫夫曼樹,別名“哈夫曼樹”、“最優樹”以及“最優二叉樹”。學習哈夫曼樹之前,首先要了解幾個名詞。
路徑長度:在一條路徑中,每經過一個結點,路徑長度都要加 1 。例如在一棵樹中,規定根結點所在層數為1層,那么從根結點到第 i 層結點的路徑長度為 i - 1 。圖 1 中從根結點到結點 c 的路徑長度為 3。
結點的權:給每一個結點賦予一個新的數值,被稱為這個結點的權。例如,圖 1 中結點 a 的權為 7,結點 b 的權為 5。
結點的帶權路徑長度:指的是從根結點到該結點之間的路徑長度與該結點的權的乘積。例如,圖 1 中結點 b 的帶權路徑長度為 2 * 5 = 10 。
樹的帶權路徑長度為樹中所有葉子結點的帶權路徑長度之和。通常記作 “WPL” 。例如圖 1 中所示的這顆樹的帶權路徑長度為:
圖1 哈夫曼樹
在構建哈弗曼樹時,要使樹的帶權路徑長度最小,只需要遵循一個原則,那就是:權重越大的結點離樹根越近。在圖 1 中,因為結點 a 的權值最大,所以理應直接作為根結點的孩子結點。
所以,哈夫曼樹中結點構成用代碼表示為:
查找權重值最小的兩個結點的思想是:從樹組起始位置開始,首先找到兩個無父結點的結點(說明還未使用其構建成樹),然后和后續無父結點的結點依次做比較,有兩種情況需要考慮:
實現代碼:
哈夫曼編碼就是在哈夫曼樹的基礎上構建的,這種編碼方式最大的優點就是用最少的字符包含最多的信息內容。
根據發送信息的內容,通過統計文本中相同字符的個數作為每個字符的權值,建立哈夫曼樹。對於樹中的每一個子樹,統一規定其左孩子標記為 0 ,右孩子標記為 1 。這樣,用到哪個字符時,從哈夫曼樹的根結點開始,依次寫出經過結點的標記,最終得到的就是該結點的哈夫曼編碼。
圖 3 哈夫曼編碼
如圖 3 所示,字符 a 用到的次數最多,其次是字符 b 。字符 a 在哈夫曼編碼是
使用程序求哈夫曼編碼有兩種方法:
采用方法 1 的實現代碼為:
采用第二種算法的實現代碼為:
圖 4 程序運行效果圖
本節的程序中對權重值分別為 2,8,7,6,5 的結點構建的哈夫曼樹如圖 4(A)所示。圖 4(B)是另一個哈夫曼樹,兩棵樹的帶權路徑長度相同。
程序運行效果圖之所以是(A)而不是(B),原因是在構建哈夫曼樹時,結點 2 和結點 5 構建的新的結點 7 存儲在動態樹組的最后面,所以,在程序繼續選擇兩個權值最小的結點時,直接選擇了的葉子結點 6 和 7 。
哈夫曼樹相關的幾個名詞
路徑:在一棵樹中,一個結點到另一個結點之間的通路,稱為路徑。圖 1 中,從根結點到結點 a 之間的通路就是一條路徑。路徑長度:在一條路徑中,每經過一個結點,路徑長度都要加 1 。例如在一棵樹中,規定根結點所在層數為1層,那么從根結點到第 i 層結點的路徑長度為 i - 1 。圖 1 中從根結點到結點 c 的路徑長度為 3。
結點的權:給每一個結點賦予一個新的數值,被稱為這個結點的權。例如,圖 1 中結點 a 的權為 7,結點 b 的權為 5。
結點的帶權路徑長度:指的是從根結點到該結點之間的路徑長度與該結點的權的乘積。例如,圖 1 中結點 b 的帶權路徑長度為 2 * 5 = 10 。
樹的帶權路徑長度為樹中所有葉子結點的帶權路徑長度之和。通常記作 “WPL” 。例如圖 1 中所示的這顆樹的帶權路徑長度為:
WPL = 7 * 1 + 5 * 2 + 2 * 3 + 4 * 3

圖1 哈夫曼樹
什么是哈夫曼樹
當用 n 個結點(都做葉子結點且都有各自的權值)試圖構建一棵樹時,如果構建的這棵樹的帶權路徑長度最小,稱這棵樹為“最優二叉樹”,有時也叫“赫夫曼樹”或者“哈夫曼樹”。在構建哈弗曼樹時,要使樹的帶權路徑長度最小,只需要遵循一個原則,那就是:權重越大的結點離樹根越近。在圖 1 中,因為結點 a 的權值最大,所以理應直接作為根結點的孩子結點。
構建哈夫曼樹
對於給定的有各自權值的 n 個結點,構建哈夫曼樹有一個行之有效的辦法:- 在 n 個權值中選出兩個最小的權值,對應的兩個結點組成一個新的二叉樹,且新二叉樹的根結點的權值為左右孩子權值的和;
- 在原有的 n 個權值中刪除那兩個最小的權值,同時將新的權值加入到 n–2 個權值的行列中,以此類推;
- 重復 1 和 2 ,直到所以的結點構建成了一棵二叉樹為止,這棵樹就是哈夫曼樹。

圖 2 哈夫曼樹的構建過程
圖 2 中,(A)給定了四個結點a,b,c,d,權值分別為7,5,2,4;第一步如(B)所示,找出現有權值中最小的兩個,2 和 4 ,相應的結點 c 和 d 構建一個新的二叉樹,樹根的權值為 2 + 4 = 6,同時將原有權值中的 2 和 4 刪掉,將新的權值 6 加入;進入(C),重復之前的步驟。直到(D)中,所有的結點構建成了一個全新的二叉樹,這就是哈夫曼樹。
哈弗曼樹中結點結構
構建哈夫曼樹時,首先需要確定樹中結點的構成。由於哈夫曼樹的構建是從葉子結點開始,不斷地構建新的父結點,直至樹根,所以結點中應包含指向父結點的指針。但是在使用哈夫曼樹時是從樹根開始,根據需求遍歷樹中的結點,因此每個結點需要有指向其左孩子和右孩子的指針。所以,哈夫曼樹中結點構成用代碼表示為:
//哈夫曼樹結點結構 typedef struct
{ int weight; //結點權重 int parent, 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; } } }
注意:s1和s2傳入的是實參的地址,所以函數運行完成后,實參中存放的自然就是哈夫曼樹中權重值最小的兩個結點在數組中的位置。
構建哈弗曼樹的代碼實現
//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 哈夫曼編碼
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; } else if(HT[p].right == 0) // 當前結點沒有左孩子,也沒有右孩子,說明為葉子結點,直接記錄哈夫曼編碼
{ (*HC)[p] = (char*)malloc((cdlen+1)*sizeof(char)); cd[cdlen] = '\0'; strcpy((*HC)[p], cd); } } else if(HT[p].weight == 1) // 如果weiget為1,說明訪問過一次,即使是左孩子返回的
{ HT[p].weight = 2; //設置訪問次數為2 //如果有右孩子,遍歷右孩子,記錄標記值 1 if (HT[p].right != 0)
{ p = HT[p].right; cd[cdlen++] = '1'; } } else //如果訪問次數為2,說明左孩子都遍歷完了,返回父結點
{ 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; } else if(HT[j].weight >= min1 && HT[j].weight < min2) // 如果介於兩者之間,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; }
運行結果 Huffman code : 2 code = 100 8 code = 11 7 code = 01 6 code = 00 5 code = 101
本節中介紹了兩種遍歷哈夫曼樹獲得哈夫曼編碼的方法,同時也給出了各自完整的實現代碼的函數,在完整代碼中使用的是第一種逆序遍歷哈夫曼樹的方法。
總結

圖 4 程序運行效果圖
本節的程序中對權重值分別為 2,8,7,6,5 的結點構建的哈夫曼樹如圖 4(A)所示。圖 4(B)是另一個哈夫曼樹,兩棵樹的帶權路徑長度相同。
程序運行效果圖之所以是(A)而不是(B),原因是在構建哈夫曼樹時,結點 2 和結點 5 構建的新的結點 7 存儲在動態樹組的最后面,所以,在程序繼續選擇兩個權值最小的結點時,直接選擇了的葉子結點 6 和 7 。