哈夫曼樹(數據結構)
前言
哈夫曼樹是二叉樹的應用
一.幾個術語定義
1.路徑
由一結點到另一結點間的分支所構成。
(如1到4的路徑爲1到3,3到4這兩個分支構成)
2.路徑長度
路徑上的分支數目。
(如1到6的路徑長度爲3, 即1到3,3到5,5到6)
3.樹的外部路徑長度(EPL)
各葉結點(外結點)到根結點的路徑長度之和。
(如這棵樹的葉子結點爲2、4、8、7,2的路徑長度爲1,4的路徑長度是2,8的路徑長度爲4,7的路徑長度是3,所以樹的外部路徑長度爲1+2+4+3=10)
4.樹的內部路徑長度( IPL )
各非葉結點(內結點)到根結點的路徑長度之和。
5.樹的路徑長度(PL)
PL= EPL+ IPL
IPL = 0+1+2+3 = 6
EPL = 1+2+3+4 = 10
PL = 16
6.權值
爲樹的葉結點賦予一個權值,一般用於表示出現頻度、概率值等。
(就是用於表示葉結點的那個圓圈裏的樹)
7.擴充二叉樹
只有度爲 2 的內結點和度爲 0的外結點。
(沒有度爲1的結點的二叉樹叫做擴充二叉樹)
8.結點的帶權路徑長度
結點到根的路徑長度與結點上權的乘積 (wi*li)。
(結點的帶權路徑長度就是該結點權值與該結點的路徑長度的乘積
如5那個結點的帶權路經長度爲 5*3=15)
9.樹的帶權路徑長度(WPL)
(我們一般關注的是樹的帶權路徑長度)
樹中所有葉子結點的帶權路徑長度之和。
二.哈夫曼樹的定義
對於具有不同帶權路徑長度的擴充二叉樹:
(具有相同權值的葉子結點,它組成了不同的二叉樹,那麼這四個結點形成的二叉樹它的帶權路徑長度是不同的)
• 對於同樣一組權值,放在外結點上,組織方式不同,帶權路徑長度也不同;
• 帶權路徑長度最小的擴充二叉樹叫做哈夫曼樹;
• 哈夫曼樹中權值大的結點離根最近。
(權值最大的葉子結點越靠近根結點,權值最小的葉子結點越遠離根結點)
三.哈夫曼樹的構造
1.基本思想
使權大的結點靠近根
2.構造過程
例:
(1)以5個葉子結點爲例,我們把這個五個權值的結點作爲葉子,構成了一棵有五棵樹的森林。
(2)首先我們從這五棵樹的森林的集合中選取權值最小的兩棵樹,對它進行合併,2、4合併得到了6這個內結點,接下來把2,4刪掉,把6加入進這個集合,此時,這個森林中就包括4棵樹7、5、9、6。
(3)接下來,重複,從新的集合中找的最小的兩棵樹,一個是5,一個是6,在進行合併得到11,再把5、6從集合中刪除,把11加入,這個森林就包括7,9,11三棵樹。
(4)重複,仍然合併兩個最小的7、9得到16,把7、9刪除,插入16,這個森林包含兩棵樹11、16。
(5)同樣,把11和16合併得到27,把11和16刪除,再加入合併後的27,此時這個森林中只剩下一棵樹,這棵樹是一棵二叉樹,我們把這可樹叫做哈夫曼樹。
總結:
操作要點:對權值的合併、刪除與替換,總是合併當前值最小的兩個。
3.構造算法
前提:
性質:一棵有n個葉子結點的Huffman樹有 2*n-1 個結點(2*n-1包括葉子結點和內結點)
存儲定義:
① 採用順序存儲結構(一維數組)
② 結點類型定義
typedef struct //用順序存儲結構(一維數組)來描述結點的定義
{
int weght; //權值
int parent,lch,rch; //指向父結點,還有左孩子、右孩子標識的地址
}*HuffmanTree;
例:
設n=4, w={70,50,20,40},m=2*4-1=7
①進行存儲四個葉子結點,並且把雙親左孩子右孩子的初始化都是零
②從中選出兩個最小的合併,刪除,插入重讀操作,改變各結點雙親、左孩子、右孩子的值。
③
代碼描述:
(把創建哈夫曼樹的過程封裝爲函數)
typedef struct //用順序存儲結構(一維數組)來描述結點的定義
{
int weght; //權值
int parent,lch,rch; //指向父節點,還有左孩子、右孩子標識的地址
}*HuffmanTree;
void CreatHuffmanTree (HuffmanTree HT,int n)
{
if(n<=1) return; //葉子小於等於1,不合適,不是哈夫曼樹
m=2*n-1; //先算出m,m表示總的結點個數
HT=new HTNode[m+1]; //然後動態分配了m+1個結點
//爲什麼創建m+1個呢?因爲0號單元未用,HT[m]表示根結點,從1到m表示這m個結點
for(i=1;i<=m;++i) //對m個結點,對它的每一個左孩子右孩子雙親都初始化爲0
{
HT[i].lch=0;
HT[i].rch=0;
HT[i].parent=0;
}
for(i=1;i<=n;++i) //輸入所有葉子結點的權值
cin>>HT[i].weight;
for( i=n+1;i<=m;++i) //構造 Huffman樹:每次選兩個最小的,然後把它們的雙親、左孩子、右孩子改一下
{
Select(HT,i-1, s1, s2);
//每次(在這棵樹的葉子結點中)在HT[k](1≤k≤i-1)中選擇兩個其雙親域爲0,
// 並且權值最小的結點,
// 並返回它們在HT(數組)中的序號s1和s2
HT[s1].parent=i; HT[s2] .parent=i;
//表示從F中刪除s1,s2 (改s1的雙親,改s2的雙親)
HT[i].lch=s1; HT[i].rch=s2 ;
//s1,s2分別作爲i的左右孩子 (改新創建的第i個的左孩子、右孩子)
HT[i].weight=HT[s1].weight + HT[s2] .weight;
//i 的權值爲左右孩子權值之和 (它的權值是s1的權值加上s2的權值的和)
}
}
四.哈夫曼編碼
1.主要用途
實現數據壓縮
例:給出一段報文(報文就是在網絡上傳輸的一些數據),報文中只包含字符集合 { C, A, S, T,H },各個字符出現的頻度(次數)是 W={ 2, 7, 4, 5,9 }(也就是權值)。
✓若給每個字符以等長編碼(3位二進制)
(因爲一共有5個字符,五個字符用二進制編碼肯定是要用三位才能編碼的,因爲2的3次方是8,5是小於8的但5也是大於4的)
✓ C : 000 A : 001 S : 010 T : 011 H:100
✓則發送這27(因爲5個字符一共出現了27次)個字符時,總編碼長度爲 ( 2+7+4+5+9 ) * 3 = 81。
✓能否減少總編碼長度,使得發出同樣報文,可以用最少的二進制代碼?
↓↓↓(我們考慮用哈夫曼樹)
2.設計哈夫曼編碼
例:字符集合 { C, A, S, T,H },出現的頻度(次數)是 W={ 2, 7, 4, 5,9 }。
(1)基本思想:
概率大的字符用短碼,概率小的用長碼,構造哈夫曼樹,寫出哈夫曼編碼。
(2)構造Huffman樹
①字符出現的概率{ 2/27, 7/27, 4/27, 5/27,9/27 },化整爲 { 2, 7, 4, 5,9 }作爲葉結點上的權值, 建立Huffman樹。
② 建好以後,左分支賦 0,右分支賦 1,得Huffman編碼(變長編碼)。
③寫出哈夫曼編碼
(概率就是權值)
✓總編碼長度(7+5+9)*2+( 2+4 )*3 = 60
✓比等長編碼的情形(81)要短。
(所以就實現了數據的壓縮)
(3)注意:
Huffman編碼是一種前綴編碼
即:任一個二進制編碼不是其它二進制編碼的前綴。解碼時不會混淆。
3.哈夫曼編碼小結
(1)哈夫曼編碼是不等長編碼。
(2)哈夫曼編碼是前綴編碼,即任一字符的編碼都不是另一字符編碼的前綴。
(3)哈夫曼編碼樹中沒有度爲1的結點。若葉子結點的個數爲n,則哈夫曼編碼樹的結點總數爲 2n-1。
(4)發送過程:根據由哈夫曼樹得到的編碼表送出字符數據。
(5)接收過程:按左0、右1的規定,從根結點走到一個葉結點,完成一個字符的譯碼。反覆此過程,直到接收數據結束。