數據結構與算法 -- 哈夫曼樹&哈夫曼編碼
前言
上一篇瞭解學習了線索化二叉樹
的一些知識,這一篇,對哈夫曼樹
和哈夫曼編碼
來做一個瞭解學習。
首先,我們先看一個經典的問題,
等級 | 優秀 | 良好 | 中等 | 及格 | 不及格 |
---|---|---|---|---|---|
考分 | 90 <= 分數 <= 100 | 80 <= 分數 < 90 | 70 <= 分數 < 80 | 60 <= 分數 < 70 | 0<=分數<60 |
一般我們會這樣判斷:
那麼,在二叉樹中是這樣的
一般情況下,每個班的成績及格不及格有一定的分佈概率
那這樣就會造成對中等
和良好
的成績判斷時,會經過很多步,當成績比較多時,就會出現效率問題。
那麼怎麼能優化解決一下這個問題呢?其實前輩們已經解決了這個問題,我們接下來學習一下
1. 哈夫曼樹
在上面的成績算法,
節點D
的路徑爲4
,
樹的總路徑爲:1 + 1 + 2 + 2 + 3 + 3 + 4 + 4 = 20
。
根據上面成績的分佈這棵樹的WPL
爲:WPL = 1 * 5 + 2 * 15 + 3 * 40 + 4 * 30 + 4 * 10 = 315
假如我們對成績的判斷順序進行如下的調整:
那麼節點D
的路徑長度爲2
樹的總路徑爲1 + 2 + 3 + 3 + 2 + 1 + 2 + 2 = 16
根據上面成績的分佈這棵樹的WPL
爲:WPL = 5 * 3 + 3 * 15 + 2 * 40 + 2 * 30 + 2 * 10 = 220
第二種情況,明顯比第一種情況更優。
那麼怎麼構建一個最優的樹呢,我們接下來看一下哈夫曼樹
的構建。
假設:A B C D E
的權重如下:
A | B | C | D | E |
---|---|---|---|---|
5 | 15 | 40 | 30 | 10 |
首先,找到權重最小的兩個A
和E
,組成二叉樹:
此時N1
的權重相當於 15
,依次找到 最小的 B
,組建二叉樹:
此時N2
的權重相當於 30
,依次找到 最小的 D
,組建二叉樹:
此時N3
的權重相當於 60
,依次找到 最小的 C
,組建二叉樹(小的在左子樹位置):
該二叉樹的WPL
:40 * 1 + 30 * 2 + 15 * 3 + 10 * 4 + 5 * 4 = 205
這種排列組成的二叉樹的WPL
,明顯小於上面兩種,以這種方式排列的二叉樹即爲哈夫曼樹
小結:
先對給出的節點的權重排序
依次找出最小的兩個節點,組成二叉樹,小的在左子樹位置
2. 哈夫曼編碼
假設如下:
字符 | A | B | C | D | E | F |
---|---|---|---|---|---|---|
權重 | 27 | 8 | 15 | 15 | 30 | 5 |
編碼 | 000 | 001 | 010 | 011 | 100 | 101 |
如上表中,每個字符的編碼位數都是一樣的,但是每個字符出現的權重是不同的,這樣就會造成權重低的字符的空間浪費
我們可以利用 哈夫曼樹 找到最佳的二叉樹排列,然後生成 哈夫曼編碼 來解決這個問題。
首先,對上面的字符生成哈夫曼樹:
- 找到權重最小的 B 和 F,組成二叉樹(小的在左邊)
- 找到 C,組成二叉樹(小的在左邊)
- 依次查找
最終的 哈夫曼樹 如下:
然後,開始哈夫曼編碼
哈夫曼編碼的原則:左子樹上的權重,替換爲 0 ,右子樹權重替換爲 1
然後從根節點,到各個葉子節點,組成每個葉子節點的編碼,如下:
字符 | A | B | C | D | E | F |
---|---|---|---|---|---|---|
權重 | 27 | 8 | 15 | 15 | 30 | 5 |
編碼 | 01 | 1001 | 101 | 00 | 11 | 1000 |
那麼對於同一個字符串BADCADFEED
的新舊二進制編碼如下:
舊編碼: 001 000 011 010 000 011 101 100 100 011 (共30個字符)
新編碼: 1001 01 00 101 01 00 1001 11 11 00 (共25個字符)
可以明顯看出,新編碼(哈夫曼編碼)比舊編碼,有更高的存儲利用率。
小結:
先生成哈夫曼樹
從根節點開始,左子樹的權值爲0,右子樹的權值 爲 1
從根節點開始,到葉子節點,組成葉子節點的編碼
3. 哈夫曼樹 & 哈夫曼編碼 的實現
可以定義哈夫曼樹(順序)的節點如下:
const int MaxValue = 10000;//初始設定的權值最大值
const int MaxBit = 4;//初始設定的最大編碼位數
const int MaxN = 10;//初始設定的最大結點個數
// 定義節點
typedef struct HaffNode{
int weight; // 權值
int flag; // 標記該節點是否合併到哈夫曼樹 0:未合併,1:合併
int parent; // 雙親節點下標
int leftChild; // 左孩子下標
int rightChild; // 右孩子下標
}HaffNode;
// 存放哈夫曼編碼的數據元素結構
typedef struct Code {
int bit[MaxBit]; // 數組
int start; // 編碼的起始下標
int weight; // 字符的權重
}Code;
// 構建哈夫曼樹
void Haffman(int weight[],int n,HaffNode *haffTree){
int j, m1, m2, x1, x2;
// 哈夫曼樹初始化
// n個葉子節點,有(2n - 1)個節點
for (int i = 0; i < 2*n - 1; i++) {
if (i < n) {
haffTree[i].weight = weight[i]; // 葉子節點賦值權重
} else {
haffTree[i].weight = 0;
}
haffTree[i].parent = 0; // 無雙親,下標爲0
haffTree[i].flag = 0; // 未合併到哈夫曼樹,爲0
haffTree[i].leftChild = -1; // 無左右孩子,給特殊在-1
haffTree[i].rightChild = -1;
}
// 構建哈夫曼樹
for (int i = 0; i < n - 1; i++) {
m1 = m2 = MaxValue;
x1 = x2 = 0;
// 循環找到所有權重中最小的兩個
for (j = 0; j < n + i; j++) {
if (haffTree[j].weight < m1 && haffTree[j].flag == 0) {
m2 = m1;
x2 = x1;
m1 = haffTree[j].weight;
x1 = j;
} else if(haffTree[j].weight < m2 && haffTree[j].flag == 0){
m2 = haffTree[j].weight;
x2 = j;
}
}
// 將找到權重最小的兩個子樹合併爲一個子樹
haffTree[x1].parent = n + i;
haffTree[x2].parent = n + i;
haffTree[x1].flag = 1;
haffTree[x2].flag = 1;
// 修改合併後的子樹(n+i)的權重
haffTree[n+i].weight = haffTree[x1].weight + haffTree[x2].weight;
// 修改合併後的子樹(n+i)的左右子樹
haffTree[n+i].leftChild = x1;
haffTree[n+i].rightChild = x2;
}
}
// 哈夫曼編碼
void HaffmanCode(HaffNode haffTree[], int n, Code haffCode[])
{
// 創建一個結點
Code *cd = (Code *)malloc(sizeof(Code));
int child, parent;
// 求n個結點的哈夫曼編碼
for (int i = 0; i < n; i++) {
// 從0開始計數
cd->start = 0;
// 取得編碼對應權值的字符
cd->weight = haffTree[i].weight;
// 當前葉子節點爲 i
child = i;
// 根據葉子節點找到雙親節點
parent = haffTree[i].parent;
// 由葉子節點向上到根節點
while (parent != 0) {
if (haffTree[parent].leftChild == child) {
cd->bit[cd->start] = 0; //左孩子結點編碼0
} else {
cd->bit[cd->start] = 1; //右孩子結點編碼1
}
// 編碼自增
cd->start++;
// 修改子節點爲當前雙親節點(向上查找)
child = parent;
// 根據子節點找到雙親
parent = haffTree[child].parent;
}
// 此時的編碼是倒序的,翻轉 011 start = 3
int temp = 0;
for (int j = cd->start - 1; j >= 0; j--) {
temp = cd->start-j-1;
haffCode[i].bit[temp] = cd->bit[j];
}
//把cd中的數據賦值到haffCode[i]中.
//保存好haffCode 的起始位以及權值;
haffCode[i].start = cd->start;
//保存編碼對應的權值
haffCode[i].weight = cd->weight;
}
}
// 調用
void show{
int i, j, n = 4, m = 0;
//權值
int weight[] = {2,4,5,7};
//初始化哈夫曼樹, 哈夫曼編碼
HaffNode *myHaffTree = malloc(sizeof(HaffNode)*2*n-1);
Code *myHaffCode = malloc(sizeof(Code)*n);
//當前n > MaxN,表示超界. 無法處理.
if (n>MaxN)
{
printf("定義的n越界,修改MaxN!");
exit(0);
}
//1. 構建哈夫曼樹
Haffman(weight, n, myHaffTree);
//2.根據哈夫曼樹得到哈夫曼編碼
HaffmanCode(myHaffTree, n, myHaffCode);
//3.
for (i = 0; i<n; i++)
{
printf("Weight = %d\n",myHaffCode[i].weight);
for (j = 0; j<myHaffCode[i].start; j++)
printf("%d",myHaffCode[i].bit[j]);
m = m + myHaffCode[i].weight*myHaffCode[i].start;
printf("\n");
}
printf("Huffman's WPS is:%d\n",m);
}