Huffman树结构的实现
接下来我们首先讨论Huffman树的实现,然后讨论Huffman树用于Huffman编码的用法和性质等。
Huffman树是一种特殊的二叉树,本章前部分讨论了二叉树数据结构,这里我们首先分析Huffman树节点内容的数据结构。
从上面的示例中可以发现在建立的Huffman树的叶节点包含信息符和概率值,中间节点没有信息符,只有概率值,且其值为两个子节点的概率值之和。可以对叶节点和中间节点分别设计数据结构,更为有效的方法是设计统一的数据结构,并对定义中间节点的信息符为无效信息符“□”。
此外在构造Huffman树的过程中需要对各节点的顺序进行调整,这要求节点之间具有可比较性,比较的对象是节点的概率值,可以通过继承Comparable接口来实现。
根据上面的讨论设计节点内容类设计为:
/**
*
*
* 霍夫曼编码时,树节点的类型为字符串及其频率组成的对,
* HuffmanPair.java
*/
public class HuffmanPair implements Comparable{
// 两个私有成员
// 字符串
private String c;
// 字符串对应的频率
private double freq;
// 无参数构造函数
public HuffmanPair(){
c = "□";
freq = -1;
}
// 带参数构造函数
public HuffmanPair(String _c,
double _f){
c = _c;
freq = _f;
}
// 设置字符串成员变量
public void setChar(String _c){
c = _c;
}
// 设置字符串的频率变量
public void setFreq( double _f){
freq = _f;
}
// 获取字符串成员变量
public String getChar(){
return c;
}
// 获取字符串的频率
public double getFreq(){
return freq;
}
// 实现接口函数,对当前对象和另一个对象比较频率大小
public int compareTo(Object arg0) {
// 当前对象较小,返回-1
if( freq < ((HuffmanPair)(arg0)).freq)
return -1;
// 当前对象较大,返回1
else if(freq > ((HuffmanPair)(arg0)).freq)
return 1;
// 相等
else
return 0;
}
// 返回当前Huffman节点内容的字符串形式
public String toString(){
return "--" + c + " " + freq;
}
}
Huffman树的节点内容类即HuffmanPair类包含两个私有成员变量:字符串类型变量c表示信息符,双精度变量freq表示信息符的频率。无参数的构造函数将c赋值为无效信息符“□”。最后的函数compareTo实现Comparable的结构函数,两个HuffmanPair类的对象之间进行比较,比较的是它们的频率值大小,如果当前对象的频率值较大,则返回1,如果较小则返回-1,相等返回0。为了便于节点内容显示,类中包含了toString函数,同时返回信息符和信息符的频率值。
Huffman树HuffmanTree的节点采用前几节中设计的二叉树节点类LinkBinTreeNode的对象,在使用时节点元素项为HuffmanPair类的对象。其层次关系为:
LinkBinTreeNode
HuffmanPair
String
double
Huffman树的构造从LinkBinTreeNode类型的数组开始,每个LinkBinTreeNode类型的数组元素都包含的节点内容都是HuffmanPair类型的。设该数组为hufflist,其长度为n。
定义一个数组下标validpos,初始化指向数组hufflist的第一个位置,即validpos ← 0。在构造Huffman树的过程中,每次向Huffman树中添加一个节点,数组就减少一个节点,validpos用于指向每一次用于建树的节点。validpos初始化从0位置开始,第一次将数组的前两个节点组合后将得到的中间节点保存至位置1,即位置validpos+1,此时位置0处的节点变成无效节点。紧接着将validpos增加1,指向刚创建的中间节点。
由于该节点的概率值是原先两个节点的和,其概率值将可能比后面的节点的概率值大,该中间节点的位置可能需要做调整:从validpos+1位置开始寻找第一个比该概率值大的节点,然后进行元素位置的调整。不断地迭代这一过程直至validpos指向数组最后一个元素。
具体步骤如下:
1.validpos ← 0,将数组hufflist进行由小到大排序。
2.并将hufflist的validpos位置和validpos + 1位置上的两个节点,用lft和rght表示,进行合并:
首先,创建新的HuffmanPair类型的对象r,将位置为lft设置为r的左子节点内容,rght设置为r的右子节点内容;
然后,r的频率freq设置为lft和rght的freq的和,其信号符c设置为无效信号符“□”
最后,validpos ← validpos + 1,并将hufflist的validpos位置处元素重设为r;
3.调整hufflist的validpos处的元素的位置:
在hufflist中寻找第一个元素不小于hufflist[validpos]的位置idx;
将hufflist中validpos~idx-2位置上的元素整体右移一位,然后将数组原先idx-1位置上的元素赋值到validpos位置;
4.返回2继续迭代,直至validpos等于n-1为止。
以hufflist为如下情况时为例:
![](https://i-blog.csdnimg.cn/blog_migrate/f707fe7b881bb71b52b6d407f7da6a82.jpeg)
1.将hufflist中元素按由小到大排序,设 validpos ← 0:
![](https://i-blog.csdnimg.cn/blog_migrate/a6eec95ff2046ce713ab68838c5605d5.jpeg)
其中阴影位置为validpos;
2.将validpos=0位置和validpos+1=1位置上的两个节点合并,得到的中间节点,概率值为0.01+0.1=0.11,重新调整位置后,结果为:
![](https://i-blog.csdnimg.cn/blog_migrate/a064097c9e1258d3cac3f42e6152c41b.jpeg)
validpos ← validpos+1=1,事实上此时位置0上的元素已经无效了,用浅色表示;
3.将validpos=1位置和validpos+1=2位置上的两个节点合并,得到的中间节点,概率值为0.11+0.15=0.26,重新调整位置后,结果为:
![](https://i-blog.csdnimg.cn/blog_migrate/7dd6a4182630783b9ce9884b401b85dd.jpeg)
validpos ← validpos+1=2,可以发现,这一步中得到的概率值为0.26的中间节点的位置是经过调整的,从位置2调整至位置位置6;
4.将validpos=2位置和validpos+1=3位置上的两个节点合并,得到的中间节点,概率值为0.17+0.18=0.35,重新调整位置后,结果为:
![](https://i-blog.csdnimg.cn/blog_migrate/51221122530f3c1d6ebf2ebb49bf62f6.jpeg)
validpos ← validpos+1=3,可以发现,这一步中得到的概率值为0.35的中间节点的位置是经过调整的,从位置3调整至位置位置6;
5.将validpos=3位置和validpos+1=4位置上的两个节点合并,得到的中间节点,概率值为0.19+0.2=0.39,重新调整位置后,结果为:
![](https://i-blog.csdnimg.cn/blog_migrate/a6a3a4e96f1283ff5e6db57ae9fce3bb.jpeg)
validpos ← validpos+1=4,可以发现,这一步中得到的概率值为0.39的中间节点的位置是经过调整的,从位置4调整至位置位置6;
6.将validpos=4位置和validpos+1=5位置上的两个节点合并,得到的中间节点,概率值为0.26+0.35=0.61,重新调整位置后,结果为:
![](https://i-blog.csdnimg.cn/blog_migrate/0d1cac3834a8a3b4eb0c0379e9a1ee9f.jpeg)
validpos ← validpos+1=5,可以发现,这一步中得到的概率值为0.61的中间节点的位置是经过调整的,从位置5调整至位置位置6;
7.最后一次节点合并,最终得到hufflist为:
![](https://i-blog.csdnimg.cn/blog_migrate/4a01d67ec1117332d0a0c00ab44796e5.jpeg)
此时validpos等于n-1,结束迭代。
将hufflist数组最后一个元素,即hufflist[n-1]赋值给二叉树的根节点即完成Huffman树的构造过程。
import Element.ElemItem;
/**
*
*
* 霍夫曼树,主要成员变量为树的根节点,
* 主要成员函数为建树和树的打印
* HuffmanTree.java
*/
public class HuffmanTree {
// 私有成员变量
// 树根节点
private LinkBinTreeNode root;
// 无参数构造函数
public HuffmanTree(){
root = null;
}
// 有参数的构造函数
public HuffmanTree(LinkBinTreeNode r){
root = r;
}
// 建立huffman树,入参为链表
// 链表元素项为HuffmanPair
public void buildTree(LinkBinTreeNode hufflist[]){
// 首先对hufflist的元素项排序
// 数组长度
int n = hufflist.length;
// 数组中第一个有效节点的位置
int validpos = 0;
// 简单的选择排序
for( int i = 0; i < n; i++){
for( int j = i + 1; j < n; j++){
// 如果第i的频率大于第j的频率,则交换
if(((HuffmanPair)(
hufflist[i].getElem().getElem()
)).compareTo(
(HuffmanPair)(
hufflist[j].getElem().getElem()
)) == 1){
LinkBinTreeNode t = hufflist[i];
hufflist[i] = hufflist[j];
hufflist[j] = t;
}
}
} // 排序完成
// 开始建树
while(validpos != n -1){
// 新建节点用于表示两个子树的根节点
LinkBinTreeNode r
= new LinkBinTreeNode();
// 设置根节点r的左右子树
r.setLeft(hufflist[validpos]);
r.setRight(hufflist[validpos + 1]);
// 将左右子节点的频率相加
double _f =
((HuffmanPair)(
hufflist[validpos].getElem().getElem()
)).getFreq()
+ ((HuffmanPair)(
hufflist[validpos+1].getElem().getElem()
)).getFreq();
// 设置节点的频率,同时节点的字符用"□"表示
r.setElem( new ElemItem<HuffmanPair>(
new HuffmanPair("□", _f)));
// 有效位置向后移
validpos++;
// 重设validpos位置为新建的节点
hufflist[validpos] = r;
// 调整validpos节点的位置,
// 因为其频率值的大小不一定是最小的
int idx;
// validpos位置的huffmanPair
HuffmanPair tt1 =
(HuffmanPair)(
hufflist[validpos].getElem().getElem());
// 定位第一个比tt1的频率大的节点的位置idx
for(idx = validpos + 1; idx < n; idx++){
HuffmanPair tt2 =
(HuffmanPair)(
hufflist[idx].getElem().getElem());
// 找到第一个大于idx
if(tt1.compareTo(tt2) <= 0){
break;
}
}
// 将validpos节点缓存,
// 并将validpos~idx - 1huffmanPair一一前移
LinkBinTreeNode t = hufflist[validpos];
for( int i = validpos; i < idx - 1; i++){
hufflist[i] = hufflist[i + 1];
}
// 将t放到合适的位置
hufflist[idx - 1] = t;
}
// 最终得到的根节点就是hufflist的最后一个huffmanPair
root = hufflist[n - 1];
}
// 在高度h处打印一个节点n
protected void printnode(LinkBinTreeNode n, int h){
// 高度为h个制表位
for( int i = 0; i < h; i++){
System.out.print("\t");
}
// 获取节点的频率值的单精度形式
double _f = (
(HuffmanPair)(
n.getElem().getElem())).getFreq();
float f = ( float)(_f);
// 节点的字符串,区分叶节点和中间节点
String c;
// 叶节点时,c为节点的字符串值
if(n.isLeaf()){
c = ((HuffmanPair)(
n.getElem().getElem())).getChar();
}
// 中间节点时,c为空
else{
c = "□";
}
// 打印节点
System.out.println("--" + c + " " + f);
}
// 打印以节点r为根节点的huffman树,
// r的高度为h,调用节点打印函数,
// 本函数为递归函数
public void ShowHT(LinkBinTreeNode r, int h){
// 根为空,直接返回
if(r == null){
return;
}
// 递归调用,显示以r的右节点为根节点的树
// 高度为h+1
ShowHT(r.getRight(), h + 1);
// 打印r节点
printnode(r, h);
// 递归调用,显示以r的左节点为根节点的树
// 高度为h+1
ShowHT(r.getLeft(), h + 1);
}
// 打印huffman树,即打印以root为根节点的树
// 起始高度为0
public void printTree(){
ShowHT(root, 0);
}
// 测试前序、中序、后序和层序遍历
public void order_visit(){
LinkBinTree lbt = new LinkBinTree(root);
// 两种前序测试
System.out.println("\n递归算法实现前序遍历:");
lbt.rec_preorder(lbt.getRoot());
System.out.println("\n迭代算法实现前序遍历:");
lbt.itr_preorder(lbt.getRoot());
// 两种中序测试
System.out.println("\n递归算法实现中序遍历:");
lbt.rec_inorder(lbt.getRoot());
System.out.println("\n迭代算法实现中序遍历:");
lbt.itr_inorder(lbt.getRoot());
// 两种后序测试
System.out.println("\n递归算法实现后序遍历:");
lbt.rec_postorder(lbt.getRoot());
System.out.println("\n迭代算法实现后序遍历:");
lbt.itr_postoder(lbt.getRoot());
// 层序测试
System.out.println("\n迭代算法实现层序遍历:");
lbt.layer_order(lbt.getRoot());
}
}
在实现的Huffman树数据结构中,私有成员变量为二叉树节点类型的Huffman树根节点root。其中函数buildTree实现Huffman树的创建,函数参数为LinkBinTreeNode类型的数组。建树时首先将数组中所有节点按照信号符的频率值由小到大进行排序,这里使用简单的选择排序。然后迭代地进行节点组合和位置调整。
此外这里我们还采用多种方式来显示Huffman树中每个节点的信息。在前一章内容中我们讨论过广义树90度旋转后的显示方法,我们也讨论过堆结构90度旋转后的显示方式,这里我们也对Huffman树采用90度旋转后的显示方式。
函数ShowHT在高度为h处显示节点r的信息,该函数是一个递归函数。如果节点r为空则返回;否则,如果r的右子节点不为空,则首先递归地在高度为h+1处打印r的右子节点;然后在高度h处打印节点r,此时调用函数printnode;最后,如果r的左子节点不为空,则递归地在高度为h+1处打印r的左子节点。
前面我们也提到,Huffman树是一种特殊的二叉树,所以二叉树的所有遍历算法对Huffman树同样适用。这里我们对Huffman树也进行前序、中序、后序以及层序遍历,直接调用二叉树类的对应函数即可。