前置:C++數據結構_樹的理論學習筆記(2)_存儲結構,二叉樹的實現
1.5 Huffman樹
1.5.1 Huffman樹的定義與存儲結構
1.Huffman樹的定義
哈夫曼樹又稱最優二叉樹,是一種帶權路徑長度最短的二叉樹。樹的帶權路徑長度, 就是樹中所有葉結點的權值乘上其到根結點的路徑長度(若根結點爲0層,葉結點到根結點的路徑長度爲葉結點的層數)之和
圖(C)所示的二叉樹其帶權路徑長度最短,而且再也找不出比此二叉樹帶權路徑長度更短的二叉樹了,因此圖(C)所示是一棵哈夫曼樹
2.Huffman編碼
(1)Huffman編碼根據字符出現的概率來構造平均長度最短的編碼,是一種變長的編碼。它的基本原理是頻繁使用的數據用較小的代碼代替,較少使用的數據用較大的編碼代替,每個數據的代碼不相同,但最終編碼的平均長度最小。
(2)變長的編碼:Huffman編碼得到的字符編碼,其長度因符號出現的概率而有所不同。
(3)Huffman編碼的規則:從根結點到葉結點(包含原信息)的路徑,向左孩子前進編碼爲0,向右孩子前進編碼爲1.也可以反過來規定
示例:
得到字符A、B、C、Z的編碼分別爲0、11、100、101.顯然Huffman編碼是前綴編碼,即任何一個字符的編碼都不是另一個字符編碼的前綴。
3.Huffman樹的存儲結構
以靜態散鏈表來存儲結點:
struct HNode
{
int weight; //結點權值
int parent; //雙親數組下標
int LChild; //左孩子數組下標
int RChild; //右孩子數組下標
};
Huffman樹是一棵正則二叉樹(只有度爲0或2的結點的二叉樹)。根據二叉樹的性質,一棵n個葉子的Huffman樹共有2n-1個結點,可以用一個大小爲2n-1的一維數組存放Huffman樹的各個節點。
如下圖(a)所示的Huffman樹有4個葉子結點,因此可以定義存儲結構爲:
HNode* HTree = new HNode[7];
其存儲內容如下圖(b)所示,-1表示無孩子結點或雙親結點
還需要設計Huffman編碼表對每個結點進行存儲。編碼表中各元素的C++描述如下:
struct HCode
{
char data;
string code;
};
其中,data存儲結點的內存(這裏假設其數據類型爲char)。code數組存儲結點對應的編碼(這裏採用string存儲)。使用HCode類型定義一個一維數組,就可以存儲所有結點的編碼了。
接下來就可以設計Huffman編碼的相關算法了,如構造、編碼、解碼算法。其C++類描述如下:
class Huffman
{
private:
HNode* HTree; //Huffman樹
HCode* HCodeTable; //存儲編碼表
int N; //葉子結點數量
void code(int i, string newcode); //遞歸函數,對第i個結點編碼
public:
void CreateHTree(int a[], int n, char name[]); //創建Huffman樹
void CreateCodeTable(); //創建編碼表
void Encode(char* s, char* d); //編碼
void Decode(char* s, char* d); //解碼
~Huffman();
};
其中,HTree存儲Huffman樹的結構,HCodeTable存儲每個結點的編碼內容。(假設要編碼的數據和編碼結果均爲字符串類型)
1.5.2 Huffman樹的構造
構造Huffman樹的方法就是Huffman算法,描述如下:
將n個帶權值的結點構成n棵二叉樹的集合T,每棵二叉樹只有一個根結點,其左右子樹都爲空:
①在T中選取兩個根結點權值最小的二叉樹作爲左右子樹,構成一棵新二叉樹。其根結點的權值爲左右子樹根結點權值之和;
②在T中刪除這兩棵樹,將新樹加入T;
③重複①②操作,直到T中僅存一棵樹,該樹即Huffman樹
圖示如下:
生成這樣的Huffman樹的C++描述:
//輸入參數a[]存儲每種字符的權值,n爲字符的種類,name爲各個字符的內容
void Huffman::CreateHTree(int a[], int n, char name[])
{
N = n;
HCodeTable = new HCode[N];
HTree = new HNode[2 * n - 1]; //根據權重數組a[0..n-1]初始化Huffman樹
for (int i = 0; i < N; i++)
{
HTree[i].weight = a[i];
HTree[i].LChild = HTree[i].RChild = HTree[i].parent = -1;
HCodeTable[i].data = name[i];
}
int x, y;
for (int i = n; i < 2 * N - 1; i++) //開始建立Huffman樹
{
SelectMin(x, y, 0, i); //從1~i中選出兩個權值最小的結點的函數
HTree[x].parent = HTree[y].parent = i;
HTree[i].weight = HTree[x].weight + HTree[y].weight;
HTree[i].LChild = x;
HTree[i].RChild = y;
HTree[i].parent = -1;
}
}
1.5.3 Huffman編碼表的構建
本文采用向上而下遞歸的處理方式,對每一個結點進行編碼。若子樹的根結點是其父結點的左分支則編碼“0”,若是右分支則編碼“1”,遞歸處理直到葉子結點。
生成編碼表的C++描述如下:
void Huffman::code(int i, string newcode) //遞歸函數,對第i個結點編碼
{
if (HTree[i].LChild == -1)
{
HCodeTable[i].code = newcode;
return;
}
code(HTree[i].LChild, newcode + "0");
code(HTree[i].RChild, newcode + "1");
}
void Huffman::CreateCodeTable() //生成編碼表
{
code(2 * N - 2, "");
}
如下圖(a)所示的Huffman樹,其對應編碼表可以是下圖(b)的結構
1.5.4 Huffman編、解碼的實現
通常,對一段信息進行哈夫曼編碼時,需要對編碼的數據進行兩遍掃描.
(1)第一遍用來統計原數據中各字符出現的頻率,利用得到的頻率值創建哈夫曼樹,並要把樹的信息及編碼表保存起來,以便解壓時創建同樣的哈夫曼樹進行解壓;
(2)第二遍根據第一遍掃描得到的哈夫曼編碼表對原始數據進行編碼,並把編碼後得到的碼字存儲起來
1.編碼
生成編碼表後,對於要編碼的字符串,每讀出一個字符,只要在編碼表中找出對應編碼即可;
2.解碼
其基本思想是將編碼串從左到右逐位判別,直到確定一個字符。即從Huffman根節點開始,根據每一位是0或者1選擇左分支或右分支,直到到達葉子結點,至此每一個字符解碼結束。然後,再從根結點開始下一個字符的解碼
解碼算法的C++描述如下:
void Huffman::Decode(char* s, char* d) //s爲編碼串,d爲解碼後的字符串
{
while (*s != '\0')
{
int parent = 2 * N - 2; //根結點在HTree中的下標
while (HTree[parent].LChild != -1) //如果不是葉子結點
{
if (*s == '0')
parent = HTree[parent].LChild;
else
parent = HTree[parent].RChild;
s++;
}
*d = HCodeTable[parent].data;
d++;
}
}