java数据结构课程设计(哈夫曼树压缩实现)
开源作者:fntp
QQ358566760欢迎读者与笔者交流沟通
使用java数据结构与算法,来实现哈夫曼树压缩文本数据,如何实现呢?
哈夫曼树简单介绍:
在计算机数据处理中,哈夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。
例如,在英文中,e的出现机率最高,而z的出现概率则最低。当利用哈夫曼编码对一篇英文进行压缩时,e极有可能用一个比特来表示,而z则可能花去25个比特(不是26)。用普通的表示方法时,每个英文字母均占用一个字节,即8个比特。二者相比,e使用了一般编码的1/8的长度,z则使用了3倍多。倘若我们能实现对于英文中各个字母出现概率的较准确的估算,就可以大幅度提高无损压缩的比例。
哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。树的路径长度是从树根到每一结点的路径长度之和,记为WPL=(W1L1+W2L2+W3L3+…+WnLn),N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。可以证明哈夫曼树的WPL是最小的。
哈夫曼编码有静态哈夫曼编码与动态哈夫曼编码之分:
在数据通信中,需要将传送的文字转换成二进制的字符串,用0,1码的不同排列来表示字符。例如,需传送的报文为“AFTER DATA EAR ARE ART AREA”,这里用到的字符集为“A,E,R,T,F,D”,各字母出现的次数为{8,4,5,3,1,1}。现要求为这些字母设计编码。要区别6个字母,最简单的二进制编码方式是等长编码,固定采用3位二进制,可分别用000、001、010、011、100、101对“A,E,R,T,F,D”进行编码发送,当对方接收报文时再按照三位一分进行译码。显然编码的长度取决报文中不同字符的个数。若报文中可能出现26个不同字符,则固定编码长度为5。然而,传送报文时总是希望总长度尽可能短。在实际应用中,各个字符的出现频度或使用次数是不相同的,如A、B、C的使用频率远远高于X、Y、Z,自然会想到设计编码时,让使用频率高的用短码,使用频率低的用长码,以优化整个报文编码。
为使不等长编码为前缀编码(即要求一个字符的编码不能是另一个字符编码的前缀),可用字符集中的每个字符作为叶子结点生成一棵编码二叉树,为了获得传送报文的最短长度,可将每个字符的出现频率作为字符结点的权值赋予该结点上,显然字使用频率越小权值越小,权值越小叶子就越靠下,于是频率小编码长,频率高编码短,这样就保证了此树的最小带权路径长度效果上就是传送报文的最短长度。因此,求传送报文的最短长度问题转化为求由字符集中的所有字符作为叶子结点,由字符出现频率作为其权值所产生的哈夫曼树的问题。利用哈夫曼树来设计二进制的前缀编码,既满足前缀编码的条件,又保证报文编码总长最短。
问题来了?那什么是静态哈夫曼编码呢?
静态哈夫曼编码:
它对需要编码的数据进行两遍扫描:第一遍统计原数据中各字符出现的频率,利用得到的频率值创建哈夫曼树,并必须把树的信息保存起来,即把字符0-255(28=256)的频率值以2-4BYTES的长度顺序存储起来,(用4Bytes的长度存储频率值,频率值的表示范围为0–232-1,这已足够表示大文件中字符出现的频率了)以便解压时创建同样的哈夫曼树进行解压;第二遍则根据第一遍扫描得到的哈夫曼树进行编码,并把编码后得到的码字存储起来。
动态哈夫曼编码:
动态哈夫曼编码使用一棵动态变化的哈夫曼树,对第t+1个字符的编码是根据原始数据中前t个字符得到的哈夫曼树来进行的,编码和解码使用相同的初始哈夫曼树,每处理完一个字符,编码和解码使用相同的方法修改哈夫曼树,所以没有必要为解码而保存哈夫曼树的信息。编码和解码一个字符所需的时间与该字符的编码长度成正比,所以动态哈夫曼编码可实时进行。
=========================================================================================================
那,如何构造哈夫曼树呢?
构造方法
假设有n个权值,则构造出的哈夫曼树有n个叶子结点。 n个权值分别设为 w1、w2、…、wn,则哈夫曼树的构造规则为:
(1) 将w1、w2、…,wn看成是有n 棵树的森林(每棵树仅有一个结点);
(2) 在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。
(见例图)
java代码实现哈夫曼算法:
package hh;
//此节点类用于演示哈夫曼树编码
public class Node {
public char aChar;
public int frequency;
public Node leftNode; //<key
public Node rightNode; //>=key
public Node(char ch,int frequency) {
this.aChar = ch;
this.frequency = frequency;
}
public void display() {
System.out.print( aChar + " ");
}
}
package hh;
import java.util.Comparator;
import java.util.HashMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
//哈夫曼树 演示
public class Huffman {
private ConcurrentHashMap<Character, String> codeTabe = new ConcurrentHashMap<>();
private Node root;
//创建编码表 通常是对一次数据编码时要生成这个表,然后解编时再通过这个表来解码
public void createCodeTable(String str) {
//统计每个字符出现的次数并排序
TreeSet<Node> treeSet = new TreeSet<>(new Comparator<Node>() {
@Override
public int compare(Node o1, Node o2) {
int cmp = o1.frequency - o2.frequency;
return cmp == 0 ? -1 : cmp;
}
});
HashMap<Character, Integer> hashMap = new HashMap<Character, Integer>();
int count = 0;
char ch;
for (int i = 0; i < str.length(); i++) {
ch = str.charAt(i);
if (hashMap.containsKey(ch)) {
hashMap.put(ch, hashMap.get(ch) + 1);
} else {
hashMap.put(ch, 1);
}
}
//排序
for (char key : hashMap.keySet()) {
treeSet.add(new Node(key, hashMap.get(key)));
}
//开始创建哈夫曼树
Node firstNode = treeSet.pollFirst();
Node secondNode;
Node newNode;
while ((secondNode = treeSet.pollFirst()) != null) { //一个个移除,不为空时
newNode = new Node(' ', firstNode.frequency + secondNode.frequency);
newNode.leftNode = firstNode;
newNode.rightNode = secondNode;
treeSet.add(newNode);
firstNode = treeSet.pollFirst();
}
root= firstNode;// 记录好树的根节点
//通过遍历树去创建一个字符对应的字典,方便编码时直接取码
bianli1(firstNode, new StringBuilder(100));
}
//遍历哈夫曼树
private void bianli1(Node node,StringBuilder sb) {
if (node != null) {
if (node.leftNode == null) { //实际上这个时候左右子节点都为空了
codeTabe.put(node.aChar, sb.toString());
return;
} else {
sb.append('0');
bianli1(node.leftNode, sb);
//移除当前这个结点的后加的编码
sb.setLength(sb.length() - 1);
sb.append('1');
bianli1(node.rightNode, sb);
//移除当前这个结点的后加的编码
sb.setLength(sb.length() - 1);
}
}
}
public String enCode(String str) throws Exception {
StringBuilder sb = new StringBuilder(100);
String hafumanCode;
for (int i = 0; i < str.length(); i++) {
if (!codeTabe.containsKey(str.charAt(i))) {
throw new Exception("can not find the coding of the char:" + str.charAt(i));
}
sb.append(codeTabe.get(str.charAt(i))+",");
}
return sb.toString();
}
//解码
public String deCode(String hafuman) throws Exception {
StringBuilder sb = new StringBuilder(100);
Node firstNode = this.root;
if(firstNode.leftNode==null || firstNode.rightNode==null){
return String.valueOf(firstNode.aChar);
}
for(int i=0;i<hafuman.length();i++){
if(hafuman.charAt(i)=='0'){
firstNode = firstNode.leftNode;
}
else if(hafuman.charAt(i)=='1'){
firstNode = firstNode.rightNode;
}
else{
// throw new Exception("checked the invalidate char, must be '1' or '0' !");
continue;
}
if(firstNode.leftNode==null){ //找到了原始编码
sb.append(firstNode.aChar);
//找到后一次搜索应该继续从根开始
firstNode = this.root;
}
}
return sb.toString();
}
}
package hh;
import java.awt.*;
import java.awt.Toolkit;
//Toolkit设置图像
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
import hh.*;
public class xinraunyasuo {
static Huffman yuHuffman= new Huffman();
static String coding;
Cache scx= new Cache();
public static void main(String[] args) {
//创建窗体对象
JFrame jf = new JFrame("鑫软压缩");
Toolkit t1 = Toolkit.getDefaultToolkit();
String ss = "C:\\Users\\Administrator\\Desktop\\yu\\src\\img\\05.png";
Image image = t1.getImage(ss);
jf.setIconImage(image);
jf.setVisible(true);
//设置大小
jf.setSize(323, 506);
//设置居中
jf.setLocationRelativeTo(null);
//设置布局
jf.setLayout(new FlowLayout());
//设置关闭事件
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//添加label
Label yasuoneirong = new Label("运行内存");
Label runtime = new Label("运行时间");
Label runrom = new Label("压缩内容");
//添加文本框
final TextField neirongkuang = new TextField(30);
neirongkuang.setColumns(30);
final TextField runshijian = new TextField(30);
runshijian.setColumns(30);
final TextField yunxingneicun = new TextField(30);
yunxingneicun.setColumns(30);
//添加按钮
Button confirm = new Button("哈夫曼压缩");
Button jiema=new Button("哈夫曼解压");
Button cancel = new Button("取消");
JTextArea jta = new JTextArea(10, 15);
jta.setTabSize(4);
jta.setFont(new Font("楷体", Font.BOLD, 16));
jta.setLineWrap(true);// 激活自动换行功能
jta.setWrapStyleWord(true);// 激活断行不断字功能
jta.setBackground(Color.CYAN);
JScrollPane jscrollPane = new JScrollPane(jta);
JPanel jpanel = new JPanel();
jpanel.setLayout(new GridLayout(1, 3));
jpanel.add(jscrollPane);
// ====================================
//添加程序主页背景
String url = "C:/Users/Administrator/Desktop/yu/src/img/02.png";
ImageIcon img = new ImageIcon(url);
JLabel label = new JLabel(img);
jf.add(label);
jf.add(yasuoneirong);
jf.add(yunxingneicun);
jf.add(runtime);
jf.add(runshijian);
jf.add(runrom);
jf.add(neirongkuang);
jf.add(confirm);
jf.add(jiema);
jf.add(cancel);
jf.add(jpanel);
neirongkuang.setText("在这里输入字符串!");
// "程序运行时间: "+(endTime - startTime)+"ms"
//添加事件
jta.setText("欢迎使用鑫软哈夫曼压缩测试工具,"
+ "请在对应的文本输入区域输入您要压缩的内容!"
+ "然后点击哈夫曼压缩,本文本框会显示所有压缩内容!"
+ "如需要解码,只需要直接点击哈夫曼解码,就可以"
+ "一键哈夫曼解码数据流!鑫软出品,qq358566760!");
confirm.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
String neirong=null;
long Beginningtime=System.currentTimeMillis();
try {
// yuHuffman.createCodeTable(neirongkuang.getText());
yuHuffman.createCodeTable(neirong);
coding = yuHuffman.enCode(neirong);
} catch (Exception ex) {
ex.printStackTrace();
}
long endTime=System.currentTimeMillis();
jta.setText("哈夫曼编码:"+coding); //哈夫曼编码
yunxingneicun.setText("虚拟机剩余内存:"+Cache.cache()+"消耗内存:"+Runtime.getRuntime().freeMemory()/1024/1024+"M");
runshijian.setText("程序运行时间: "+(endTime - Beginningtime)+"ms");
}
});
cancel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
System.exit(0);
}
});
//解压
jiema.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
try {
coding =yuHuffman.deCode(coding);
} catch (Exception e1) {
e1.printStackTrace();
}
jta.setText("解码后字符串:"+coding); //
}
});
}
// 内存占用计算
}
class Cache{
public static String cache() {
return ((Runtime.getRuntime().maxMemory()/1024/1024-Runtime.getRuntime().freeMemory()/1024/1024)+"M");
}
}
演示结果
(以下为原始软件·界面)
对图像ui美化后:
欢迎笔者对项目代码进行修改与评论,感谢支持。