首先介绍什么是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) {}
};
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中的链表类<list>,但最后还是改成用我之前编的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;
}
};
#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;
}
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 ;
}
#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算法(也叫自适应哈夫曼编码)。动态huffman编码使用一棵动态变化的huffman树,对第n+1个字符的编码是根据原始数据中前n个字符得到的huffman树来进行的。编码和解码使用相同的初始huffman树,每处理完一个字符,编码和解码使用相同的方法修改树,所以没有必要为解码而保存树的信息。编码和解码一个字符所需的时间与该字符的编码长度成正比,所以这种编码可实时进行。但它比普通的huffman编码要复杂的许多,有兴趣的可参考有关数据结构与算法的书籍,或者看看这里。