哈夫曼樹
定義:哈夫曼樹又稱最優樹,是一類帶權路徑最短的樹。
結點的帶權路徑:結點所代表的數乘以從根到改結點所經過的路(即改結點所在的深度-1)
樹的帶權路徑(WPL):就是指所有結點帶權路徑和的最小值
那到底什麼叫WPL呢?
舉個栗子:如圖所示的三顆二叉樹,都含有四個葉子結點a,b,c,d,權值分別爲7,5,2,4;那麼它們的WPL 分別爲多少呢?
1 2
3
很明顯 第三顆樹的WPL 的最小 也就是我們所求的哈夫曼樹
那麼哈夫曼樹到底該怎麼畫呢 ?
這裏爲了方便起見,我們先直接把權值代表該葉子結點 給你一組權值 2,4,5,3,該怎麼畫出哈夫曼樹呢?
第一步:這四個葉子初始時呢 是這個樣子滴 (先排個序
第二步: 我們找出權值最小的兩個結點2和3,然後將他們合併
重複第二步: 如果有重複 隨便選(所以最後的樹可能不一樣但是WPL相等) 那這裏我們選第二個5和4 合併
繼續重複 顯然 現在沒得選了 合併
這樣 一顆哈夫曼樹就創建成功了 WPL= 2*2+3*2+4*2+5*2=28
當然 如果在第二步合併的是第一個5和4 那麼哈夫曼樹爲
WPL =2*3+3*3+4*2+5*1=28
畫完哈夫曼樹之後 有沒有發現什麼規律?
1)在哈夫曼樹中,權值越大的結點距離根節點就越近。
2)哈夫曼樹是一顆只有度爲1和度爲2的二叉樹,葉子結點爲n個,那麼度爲2的結點有n-1個,整棵樹的結點就有2n-1個。
那麼 前面學習瞭如何畫出哈夫曼樹,但是如何用代碼實現呢?
第一步: 考慮哈夫曼樹的結點是什麼類型的呢?
由哈夫曼樹的特點我們知道結點數一共有2n-1個那麼我們可以用一維數組存儲這些結點,而對於每一個結點來說,分爲四部分,分別存放權值,雙親,左孩子和右孩子;
typedef struct{
int weight;
int parent,lchild,rchild;
}htnode,*nuffmantree;
第二步:創建哈夫曼樹
(1) 初始化
動態申請2n-1個空間,循環2n-1次將雙親,左孩子,右孩子置爲-1;循環n次,將權值賦給結點的weight;
(2)選取與合併
選出還沒有雙親的兩個權值最小的結點 ,將它們合併
(3)刪除與加入
將上一步選出了兩個結點的雙親置更新,新結點的左右孩子更新
(4)重複(2)和(3)n次
代碼實現
//構造哈夫曼樹
void createnuffmantree(nuffmantree &HT,int w[],int n){
//初始化
HT=new htnode[2*n-1]; //動態申請2n-1個空間
for(int i=0;i<2*n-1;i++){//雙親 ,左右孩子置-1
HT[i].parent=HT[i].lchild=HT[i].rchild=-1;
}
for(int i=0;i<n;i++){//初始化前n個的權值
HT[i].weight=w[i];
}
//選取與合併,刪除與加入
for(int k=n;k<2*n-1;k++){
int s1,s2;
Select(HT,k,s1,s2);//選取兩個權值最小值的位置
HT[s1].parent=k; //s1位置對應雙親爲i
HT[s2].parent=k; //s2位置對應雙親爲i
HT[k].lchild=s1; //更新左孩子權值
HT[k].rchild=s2; //更新右孩子權值
HT[k].weight=HT[s1].weight+HT[s2].weight; //更新自己的權值
}
}
Select函數 遍歷0~k-1的結點 找出還沒有雙親的權值最小的兩個結點的下標s1,s2
//Select 函數 找出沒有雙親結點的兩個位置
void Select(nuffmantree HT,int k,int &s1,int &s2){
int sw1=1e9,sw2=1e9;//初始化兩個最大權值
for(int i=0;i<k-1;i++){
if(HT[i].parent==-1&&HT[i].weight<sw1){//沒有雙親且對應下標權值小於sw1
s1=i;//確定下標
sw1=min(sw1,HT[i].weight);//更新sw1
}
}
for(int i=0;i<k;i++){
if(HT[i].parent==-1&&HT[i].weight<sw2&&HT[i].weight>sw1){//沒有雙親且對應下標權值小於sw2大於sw1
s2=i;//確定下標
sw2=min(sw2,HT[i].weight);//更新sw2
}
}
}
再來個栗子 權值 2 4 5 3
初始化 創建後
哈夫曼樹也創建出來了,那麼下一步就是編碼了
本來以爲編碼挺難的 其實寫出來 也不難嘛~
既然在上一步我們已經畫好了哈夫曼樹,現在只要對哈夫曼樹加一點點東西就好
我們把左枝置0,右枝置1(當然也可以倒過來 隨心所欲~)
對於上個例子 我們把 2 4 5 3 分別當做字母 A B C D的權值
結果如圖:
這樣就得到了 A B C D 的編碼 A:00 B:10 C:11 D:01
完整代碼實現:
#include<iostream>
#include<cstring>
using namespace std;
typedef struct{
int weight;
int parent,lchild,rchild;
}htnode,*nuffmantree;
int w[101],n;
//Select 函數 找出沒有雙親結點的兩個位置
void Select(nuffmantree HT,int k,int &s1,int &s2){
int sw1=1e9,sw2=1e9;//初始化兩個最大權值
for(int i=0;i<k-1;i++){
if(HT[i].parent==-1&&HT[i].weight<sw1){//沒有雙親且對應下標權值小於sw1
s1=i;//確定下標
sw1=min(sw1,HT[i].weight);//更新sw1
}
}
for(int i=0;i<k;i++){
if(HT[i].parent==-1&&HT[i].weight<sw2&&HT[i].weight>sw1){//沒有雙親且對應下標權值小於sw2大於sw1
s2=i;//確定下標
sw2=min(sw2,HT[i].weight);//更新sw2
}
}
}
//構造哈夫曼樹
void createnuffmantree(nuffmantree &HT,int w[],int n){
//初始化
HT=new htnode[2*n-1]; //動態申請2n-1個空間
for(int i=0;i<2*n-1;i++){//雙親 ,左右孩子置-1
HT[i].parent=HT[i].lchild=HT[i].rchild=-1;
}
for(int i=0;i<n;i++){//初始化前n個的權值
HT[i].weight=w[i];
}
//選取與合併,刪除與加入
for(int k=n;k<2*n-1;k++){
int s1,s2;
Select(HT,k,s1,s2);//選取兩個權值最小值的位置
HT[s1].parent=k; //s1位置對應雙親爲i
HT[s2].parent=k; //s2位置對應雙親爲i
HT[k].lchild=s1; //更新左孩子權值
HT[k].rchild=s2; //更新右孩子權值
HT[k].weight=HT[s1].weight+HT[s2].weight; //更新自己的權值
}
}
//哈夫曼編碼
void creathuffmancode(nuffmantree HT,char ** &HC,int n){
HC=new char *[n]; //可以將HC理解爲二維的char[][]數組
char *cd=new char[n];//cd 爲一維的char[]數組
cd[n-1]='\0';
for(int i=0;i<n;i++){
int start=n-1;
int c=i;//定位第幾個字符
int f=HT[i].parent;//第i個位置的雙親
while(f!=-1){//由葉子向雙親遍歷
start--;
if(HT[f].lchild==c) cd[start]='0';//左孩子爲0
else cd[start]='1';//右孩子爲1
c=f;f=HT[f].parent;//c記錄當前位置,更新f
}
HC[i]=new char [n-start];
strcpy(HC[i],&cd[start]);//將cd[]內容複製給HC[i]
}
delete cd;
}
int main(){
nuffmantree HT;
char **HC;
cout<<"請輸入字符個數:";
cin>>n;
char a[256];
cout<<"請分別輸入字符及對應的權值:"<<endl;
for(int i=0;i<n;i++)
cin>>a[i]>>w[i];
createnuffmantree(HT,w,n);//建樹
creathuffmancode(HT,HC,n);//編碼
for(int i=0;i<n;i++){//輸出
cout<<a[i]<<": ";
for(int j=0;j<strlen(HC[i]);j++)
cout<<HC[i][j];
cout<<endl;
}
return 0;
}