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