使用java实现Huffman树

我们为什么需要Huffman树

目前,我们常用的图像、音频、视频等多媒体信息,信息量大,以传统方式存储无疑会占用大量存储空间,因此,我们引入了Huffman编码来解决这个问题。

什么是Huffman编码

Huffman编码是一种变长的编码方案,数据的编码因其使用频率不同而长短不一,使用频率高的数据其编码较短,使用频率低的数据其编码较长,从而使得所有数据的编码总长度最短。

如何实现Huffman编码

考虑实现Huffman编码的结构,我们决定使用二叉树来实现。

这里涉及到一个概念,二叉树的带权外路径长度:树中所有叶子的带权路径长度之和。而Huffman树则是二叉树的带权外路径长度最小的二叉树,也称最优二叉树。

构造Huffman树并得到Huffman编码

这里,我们使用静态三叉链表存储Huffman树。

这是静态三叉链表结点类TriElement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* @author cust
* 二叉树的静态三叉链表结点类
*/
public class TriElement{

int data; //数据域
int parent,left,right; //父母结点和左、右孩子结点下标

//不设置setter方法和getter方法,确保静态三叉链表结点只在具有权值时构造,且不可再被修改

//构造结点,各参数依次指定元素、父母结点下标、左/右孩子结点下标
public TriElement(int data, int parent, int left, int right)
{
this.data = data;
this.parent = parent;
this.left = left;
this.right = right;
}

public TriElement(int data) { //构造元素值为data、无父母的叶子结点
this(data, -1, -1, -1);
}

public String toString() { //返回结点的描述字符串
return "("+this.data+","+this.parent+","+this.left+","+this.right+")";
}

public boolean isLeaf() { //判断是否叶子结点
return this.left==-1 && this.right==-1;
}

public boolean equals(Object obj) { //比较是否相等 ,覆盖Object类的equals(obj)方法
//仅比较元素值
return obj==this || obj instanceof TriElement && this.data==((TriElement)obj).data;
}

}

这是Huffman树类HuffmanTree

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//哈夫曼树类
public class HuffmanTree{ //Huffman树类

private String charset; //字符集合
private TriElement[] huftree; //静态三叉链表结点数组
//用来存储数据

//不设置setter方法和getter方法,确保每一颗huffman树只在具有权值时构造,且不可再被修改

/**
* @param weights 权重数组
* 通过Huffman算法构建Huffman树
* 主要由三个步骤组成:
* 1.初始化
* 构造出wehights.length个结点的森林
* 2.合并二叉树
* 选择当前根结点权值最小的两颗二叉树合并,左孩子权值较小,合并后根结点的权值为其孩子结点的权值之和
* 3.重复2
* 只剩下最后一棵二叉树,这就是Huffman树
*/
//构造Huffman树,weights指定权值集合,数组长度为叶子结点数;默认字符集合从A开始
public HuffmanTree(int[] weights)
{
//准备部分
this.charset = "";
for (int i = 0; i < weights.length; i++) {
//将权值数组中的每一个数字转化为对应的字符
//默认字符集合是从'A'开始的weights.length个字符
// this.charset += (char)('A'+i);
if(i <= 25){
this.charset += (char)('A'+i);
}else if(i <= 51){
this.charset += (char)('a'+(i - 26));
}else if(i == 52){
this.charset += " ";
}

}
// System.out.println(charset);
// System.out.println(charset);
int n = weights.length; //叶子结点数
//叶子结点数与权值数组一一对应,每一个叶子都对应了一个字符的编码

//初始化
this.huftree = new TriElement[2*n-1]; //n个叶子的Huffman树共有2n-1个结点
//前n个存放叶子结点,后n-1个存放2度结点
for(int i=0; i<n; i++) //Huffman树初始化n个叶子结点
this.huftree[i] = new TriElement(weights[i]); //构造无父母的叶子结点

//合并二叉树
for(int i = n; i < 2*n-1; i++) { //构造n-1个2度结点
//当n-1个2度顶点构造完成时结束循环

int min1 = Integer.MAX_VALUE, min2 = min1; //最小和次小权值,初值为整数最大值
int x1=-1, x2=-1; //最小和次小权值结点下标
for (int j = 0; j < i; j++) { //寻找两个无父母的最小权值结点下标
//若该结点具有父母,说明该结点已经合并
if (this.huftree[j].parent == -1) //第j个结点无父母
//若能选出最小的,则用原本最小的记录,然后将原本记录最小的存储到次小的
if (this.huftree[j].data < min1) { //第j个结点权值最小
min2 = min1; //min2记得次小权值
x2 = x1; //x2记得次小权值结点下标
min1 = this.huftree[j].data; //min1记得最小权值
x1 = j; //x1记得最小权值结点下标
}else { //若能得到比次小小的,但比最小的大,则只需要更新次小的即可
if (this.huftree[j].data < min2) { //第j个结点权值次小
min2 = huftree[j].data; //记录次小权值
x2 = j; //记录次小的位置
}
}
}

this.huftree[x1].parent = i; //合并两棵权值最小的子树,左孩子最小
this.huftree[x2].parent = i;
this.huftree[i] = new TriElement(min1+min2, -1, x1, x2); //构造结点,指定值、父母、左右孩子
}

//Huffman树构造完毕
}

/**
* @param i 字符在charset中的位置
* @return 对应的描述字符串
* 通过访问Huffman树来实现
*/
private String getCode(int i) //返回charset第i个字符的Huffman编码字符串
{
int n = 59;
// int n = 8; //最多可以表示8个字符的huffman编码,这里可以修改为26
char hufcode[] = new char[n]; //声明字符数组暂存Huffman编码
int child = i, parent = this.huftree[child].parent; //获取这个元素的父节点
for (i = n-1; parent != -1; i--) //由叶结点向上直到根结点,反序存储编码
{
hufcode[i] = (huftree[parent].left==child) ? '0' : '1'; //左、右孩子编码为0、1
child = parent; //向上转移到根结点
parent = huftree[child].parent; //得到根结点
}
return new String(hufcode,i+1,n-1-i); //由hufcode数组从i+1开始的n-1-i个字符构造串
}

public String toString() //返回Huffman树的结点数组和所有字符的编码字符串
{
String str="Huffman树的结点数组:";
for (int i=0; i < this.huftree.length; i++)
str += this.huftree[i].toString()+",";
str = str.substring(0, str.length()-1);
str += "\nHuffman编码: ";
for (int i=0; i<this.charset.length(); i++) //输出所有叶子结点的Huffman编码
str+=this.charset.charAt(i)+":"+getCode(i)+",";
return str.substring(0, str.length()-1);
}

//数据压缩,将text各字符转换成Huffman编码存储,返回压缩字符串
public String encode(String text)
{
String compressed = ""; //被压缩的数据,以字符串显示
for (int i = 0; i < text.length(); i++) {
// compressed += getCode(text.charAt(i)-'A'); //默认字符集是从A开始的n个字符
if(text.charAt(i)-'A' >= 0 && text.charAt(i)-'A' <= 25){
compressed += getCode(text.charAt(i)-'A'); //默认字符集是从A开始的n个字符
}else if(text.charAt(i)-'a' >= 0 && text.charAt(i)-'a' <= 25){
compressed += getCode(text.charAt(i)-'a'+26); //默认字符集是从A开始的n个字符
}else{
compressed += getCode(52); //默认字符集是从A开始的n个字符
}
}

return compressed; //返回对应字符串
}

//数据解压缩,将压缩compressed中的0/1序列进行Huffman译码,返回译码字符串
public String decode(String compressed)
{
String text = ""; //结果字符串
int node = this.huftree.length-1; //node搜索一条从根到达叶子的路径
//huftree的最后一个元素是根结点
for (int i = 0; i < compressed.length(); i++) //将每一段数字转化成对应的英文字符
{
if (compressed.charAt(i) == '0') //根据0、1分别向左或右孩子走
node = huftree[node].left;
else
node = huftree[node].right;
if (huftree[node].isLeaf()) //到达叶子结点
{
text += this.charset.charAt(node); //获得一个字符
node = this.huftree.length-1; //node再从根结点开始
}
}
return text;
}

}

这是与Huffman树对应的字符映射类AsciiCodeMap,在Huffman树中指定的默认字符集则是来自于这里,此处我只设置了大小写英文字母和空格,实际上也可以继续添加,最好设置为ASCII的所有字符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.HashMap;
import java.util.Map;

//英文符号对应的映射
public class AsciiCodeMap {

public Map<Character, Integer> asciiCodeMap = new HashMap<Character, Integer>();

public AsciiCodeMap(){

for(int i = 0; i <= 25; i++){
asciiCodeMap.put((char)('A'+(i-26)), i);
}
for(int i = 26; i <= 51; i++){
asciiCodeMap.put((char)('a'+(i-26)), i);
}
asciiCodeMap.put(' ', 52);

}

}

这里是测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.Scanner;

//采用Huffman算法对字符串进行数据压缩和解压缩。
public class HuffmanTree_ex {

public static void main(String[] args)
{
// String text="AAAACFHHHFFFCEEEHEHCGBGAFFEHHHHHHHECCAEECDCEEEADC"
// + "CDAACCCCABHHAAGADDAAAFAHHHHAA"; //【例6.4】 数据
// int[] weight6_34={19, 2, 13, 5, 11, 7, 3, 17}; //6-34
// HuffmanTree huffman = new HuffmanTree(weight6_34); //构造Huffman树
// System.out.println(huffman.toString()); //输出Huffman树的结点数组和所有字符编码
// String compressed = huffman.encode(text);
// System.out.println("将"+text+"压缩为"+compressed+","+compressed.length()+"位");
// System.out.println("将"+compressed+"解码为"+huffman.decode(compressed));

//手动编码,测试
Scanner input = new Scanner(System.in);
String text2 = input.nextLine();
AsciiCodeMap a = new AsciiCodeMap();
// String text2="Hello world";
int[] weight = new int[59]; //6-34
for(int i = 0; i<weight.length; i++){
weight[i] = 0;
}
for(int i = 0; i<text2.length(); i++){
int m = a.asciiCodeMap.get(text2.charAt(i)); //找到对应的索引位置
weight[m]++;
// System.out.println(text2.charAt(i)+" "+ m);
}
HuffmanTree huffman2 = new HuffmanTree(weight); //构造Huffman树
System.out.println(huffman2.toString()); //输出Huffman树的结点数组和所有字符编码
String compressed2 = huffman2.encode(text2);
System.out.println("将"+text2+"压缩为"+compressed2+","+compressed2.length()+"位");
System.out.println("将"+compressed2+"解码为"+huffman2.decode(compressed2));
input.close();
}

}

总结

构建Huffman树,需要了解Huffman编码的基本原理:也就是出现次数多的字符的编码尽可能地短,这样就可以实现了我们所说的数据压缩。


使用java实现Huffman树
https://fulequn.github.io/2020/08/Article202008203/
作者
Fulequn
发布于
2020年8月20日
许可协议