在传输文件的过程中我们经常会先对文件进行压缩,然后再传输。等接收到文件后,在进行解压缩。于是也打算自己动手写一个简单的文件压缩程序,大致分为三个过程,Huffman树的构建,利用哈夫曼树对输入的字符串进行压缩和解压缩处理,文件处理。今天,我们先来实现Huffman树的构建。在动手写代码之前我们先做一些准备工作。(这里不会详细介绍Huffman树,对Huffman基本概念不了解的同学请自行查阅其他资料)
一、Huffman树是什么
Huffman树又被称为“最优二叉树”。它是带权路径长度最短的树,权值较大的结点离根较近(这样子,出现频率较高的节点相应的编码长度就会越短)。
Huffman树的一个应用就是压缩文件。在计算机中字符所采用的编码是Ascii码,总共有八个bit位,可以表示256个字符。但是在实际生活中,我们的文本值用到这些字符当中的一小部分。那么此时这种编码方式就显得很浪费空间,传输起来肯定就比较慢。于是我们就想利用Huffman树重新对这些字符进行编码,以减少传输文本的大小,提高传输的速度。
二、构建思路(压缩文本以英文为例)
1.节点类需要包含哪些信息
A.数据(这里是字符);B.权值;C.左孩子节点;D.右孩子节点;E.Huffman编码
2.构建节点类
遍历带压缩的字符串,为出现的所有字符构建节点类,添加到节点数组nodes中;
3.构建树
遍历节点数组,每次都找出其中权值最小的两个节点,并为他们构建一个父节点,这个父节点的权值是这两个节点的权值之和,数据为空,左孩子是权值较小的那个节点,右孩子是权值较大的那个节点。终止条件是当nodes中只剩一个节点时。
4.获取字符编码
根据所构建的树,翻译出各个字符所对应的Huffman编码。在建树结束之后再去遍历整棵树,然后得到相应的编码。我这里的为每个节点新增了一个编码code,这个code被初始化为"”。这样我们在从根节点往下遍历时,遇到根节点的左孩子节点,就设置它的编码为其父节点的编码+“0”;右孩子节点,则设置它的编码为其父节点的编码+“1”,最后我们再把其中的叶节点的字符和编码添加到HashMap里面。
三、具体代码
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Queue;
//构造Huffman树
public class Huffman {
//构造节点类
public static class Node<E>{
E data;//数据,这里是字符
double weight;//权值
String code="";
Node leftChild;
Node rightChild;
public Node(E data,double weight) {
this.data=data;
this.weight=weight;
}
}
public static void main(String[] args) {
List<Node> nodes=new ArrayList<Node>();
//这里只能用泛型的类型数据,比如int只能用Integer,这里的char只能用Character来代替
HashMap<Character,String> map=new HashMap<Character,String>();
nodes.add(new Node('a',40.0));
nodes.add(new Node('b',8.0));
nodes.add(new Node('c',10.0));
nodes.add(new Node('d',30.0));
nodes.add(new Node('e',10.0));
nodes.add(new Node('f',2.0));
Node root=Huffman.creatTree(nodes);//调用建树函数
//打印出所有的节点
List<Node> list=new ArrayList<Node>();
list=Huffman.breadthFirst(root);
for(int i=0;i<list.size();i++) {
//判断字符是否存在,若存在则将其加入到Hashmap中
if(list.get(i).data!=null) {
map.put((Character) list.get(i).data, list.get(i).code);
}
}
System.out.println(map);
}
/*
* 构造哈夫曼树树
* 通过插入节点数组来建树
*/
//建树函数
public static Node creatTree(List<Node>nodes) {
//判断数组中是否还有两个以上的节点
while(nodes.size()>1) {
Node minleft=nodes.get(0);
int position=0;
for(int i=1;i<nodes.size();i++)
if(minleft.weight>nodes.get(i).weight) {
minleft=nodes.get(i);//找到最小值
position=i;
}
nodes.remove(position);//移除最小值
Node minright=nodes.get(0);
position=0;
for(int i=1;i<nodes.size();i++) {
if(minright.weight>nodes.get(i).weight) {
minright=nodes.get(i);//找到第二个最小值
position=i;
}
}
nodes.remove(position);//移除最小值
Node parent=new Node(null,minleft.weight+minright.weight);//构造父节点
//设置孩子节点的指针
parent.leftChild=minleft;
parent.rightChild=minright;
//把新建的父节点加到数组中
nodes.add(parent);
}
return nodes.get(0);
}
//从根节点往下走,获取各个叶子结点的编码
public static List<Node> breadthFirst(Node root){
Queue<Node> queue=new ArrayDeque<Node>();//创建一个队列
List<Node> list=new ArrayList<Node>();//创建一个数组
//如果根节点不为空,就将它加入队列
if(root!=null) {
queue.offer(root);
}
while(!queue.isEmpty()) {
//将队位元素加到list数组中
list.add(queue.peek());
/*
* java 堆栈中的方法poll和pop区别如下:
*pop:相当于get的操作,就是只是查看。从此列表所表示的堆栈处弹出一个元素。
*poll:相当于先get然后再remove掉,就是查看的同时,也将这个元素从容器中删除掉。 获取并移除此列表的头(第一个元素)
*/
Node p=queue.poll();
//如果左孩子节点不为null,则入队列
if(p.leftChild!=null) {
//左孩子节点的编码等于父节点+0
p.leftChild.code=p.code+"0";
queue.offer(p.leftChild);
}
//如果右孩子节点不为空,则右孩子节点入队列
if(p.rightChild!=null) {
//右孩子节点的编码等于父节点+1
p.rightChild.code=p.code+"1";
queue.offer(p.rightChild);
}
}
return list;
}
}
四、运行结果
五、总结反思
1.<>里面只能放泛型数据类型,而不能放具体的数据类型。比如int就只能用Integer,char只能用Character
2. java 堆栈中的方法poll和pop区别:
pop:相当于get的操作,就是只是查看。从此列表所表示的堆栈处弹出一个元素。
poll:相当于先get然后再remove掉,就是查看的同时,也将这个元素从容器中删除掉。 获取并移除此列表的头(第一个元素)
3.String最好初始为=“”;如果初始为null,后面如果要把这个字符串和其他字符串相连接,比如data=null,那么data+"12"就会变成“null12”
如何利用建好的Huffman树对字符进行压缩,可以看第二篇博客:文件压缩(二)——英文字符串的处理