数据结构学习笔记--Huffman树

 
首先介绍什么是Huffman树(译作哈夫曼树或霍夫曼树)。huffman树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶子结点的权值(人为规定)乘上其到根结点的路径长度。树的带权路径长度记为WPL,N个权值Wi(i=1,2,...n)构成一棵有N个叶子结点的二叉树,而huffman树的WPL是最小的。
Huffman 树的一个主要应用是huffman编码,David Huffman在上世纪五十年代初提出了这种编码。根据字符出现的概率来构造平均长度最短的编码。各码字(即为符号经哈夫曼编码后得到的编码)长度严格按照码字所对应符号出现概率的大小的逆序排列。它是一种变长的编码。
为什么huffman编码是前缀编码呢?其实证明很简单,因为所有编码字符都位于huffman树的叶子节点上,任意叶子节点均不是其他叶子节点的祖先,所以对应的编码也就不是其他结点编码的前缀了。
根据选择的存储方式的不同,构造huffman树的算法有许多种(我就找到了8种,汗~)。书上这里使用的是顺序存储的方式,每个结点附有双亲指针,其效率要比使用二叉链表高一些。这里我使用的是二叉链表,虽然它效率很低(可能是是最差的),但站在学习的角度上看它不失为一种好方法:简洁和易于理解。
这样huffman树就与之前介绍的二叉表达式树很像了。首先给是结点定义:
 
class  HCNode {
public :
    
int  index;
    
int  weight;
    HCNode
*  left;
    HCNode
*  right;
    HCNode(
int  wgt,  int  n, HCNode *  lef  =  NULL, HCNode *  rgt  =  NULL) 
        : weight(wgt), index(n), left(lef), right(rgt) {}
};
 
这里假设是对26个英文字符进行编码。在构造huffman树时每个待编码字符在树中的位置会被打乱,需要一个整型量记录其在权值数组中的位序。
有了结点定义后下面该设法构造huffman树了,建树时需要一个一维整型数组(由用户给出),其中记录了每一个字符的权值。按照huffman的方法建树过程是这样的。
一、对给定的n个权值构成n棵二叉树的初始集合F={T1,T2,T3,...,Ti,...,Tn},其中每棵二叉树Ti中只有一个权值为Wi的根结点,它的左右子树均为空。
二、在F中选取两棵根结点权值最小的树作为新构造的二叉树的左右子树,新二叉树的根结点的权值为其左右子树的根结点的权值之和。
三、从F中删除这两棵树,并把这棵新构造的二叉树加入到集合F中。
四、重复二和三两步,直到集合F中只有一棵二叉树为止,即为构造好的huffman树。
我们可以使用一个线性链表来存放每棵树的根结点指针。这样一来,每次从链表中摘下两个权值最小的结点,新建一个根结点,并指向它们,再把新建的结点指针插入链表尾部。反复这样进行直到链表内只剩一个结点时结束。
在选择链表时有点犹豫,刚开始用的是STL中的链表类,但最后还是改成用我之前编的LinkList了。因为其中一些函数确实能让这个程序简单不少。还是先把代码贴上来:
 
#include  " ../../线性表(链式存储)/LinkList.h "
#include 
" ../../线性表(链式存储)/Node.h "
#include 
" ../../require.h "
// 省略若干...
class  HuffmanCode {
    LinkList
< HCNode *>  m_nodeList;
    
string *  m_ptrCode;
    
int  m_nNum;  // 编码的字符个数
    
// 生成huffman树
     void  createTree( int   * w,  int  n);
    
// 先序遍历huffman树
     void  preOrder(HCNode *  cur,  long  code  =   1 );
    
// 选择权值最小的树根结点,并从链表中剔除
    HCNode *  getLightestNode();
    
void  destroy(HCNode *  cur) {
        
if  (cur  !=  NULL) {
            destroy(cur
-> left);
            destroy(cur
-> right);
            delete cur;
        }
    }
public :
    HuffmanCode(
int *  weight,  int  n) { 
        m_ptrCode 
=   new   string [n];
        m_nNum 
=  n;
        createTree(weight, n); 
    } 
    
~ HuffmanCode() { destroy(m_nodeList.GetHeadElem()); }
    
// 编码一串字符
     const   string  Coding( const   string  str);
    
// 解码
     const   string  translate( const   string  str);
    
void  output() {
        
for  ( int  i  =   0 ; i  <  m_nNum; i ++ )
            cout 
<<   char (i  +   ' A ' <<   " "   <<  m_ptrCode[i]  <<  endl;
    }
};
 
m_nodeList 存放树的根节点指针,m_ptrCode存放每个字符的编码,由构造函数中调用createTree构建huffman树。Coding与translate提供编码与解码的实现。下面是函数实现:
 
void  HuffmanCode::createTree( int *  w,  int  n) {
    
for  ( int  i  =   0 ; i  <  n; i ++ )
        m_nodeList.InsertLast(
new  HCNode(w[i], i  +   1 ));
    
while  (m_nodeList.GetLength()  >   1 ) {
        
// 选择权最小的两个结点
        HCNode *  pNode1  =  getLightestNode(); 
        HCNode
*  pNode2  =  getLightestNode(); 
        
// 创建一棵新树,链入链表尾部
        m_nodeList.InsertLast( new  HCNode(pNode1 -> weight
            
+  pNode2 -> weight,  0 , pNode1, pNode2));
    }
    
// 为叶子结点编码
    preOrder(m_nodeList.GetHeadElem());
}
void  HuffmanCode::preOrder(HCNode *  cur,  long  code) {
    
// 叶子结点编码
     if  (cur -> index  !=   0 ) {
        
string  strTemp;
        
long  c  =  code;
        
while  (c  !=   1 ) {
            strTemp 
+=  c  &   1   ?   " 1 "  :  " 0 " ;
            c 
>>=   1 ;
        }
        
for  ( int  i  =  strTemp.length()  -   1 ; i  >=   0 ; i -- )
            m_ptrCode[cur
-> index  -   1 +=  strTemp[i];
        
return ;
    }
    
// 左分枝加0
    preOrder(cur -> left, code  <<   1 );
    
// 右分枝加1
    preOrder(cur -> right, (code  <<   1 +   1 );
}
// 选择权值最小的树根结点,并从链表中剔除
HCNode *  HuffmanCode::getLightestNode() {
    
int  pos  =   1 ;
    
for  ( int  i  =   2 ; i  <=  m_nodeList.GetLength(); i ++ )
        
if  (m_nodeList.GetNode(i) -> data -> weight  <  
            m_nodeList.GetNode(pos)
-> data -> weight)
            pos 
=  i;
    
return  m_nodeList.DeleteAt(pos);
}
const   string  HuffmanCode::Coding( const   string  str) {
    
string  strCode;
    
for  (unsigned  int  i  =   0 ; i  <  str.length(); i ++ )
        strCode 
+=  m_ptrCode[str[i]  -   ' A ' ];
    
return  strCode;
}
const   string  HuffmanCode::translate( const   string  str) {
    
string  strText;
    HCNode
*  root  =  m_nodeList.GetHeadElem();
    HCNode
*  cur  =  root;
    
for  ( int  i  =   0 ; i  <  str.length(); i ++ ) {
        cur 
=  str[i]  ==   ' 0 '   ?  cur -> left : cur -> right;
        
if  (cur -> index) {
            strText 
+=   char (cur -> index  +   ' A '   -   1 );
            cur 
=  root;
        }
    }
    
return  strText;
}
 
LinkList 内有几个函数需要说明一下:GetHeadElem取链表第一个结点的数据,GetNode(i)取链表中第i个结点的指针,DeleteAt(i)删除链表中的第i个结点,并返回结点数据。在建好树之后进行先序遍历,为每一个叶子节点编码,并存于m_prtCode中。
使用二叉链表来实现huffman编码很简单,只要注意编码时需要规定树的左枝与右枝谁记录0与1就可以了。下面是测试:
 
#include  < sstream >
#include 
" HuffCode.h "
int  main() {
    stringstream input(
" 4 10 20 30 40 ABCCADA " );
    
int   * weight;
    
int  n;
    cout 
<<   " 输入编码字符个数: "   <<  endl;
    input 
>>  n;
    weight 
=   new   int [n];
    
for  ( int  i  =   0 ; i  <  n; i ++ ) {
        cout 
<<   " "   <<  i  +   1   <<   " 个字符的权值: "   <<  endl;
        input 
>>  weight[i];
    }
    HuffmanCode huff(weight, n);
    cout 
<<   " 建立huffman树: "   <<  endl;
    huff.output();
    cout 
<<   " 输入待编码字符串: "   <<  endl;
    
string  text;
    input 
>>  text;
    
string  code  =  huff.Coding(text);
    cout 
<<   " 编码: "   <<  text  <<   "  -->  "   <<  code  <<  endl;
    cout 
<<   " 解码: "   <<  code  <<   "  -->  "  
        
<<  huff.translate(code)  <<  endl;
    
// 100010101101101000001000
     return   0 ;
}
 
最终输出:
 

数据结构学习笔记--Huffman树_第1张图片

 

在实际编码时,每个字符的权值需要整个扫描一遍文件才能确定,这样算法需要扫描两次文件,效率比较低。所以后来又提出了一种动态huffman算法(也叫自适应哈夫曼编码)。动态huffman编码使用一棵动态变化的huffman树,对第n+1个字符的编码是根据原始数据中前n个字符得到的huffman树来进行的。编码和解码使用相同的初始huffman树,每处理完一个字符,编码和解码使用相同的方法修改树,所以没有必要为解码而保存树的信息。编码和解码一个字符所需的时间与该字符的编码长度成正比,所以这种编码可实时进行。但它比普通的huffman编码要复杂的许多,有兴趣的可参考有关数据结构与算法的书籍,或者看看这里。
 

你可能感兴趣的:(数据结构学习笔记)