目录
一、什么是哈夫曼树
我们知道,从树根到某个结点的路径是从根结点开始沿着某个分支到达该结点的一个结点序列,路径所含的分支数(结点个数减1)称为此路径的长度。而一棵树的路径长度是指从树根到其余各结点的路径长度之和。结点的带权路径长度是指从根结点到该结点之间的路径长度与该结点上所带权值的乘积。设一棵树有 n个叶结点,每个叶结点带有权值Wₖ,从根结点到每个叶结点的长度lₖ,则每个叶结点的带权路径长度之和就是这棵树的带权路径长度(WPL),它可以被表
哈夫曼树就是WPL值最小的二叉树,它的特点是权值越大的叶结点越靠近根结点,而权值越小的叶结点越远离根结点。
例如队列:3,7,5,1,8 。对这一组数字进行从小到大的规则排序,排序后为1,3,5,7,8。在这些数字中选择两个最小的数字:1,3 用类似树杈的“树枝”连接两个最小的数,在顶点处计算出这两个数字的和4,将这两数字和加入队列中,并比较剩下的数字和这个和的大小,再取出两个最小的数字进行排序。重复上述操作,构成的哈夫曼树如右图所示
二、构造哈夫曼树
1.新建Node类
class TreeNode{
int data; //结点权值
public TreeNode left; //指向左子树
public TreeNode right; //指向右子树
public TreeNode(int data) { //构造方法,创建结点是传入权值
this.data = data;
}
}
2.创建结点
int[] arr = {7,5,9,6,3,8,10,4}; //数组存放结点权值
List<TreeNode> list = new ArrayList<>(); //队列,存放结点
for(int i = 0;i < arr.length;i++) {
TreeNode node = new TreeNode(m.charAt(i)); //创建结点
list.add(node); //将结点存入队列list中
}
3.对结点进行从小到大排序
使用二分法,根据结点的权值对结点从小到大排序。
public void sort(List<TreeNode> list,int start,int end) {
int left = start;
int right = end;
TreeNode key1 = list.get(start);
int key = key1.data;
while(right > left) {
while(list.get(right).data>=key && right > left) {
right--;
}
while(list.get(left).data<=key && right > left) {
left++;
}
if(list.get(left).data > list.get(right).data) {
TreeNode temp = list.get(left);
list.set(left, list.get(right));
list.set(right, temp);
}
}
list.set(start, list.get(left));
list.set(left, key1);
if(start < left) {
sort(list,start,left-1);
}
if(right < end) {
sort(list,left+1,end);
}
}
4.构造树
public TreeNode creatTree() {
TreeNode leftNode;
TreeNode rightNode;
TreeNode node;
while(list.size() > 1) { //当队列中没有元素时结束循环
leftNode = list.remove(0); //取出队列的第一个结点
rightNode = list.remove(0); //取出的是权值最小的两个结点
node = new TreeNode(leftNode.data + rightNode.data); //创建权值为最小两个数和的结点
node.left = leftNode;
node.right = rightNode;
list.add(node); //将新结点存入队列中
sort(list,0, list.size()-1); //对结点从小到大排序
if(list.size()==1) { //当队列中只有一个结点时,该结点就是哈夫曼树的根结点
node = list.remove(0);
node.left = leftNode;
node.right = rightNode;
return node;
}
}
return null;
}
三、哈夫曼编码
每个结点的左分支被记为0,右分支被记为1。某一字符的编码可通过组合从根结点到该字符结点(叶结点)的路径上所标注的0、1得到。注意前缀编码树的特点是每个字符必是叶结点,且树中没有度为1的结点。一个字符的编码长度即是该字符结点在其哈夫曼树中的深度d-1。哈夫曼编码的特点使得出现频率高的字符编码短些,出现频率低的字符编码长些,这样可以达到更好的压缩效果。
在前面新建的Node类中,加入code的元素。
//用递归方法给树的叶子编码
public void setCode(TreeNode node) {
if(node.left != null) {
node.left.code = node.code + "0";
setCode(node.left);
}
if(node.right != null) {
node.right.code = node.code + "1";
setCode(node.right);
}
}
四、压缩
1.根据给定的字符串内容(读取文本文件),统计每个字符出现的频率,创建对应的节点
2.根据节点的权值创建哈夫曼树,统计字符编码
3.把字符数据替换成对应的编码
4.把字符编码每8个一组转成byte写入文件:最后一个字节不足要补0,将它补满8位
5.把码表写入文件
1.读取文本信息
public String readFile(String path) {
File file = new File(path);
FileReader fr;
String str = ""; //存放文本信息
try {
fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr); //可以对文件进行行输入
String s = "";
while((s = br.readLine()) != null) { //br.readLine()行输入
str += s;
}
} catch (Exception e) {
e.printStackTrace();
}
return str;
}
2.用HashMap统计每个字符出现的频率,创建对应的节点
public void number(String m) { //s为文本信息
hm = new HashMap<>();
for(int i = 0;i < m.length();i++) {
if(!(hm.containsKey(m.charAt(i)+""))) { //与hm比较,若没有该字符
hm.put(m.charAt(i)+"",1); //m.charAt(i)是char类型,+""自动转为String
}else {
hm.put(m.charAt(i)+"", hm.get(m.charAt(i)+"")+1);//若有该字符,则+1;
}
}
for(String s : hm.keySet()) {
System.out.print(s + ":" + hm.get(s) + " ");
StrTreeNode node = new StrTreeNode(hm.get(s),s); //创建结点
list.add(node);
}
}
接下来就是用得到的结点,根据结点权值,也就是字符出现的频率进行从小到大排序,然后再构造树并生成哈夫曼编码,并将哈夫曼编码存入码表hmcode
3.把字符串数据替换成对应的编码
//将哈夫曼编码存入码表
HashMap<String,String> hmcode = new HashMap<>(); //存放字符和对应的编码,码表
public void inorder(StrTreeNode node,String m) {
if(node.left == null && node.right == null) {
hmcode.put(node.s, node.code); //存入码表
}
if(node.left != null) {
inorder(node.left,m);
}
if(node.right != null) {
inorder(node.right,m);
}
}
//把字符串数据替换成对应的编码
public String setCode(String m) {
StringBuilder stringb = new StringBuilder();
for(int i = 0;i < m.length();i++) {
stringb.append(hmcode.get(m.charAt(i)+""));
}
return stringb.toString();
}
4.把字符编码每8个一组转成byte写入文件
//把字符编码没每8个一组转成byte写入文件:最后一个字节不足要补全
public void change(String n) throws Exception {
FileOutputStream fos = new FileOutputStream(fName);
ObjectOutputStream oos = new ObjectOutputStream(fos);
int c = 8 - n.length() %8;
int x = 8 - c;
int data;
if(c!=0) { //不足8为补0
for(int i = 1;i <= x;i++) {
n += '0';
}
}
for(int i = 0;i < n.length();i=i+8) {
String a;
String b = "";
b += n.charAt(i);
b += n.charAt(i+1);
b += n.charAt(i+2);
b += n.charAt(i+3);
b += n.charAt(i+4);
b += n.charAt(i+5);
b += n.charAt(i+6);
b += n.charAt(i+7);
data = bit_to_byte(b);
oos.write(data); //将数值写入文件
}
oos.writeObject(hmcode); //将码表存入文件
oos.close();
}
//8位二进制转成byte即十进制
public int bit_to_byte(String bit2) {
int y = 2;
int x0 = bit2.charAt(7) - '0';
int x1 = (bit2.charAt(6) - '0')*y;
y = y*2;
int x2 = (bit2.charAt(5) - '0')*y;
y = y*2;
int x3 = (bit2.charAt(4) - '0')*y;
y = y*2;
int x4 = (bit2.charAt(3) - '0')*y;
y = y*2;
int x5 = (bit2.charAt(2) - '0')*y;
y = y*2;
int x6 = (bit2.charAt(1) - '0')*y;
y = y*2;
int x7 = (bit2.charAt(0) - '0')*y;
y = y*2;
return (x0+x1+x2+x3+x4+x5+x6+x7);
}
五、解压
解压部分相对于压缩部分要简单很多
1.读取文件数据,把byte数据转成二进制
2.二进制不足8为要补0
3.根据码表把二进制数据还原成对应的字符内容,再写入文件
1.读文件数据并转成二进制
//读需要解压的文件,并转成编码的形式
public String read() {
String code = "";
ObjectInputStream ois;
try {
FileInputStream fips = new FileInputStream(fName);
ois = new ObjectInputStream(fips);
int n = ois.read();
while(n !=-1) { //当文件数据读到最后,ois.read()返回-1
code += this.byte_to_bit(n); //十进制转二进制
n = ois.read();
}
hm = (HashMap)ois.readObject();
} catch (Exception e) {}
return code;
}
//十进制转二进制,并补0
public String byte_to_bit(int data) {
int m = data;
int count = 0;
String n = "";
while(m!=0) {
int a = m%2;
m = m/2;
count++;
n += String.valueOf(a); //int类型转String
}
for(int i = count+1;i <=8;i++) { //补0
n += "0";
}
String next = "";
for(int i = 7;i >= 0;i--) { //由于读从文件读出的数据是倒置的,使用将其倒置得到原来的编码
next += n.charAt(i);
}
return next;
}
2.将编码转成字符串
public String imcode(String code) {
String m = "";
String n = "";
for(int i = 0;i < code.length();i++) {
n += code.charAt(i);
Iterator<String> iterator = hm.keySet().iterator(); //遍历码表
while (iterator.hasNext()) {
Object key = iterator.next();
if(n.equals(hm.get(key))) {
m += key;
n = "";
}
}
}
return m;
}
3.将字符串写入到文件中
public void intofile(String m) {
File file = new File(fName);
try {
FileOutputStream fos = new FileOutputStream(file);
PrintStream ps = new PrintStream(fos);
ps.print(m);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
六、添加图形化界面
public void show() {
JFrame jf = new JFrame();
jf.setSize(240, 120);
jf.setTitle("压缩");
jf.setLocationRelativeTo(null);
jf.setDefaultCloseOperation(3);
FlowLayout flow = new FlowLayout();
jf.setLayout(flow);
//按钮和文本框
JButton jb1 = new JButton("压缩");
jf.add(jb1);
JButton jb2 = new JButton("解压");
jf.add(jb2);
JLabel jl = new JLabel("需要进行操作的文件是:");
jf.add(jl);
JTextField jtf = new JTextField();
Dimension dm = new Dimension(200,20);
jtf.setPreferredSize(dm);
jf.add(jtf);
//监听器
ButtonMouse mouse = new ButtonMouse(jf,jtf);
jb1.addMouseListener(mouse);
jb1.addActionListener(mouse);
jb2.addMouseListener(mouse);
jb2.addActionListener(mouse);
jf.setVisible(true);
}