數據結構與算法系列研究六——哈夫曼編碼與譯碼


哈夫曼編碼與譯碼

一、哈夫曼編碼定義

1.1、基本術語
  路徑:  從一結點到另一結點上的分支構成這兩個結點的路徑。
  路徑長度:  路徑上的分支數目。
  樹的路徑長度:  從根到所有結點的路徑長度之和。
  結點的帶權路徑長度:  從該結點到樹根之間的路徑長度與結點上權值的乘積。
  樹的帶權路徑長度:  樹中所有葉子結點的帶權路徑長度之和。


1.2、哈夫曼樹定義: 設有n 個權值 {w1,w2,......wn},試構造具有 n 個葉結點的二叉樹,每個葉結點權值為 wi ,則其中帶權路徑長度WPL最小的二叉樹稱為哈夫曼樹(最優二叉樹)。特點:權值越大的葉子離根越近。若葉結點上的權值均相同,則完全二叉樹一定是最優二叉樹,否則完全二叉樹不一定是最優二叉樹。

WPL=2*(7+5+2+4)=36
WPL=3*(7+5)+2*4+2=46
WPL=3*(2+4)+2*5+7=35

1.3、哈夫曼樹構造:
  (1) 根據給定的n個權值 {w1,w2,......wn}, 生成  n 棵二叉樹的集合F= {T1,T2,.......Tm};其中每棵二叉樹Ti只有一個帶權為Wi的根結點,左右子樹為空。
  (2) 在 F 中選擇兩棵根結點值最小的樹 Ti ,Tj 作為左右子樹,構成一棵新二叉樹Tk , Tk根結點值為Ti ,Tj根結點權值之和;
  (3) 在 F 中刪除Ti ,Tj ,並把 Tk  加到 F中;
  (4) 重復 (2) (3),直到 F中只含一棵樹。
   例:w={7,5,2,4}

1.4、哈夫曼編碼
   數據的壓縮過程稱為編碼,解壓過程稱為解碼。
     編碼:將文件中的字符轉換為唯一的一個二進制串。
     解碼:將一個二進制串轉換為對應的字符。
     定長編碼:設編碼字符個數為n,碼長為k,則k= 上界(log2^(n+1))
     不等長編碼:使出現頻率最多的字符采用盡可能短的編碼。
     前綴編碼:對不等長編碼,要求任一字符的編碼都不是另一個字符的編碼的前綴。
     采用二叉樹設計前綴編碼:用二叉樹的葉結點表示待編碼的字符,並約定左分支表示字符‘0’,右分支表示字符‘1’,則從根結點到葉子結點的路徑上分支字符組成的字符串作為該葉子結點的編碼。由此得到的編碼必為二進制的前綴編碼。
    方法:以n種字符出現的頻率作權,設計一棵哈夫曼樹,由此得到字符的二進制前綴編碼為總長最短的二進制前綴編碼,這種編碼即為哈夫曼編碼。

二、實驗實現

  2.1、實驗內容
    哈夫曼編碼生成與譯碼。
    輸入符號(序號用英文字母A, B, C, …表示)以及各符號出現概率,以字符串形式輸出各符號對應的二進制哈夫曼編碼。
    以字符串形式輸入接收到的比特序列,輸出譯碼后的符號序列。建議用菜單形式提供功能。
  2.2、輸入與輸出
    輸入:編碼的個數,編碼的權重,編碼的符號,接收到的比特序列。
    輸出:編碼結果和譯碼結果。
  2.3.關鍵數據結構與算法描述
    關鍵數據結構:霍夫曼樹的節點構造以及權重表,符號表,編碼表,實際符號數。由於采用數組的形式來存儲所有節點,則要提供左右子樹和雙親節點指針。

 1 typedef  char ElemType;  2 typedef struct {  3     int    parent;  //雙親下標
 4     int    lchild;  //左兒子下標
 5     int    rchild;  //右兒子下標
 6     double      w;  //結點權重
 7 }HF_BTNode;         //碼樹結點類型
 8 typedef struct
 9 { 10      int       n;            //實際符號數, n<=N
11      ElemType  s[N];         //符號表
12      double    weight[N];    //符號權重表
13      char      code[N][N+1]; //編碼表
14      HF_BTNode hf[2*N-1];    //碼樹
15 } HFT; //Huffman碼樹及碼表
View Code

   算法描述:
   首先是哈夫曼樹的生成需要根據相應的數據結構采用相應的算法。因為采用的是數組存儲樹的節點,屬於順序存儲結構。首先根據算法應該先找到所有根節點中最小的兩個組成一棵新樹的左右子樹,刪除這兩個節點(此處用parent為-1來說明為根節點,如不是-1,則為刪除),添加新生成的節點即讓新樹的根節點的parent為-1即可。因此根據思路可以遍歷要生成節點前的所有節點來不斷生成新樹,最后形成只有二度節點和零度節點的二叉樹。具體算法如下:

   int  m=2*a.n-1;  //所有節點數
   for(int i=0; i<m; i++)//對所有節點
 { a.hf[i].parent=a.hf[i].lchild=a.hf[i].rchild=-1;//-1代表着為根節點
 } for(i=0; i<a.n; i++) { a.hf[i].w=a.weight[i];  //權重賦值
 } /**********************生成霍夫曼樹***********************************/
   int j1,j2,j; for(i=a.n; i<m; i++) //從原有節點之后增添數值
 { for(j1=0; j1<i; j1++) //遍歷帶增添節點之前所有節點找到根節點
 { if(a.hf[j1].parent==-1) { break; } } for(j=j1+1; j<i; j++)//找到最小的根節點作為新增節點的左子樹
 { if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j1].w) { j1=j; } }//j1為最小節點
       a.hf[j1].parent=i; a.hf[i].lchild=j1; for(j2=0; j2<i; j2++) //同理找到次小節點,作為新增節點右子樹
 { if(a.hf[j2].parent==-1) { break; } } for(j=j2+1; j<i; j++) { if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j2].w) { j2=j;  //j2為次小節點
 } } a.hf[j2].parent=i; a.hf[i].rchild=j2; a.hf[i].w=a.hf[j1].w+a.hf[j2].w;//節點生成完成,權重賦值
   }
View Code

  然后,就要進行編碼了,對生成的霍夫曼樹約定左邊編1,右邊編0,只需從符號表亦即最原始節點開始追溯到根,即可的反序的編碼,然后將其翻轉即可。具體代碼如下:

/*********************生成code字符串數組*******************/
     for(i=0; i<a.n; i++) { int j=i; char *p=a.code[i],*q; //j從第i個葉子出發上溯至根結點,故編碼表一定和字符表對應
          while(a.hf[j].parent!=-1)//未到根節點時持續編號
 { int      child=j; j=a.hf[j].parent; if(a.hf[j].lchild==child) { *p++='1';//左邊是1
 } else { *p++='0';//右邊是0
 } } *p='\0'; q=a.code[i]; p--;        //因是從葉子到根故要字符串逆序
           char ch; while(q<p) { ch=*q; *q=*p; *p=ch; q++; p--; } }
View Code

  最后,就是譯碼過程了,根據二進制碼的字符串開始遍歷,根據霍夫曼樹從根出發按照1向左走,0向右走的規則,直至不能再走,則得到了想要的節點,該節點對應與符號表中一個數,故此唯一確定。如此循環往復,直至編碼被譯完。具體代碼如下:

while(receive[i]!='\0') { k=2*a.n-2; //k指向根結點,注意是從0開始
       while(k>=a.n&&(receive[i]!='\0')) { if(receive[i++]=='1') { k=a.hf[k].lchild; //向左走
 } else { k=a.hf[k].rchild; //向右走
 } } if(k<a.n) { decoded[j++]=a.s[k]; //根據對應關系輸出一個符號
 } } decoded[j]='\0';
View Code

  2.4、測試與理論
   理論:按理說輸入相應的編碼符號表,編碼數,編碼權重就會生成相應的霍夫曼樹,例如輸入編碼符號表為“abcde”,編碼數自然為5,編碼權重分別為0.1,0.2,0.3,0.3,0.1則應該生成相應的形如下面的霍夫曼樹:

則輸入序列”1110110001010001”對應與“baeccdc”,此處要注意權重相同時的編碼處理方法,誰先被發現,誰就是左子樹。
測試結果為:

選擇1之后:

選擇1后,再選擇2,輸入數據得到:

可見結果是正確的。

2.5、附錄(源代碼)

 1 #include "stdio.h"
 2 #include "stdlib.h"
 3 #include "iostream"
 4 using namespace std;  5 #define N   300     //最大允許符號數
 6 typedef  char ElemType;  7 typedef struct {  8     int    parent;  //雙親下標
 9     int    lchild;  //左兒子下標
 10     int    rchild;  //右兒子下標
 11     double      w;  //結點權重
 12 }HF_BTNode;         //碼樹結點類型
 13 typedef struct
 14 {  15      int       n;            //實際符號數, n<=N
 16      ElemType  s[N];         //符號表
 17      double    weight[N];    //符號權重表
 18      char      code[N][N+1]; //編碼表
 19      HF_BTNode hf[2*N-1];    //碼樹
 20 } HFT; //Huffman碼樹及碼表
 21 
 22 void createHF(HFT &a)  23 {  24    int  m=2*a.n-1;  //所有節點數
 25    for(int i=0; i<m; i++)//對所有節點
 26  {  27      a.hf[i].parent=a.hf[i].lchild=a.hf[i].rchild=-1;//-1代表着為根節點
 28  }  29    for(i=0; i<a.n; i++)  30  {  31        a.hf[i].w=a.weight[i];  //權重賦值
 32  }  33    /**********************生成霍夫曼樹***********************************/
 34    int j1,j2,j;  35    for(i=a.n; i<m; i++) //從原有節點之后增添數值
 36  {  37        for(j1=0; j1<i; j1++) //遍歷帶增添節點之前所有節點找到根節點
 38  {  39            if(a.hf[j1].parent==-1)  40  {  41               break;  42  }  43  }  44        for(j=j1+1; j<i; j++)//找到最小的根節點作為新增節點的左子樹
 45  {  46            if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j1].w)  47  {  48                 j1=j;  49  }  50        }//j1為最小節點
 51        a.hf[j1].parent=i;  52        a.hf[i].lchild=j1;  53        for(j2=0; j2<i; j2++) //同理找到次小節點,作為新增節點右子樹
 54  {  55          if(a.hf[j2].parent==-1)  56  {  57                    break;  58  }  59  }  60        for(j=j2+1; j<i; j++)  61  {  62           if(a.hf[j].parent==-1&&a.hf[j].w<a.hf[j2].w)  63  {  64              j2=j;  //j2為次小節點
 65  }  66  }  67         a.hf[j2].parent=i;  68         a.hf[i].rchild=j2;  69         a.hf[i].w=a.hf[j1].w+a.hf[j2].w;//節點生成完成,權重賦值
 70  }  71    /***********************樹生成完畢*********************/      
 72     /*********************生成code字符串數組*******************/
 73      for(i=0; i<a.n; i++)  74  {  75           int j=i;  76           char *p=a.code[i],*q; //j從第i個葉子出發上溯至根結點,故編碼表一定和字符表對應
 77           while(a.hf[j].parent!=-1)//未到根節點時持續編號
 78  {  79                  int      child=j;  80                 j=a.hf[j].parent;  81                 if(a.hf[j].lchild==child)  82  {  83                     *p++='1';//左邊是1
 84  }  85                 else
 86  {  87                     *p++='0';//右邊是0
 88  }  89  }  90            *p='\0';  91            q=a.code[i];  92            p--;        //因是從葉子到根故要字符串逆序
 93            char ch;  94            while(q<p)  95  {  96                ch=*q;  97                *q=*p;  98                *p=ch;  99                q++; 100                p--; 101  } 102            
103  } 104      //根據對應關系輸出編碼對應表
105      cout<<"the code corresponding's string:"<<endl; 106      for(i=0;i<a.n;i++) 107  { 108          cout<<a.s[i]<<":"<<a.code[i]<<endl; 109  } 110 } 111 /**************譯碼算法********************/
112 char * dec(HFT &a, char receive[]) 113 { 114    int i,j,k; 115    i=j=0;      //一定要初始化為0
116    char decoded[N]; 117     
118    while(receive[i]!='\0') 119  { 120        k=2*a.n-2; //k指向根結點,注意是從0開始
121        while(k>=a.n&&(receive[i]!='\0')) 122  { 123           if(receive[i++]=='1') 124  { 125              k=a.hf[k].lchild; //向左走
126  } 127           else
128  { 129             k=a.hf[k].rchild; //向右走
130  } 131  } 132        if(k<a.n) 133  { 134           decoded[j++]=a.s[k]; //根據對應關系輸出一個符號
135  } 136  } 137    decoded[j]='\0'; 138    return decoded; 139 } 140 void MainMenu( ) 141 { 142  HFT tree; 143     int i; 144     char choice,ch; 145     int s=0; 146     char InputCode[N],decode[N]; 147 start: 148     system("cls"); 149     cout<<"-------------------welcome----------------------"<<endl; 150     cout<<" 1.編碼 "<<endl; 151     cout<<" 2.譯碼 "<<endl; 152     cout<<" 3.退出 "<<endl; 153     cout<<"-------------------end--------------------------"<<endl; 154     cin>>choice; 155     switch(choice) 156  { 157     case '1': 158         system("cls"); 159         cout<<"plese input the code num:"<<endl; 160         cin>>tree.n; 161         cout<<"please input the code weight:"<<endl; 162         for(i=0;i<tree.n;i++) 163  { 164           cin>>tree.weight[i]; 165         
166  } 167         for(i=0;i<tree.n;i++) 168  { 169           s+=tree.weight[i]; 170  } 171 b:        cout<<"please input the needed string:"<<endl; 172         cin>>tree.s; 173         if(strlen(tree.s)!=tree.n) 174  { 175             goto b; 176  } 177  createHF(tree); 178         cout<<"|--------------------1.返回--------------------|"<<endl; 179         cout<<"|--------------------2.退出--------------------|"<<endl; 180         cin>>ch; 181         if(ch=='1') 182             goto start; 183         else
184             exit(0); 185 
186     case '2': 187         system("cls"); 188         cout<<"input the code:"<<endl; 189         cin>>InputCode; 190  strcpy(decode,dec(tree,InputCode)); 191         cout<<decode<<endl; 192         cout<<"|--------------------1.返回--------------------|"<<endl; 193         cout<<"|--------------------2.退出--------------------|"<<endl; 194         cin>>ch; 195         if(ch=='1') 196             goto start; 197         else
198             exit(0); 199     case '3': exit(0); 200     default: 201         cout<<"error"; 202         goto start; 203  } 204        
205 } 206 int main() 207 { 208  MainMenu( ); 209      return 0; 210 }
源代碼

    關於本系列已經是第六個了,看了一下有回饋的卻很少,可能是剛剛開始寫博客吧,很多地方還是有點生疏的,再加上臨近畢業答辯,抽出一點時間學點東西也是忙里偷閑的,寫的有不好之處請各位多多擔待,畢竟營造良好的網絡學習環境和氛圍是我們新一代人應該做的事情,這個時代講究的是信息的交流和共享,在這一點我一直在努力着,希望大家能錯我的文章中受到啟發,這樣也是非常好的一件事了,獨樂樂不如眾樂樂,就是這個道理了!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM