什麼是哈弗曼樹
- 百度百科的定義
給定N個權值作爲N個葉子結點,構造一棵二叉樹,若該樹的帶權路徑長度達到最小,稱這樣的二叉樹爲最優二叉樹,也稱爲哈夫曼樹(Huffman Tree)。哈夫曼樹是帶權路徑長度最短的樹,權值較大的結點離根較近。
哈弗曼樹
上述定義很學術,是很嚴謹的表達,但是看起來總不是那麼好理解。在這裏,我們不說理論,直接來看一顆哈弗曼樹是如何構建的,通過具象事物來理解抽象的概念。
怎樣構建哈弗曼樹
下邊,我們使用一個哈夫曼編碼的例子來理解哈弗曼樹。
- 背景問題
在這裏,我們通過一個示例來說明。
現在,需要將一篇英文文章從A發送給B,要求是編碼的長度最短。英文字母一共26個,大小寫如果不同,那就是52個。那麼我們需要6位二進制來進行編碼(2^5 < 52 < 2 ^6=64)。如果這篇文章有字母10000個,那麼編碼長度就是10000*6。我們知道,在文章中,每一個字母出現的頻率是不同的,思考:對不同頻率的字母使用不通長度的編碼位數,也就是出現頻率最高的字母最短的編碼長度,給頻率最低的字母最長的編碼長度。 這樣就能使得整篇文章的編碼總長度降到最低。提高傳輸效率。
實現方式如下:
字符頻率表
字母 | A | B | C | D | E | F | G | H |
---|---|---|---|---|---|---|---|---|
頻率 | 80 | 30 | 20 | 75 | 40 | 8 | 55 | 60 |
根據上述表格,我們知道,A的編碼是最短的,F的編碼是最長的。這裏的頻率是我任意指定的,實際字母出現的概率在密碼學中有統計(可以參考字母頻率)。
- 構建
第一步:選出其中頻率最小的兩個字母F和C,用這連個字母組成二叉樹,小的爲左孩子,大的爲右孩子。並且將F和C的頻率之和作爲根節點,返回給頻率表。
第二步:連續重複上述操作。
F+C 小於 B,所以28在左孩子的位置。
字母 | A | B | D | E | FC | G | H |
---|---|---|---|---|---|---|---|
頻率 | 80 | 30 | 75 | 40 | 28 | 55 | 60 |
發現此時最小的是 40 和 55 (E和G)
字母 | A | D | E | FCB | G | H |
---|---|---|---|---|---|---|
頻率 | 80 | 75 | 40 | 58 | 55 | 60 |
此時最小的是58 和60 (EG和H)
字母 | A | D | FCB | EG | H |
---|---|---|---|---|---|
頻率 | 80 | 75 | 58 | 95 | 60 |
字母 | A | D | FCBH | EG |
---|---|---|---|---|
頻率 | 80 | 75 | 118 | 95 |
字母 | AD | FCBH | EG |
---|---|---|---|
頻率 | 155 | 118 | 95 |
字母 | AD | FCBHEG |
---|---|---|
頻率 | 155 | 213 |
至此,哈弗曼樹構造完成了,那麼前面說的編碼是怎麼實現的呢?按照二進制,輸的左邊標0,右邊標1.沿着樹的方向直至字母所在的葉子節點的0和1的序列即爲該字母的哈夫曼編碼。如下如:
下表是所有字母的最終編碼
字母 | 編碼 |
---|---|
A | 01 |
B | 1101 |
C | 11001 |
D | 00 |
E | 100 |
F | 11000 |
G | 101 |
H | 111 |
假如我要傳送ABC四個字母,那麼編碼就是0 111111 1111101,一共14位,如果按照最開始的一個字母6位編碼,那麼長度就是18了。
哈弗曼樹的代碼實現
構建哈夫曼樹時,需要每次根據各個結點的權重值,篩選出其中值最小的兩個結點,然後構建二叉樹。
查找權重值最小的兩個結點的思想是:從樹組起始位置開始,首先找到兩個無父結點的結點(說明還未使用其構建成樹),然後和後續無父結點的結點依次做比較,有兩種情況需要考慮:
- 如果比兩個結點中較小的那個還小,就保留這個結點,刪除原來較大的結點;
- 如果介於兩個結點權重值之間,替換原來較大的結點;
哈夫曼樹的結構數據結構
// 哈夫曼樹結點結構
typedef int Type;
typedef struct HuffmanNode_
{
Type weight; // 節點權重
Type parent, left, right; //父結點、左孩子、右孩子在數組中的位置下標
}Node, *HuffmanTree;
// 選中頻率最小的兩個數據
// HT數組中存放的哈夫曼樹,end表示HT數組中存放結點的最終位置,s1和s2傳遞的是HT數組中權重值最小的兩個結點在數組中的位置
void select(HuffmanTree HT, int *pos1, int *pos2, int end)
{
int min1 = 0, min2 = 0;
int i = 1; // 數組的 0 號元素作爲根節點的位置所以不使用
// 找到沒有構建成樹的第一個節點
while (HT[i].parent != 0 && i <= end)
{
i++;
}
min1 = HT[i].weight;
*pos1 = i;
i++;
// 找到沒有構建成樹的第二個節點
while(HT[i].parent != 0 && i <= end)
{
i++;
}
min2 = HT[i].weight;
if (min2 < min1)
{
min2 = min1;
*pos2 = *pos1;
min1 = HT[i].weight;
*pos1 = i;
}
else
{
*pos2 = i;
}
// 取得兩個節點之後,跟之後所有沒有構建成樹的節點逐一比較,最終獲取最小的兩個節點
for (int j = i+1; j <= end; ++j)
{
// 如果已經存在父節點,也就是已經被構建樹了,則跳過
if (HT[j].parent != 0)
{
continue;
}
// 如果比min1 還小,將min2 = 敏, min1修改爲新的節點下標
if (HT[j].weight < min1)
{
min2 = min1;
min1 = HT[j].weight;
*pos2 = *pos1;
*pos1 = j;
}
else if (HT[j].weight < min2 && HT[j].weight > min1)
{
// 如果大於 min1 小於 min2
min2 = HT[j].weight;
*pos2 = j;
}
}
}
// 創建完整的哈夫曼樹
// HT爲地址傳遞的存儲哈夫曼樹的數組,w爲存儲結點權重值的數組,n爲結點個數
HuffmanTree init_huffman_tree(Type *weight, int node_num)
{
if (node_num <= 1)
{
// 只有一個節點那麼編碼就是 0
return NULL;
}
int tree_node_num = node_num * 2 - 1; // 根節點不使用
HuffmanTree p = (HuffmanTree)malloc((tree_node_num+1) * sizeof(Node));
// 初始化哈夫曼數組中的所有節點
for (int i = 1; i <= tree_node_num; ++i)
{
if (i <= node_num)
{
(p+i)->weight = *(weight+i-1); // 第0個位置不使用
}
else
{
(p+i)->weight = 0;
}
(p+i)->parent = 0;
(p+i)->left = 0;
(p+i)->right = 0;
}
return p;
}
void close_huffman_tree(HuffmanTree HT)
{
if (HT)
{
free(HT);
HT = NULL;
}
}
void create_huffman_tree(HuffmanTree HT, int node_num)
{
if (NULL == HT || node_num <= 1)
{
return;
}
int tree_node_num = node_num * 2 - 1; // 根節點不使用
for (int i = node_num + 1; i <= tree_node_num; ++i)
{
int pos1 = -1, pos2 = -1;
// 找到頻率最小的連個節點
select(HT, &pos1, &pos2, i-1);
printf("當前最小的兩個節點 [%d %d]\n", HT[pos1].weight, HT[pos2].weight);
// 這裏使用下表來表示父子關係
HT[pos1].parent = HT[pos2].parent = i; // pos1 位置的元素和pos2位置的元素 的父節點就是,第 i個位置的元素
HT[i].left = pos1; // 父節點的左後孩子賦值
HT[i].right = pos2;
HT[i].weight = HT[pos1].weight + HT[pos2].weight; // 父節點的權重等於 左右孩子權重的和
}
}
- 測試代碼
void print(HuffmanTree HT, int node_num)
{
if (NULL == HT)
{
printf("數組爲空\n");
return;
}
int tree_node_num;
for (int i = 1; i < tree_node_num; ++i)
{
printf("%d 的父節點:%d 左孩子:%d 右孩子:%d\n", HT[i].weight, HT[HT[i].parent].weight, HT[i].left, HT[i].right);
}
}
int main(int argc, char const *argv[])
{
Type weight[8] = {80, 30, 20, 75, 40, 8, 55, 60};
int node_num = sizeof(weight) / sizeof(Type);
HuffmanTree HT = init_huffman_tree(weight, node_num);
create_huffman_tree(HT, node_num);
print(HT, node_num);
close_huffman_tree(HT);
return 0;
}
- 測試結果
!
爲什麼要設計哈弗曼樹
- 哈弗曼樹主要用於哈夫曼編碼,其主要作用就是利用頻率屬性進行編碼,最終達到目的:讓高頻的數據擁有短編碼,而低頻的數據擁有長編碼。
- 哈夫曼編碼並不是適用於所有場景,它更適用於頻率變化多端的數據編碼。