/**
霍夫曼编码算法描述:
1、第一件事情就是将每个node存入到list中,后续进行排序、建树等等操作,但现在node的构造方法里面需要符号以及权值,如何做到?
首先应该根据输入的字符串,统计每一个byte出现的次数(权值),存入到map中!(每个byte是key,权值是value)
上面这个思想还是很重要的,做完之后遍历map(去复习),循环将node new出来,并存到list中
2、通过第一步的nodes ,构建二叉树。
第一步:排序; 第二步:取最小节点组合为新节点; 第三步:删除旧节点,挂上新节点 第四步:循环
3、将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合。
叶子节点是需要编码的,非叶子是合成的不需要编码。叶子节点判断方法:node.getData ?= null
如果是非叶子节点,就向左递归、向右递归。如果是叶子节点。马上存入到map中
4、编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[] (当做一个小算法好好练练)
也就是说,原来的byte数组就是字符串对应转来的那个数组。
现在得到的数组是每个字符进行编码后(由于每个字符编码后是二进制一串数字很长,所以每8位取做一个),每八位取做一个byte 存入数组中
方法:
5、解码第一步:将每个byte转为一个二进制的字符串,这里有很多要注意的点,涉及二进制。
第一,先思考这个编码后的byte[]是怎么来的?
首先字符每个元素串转为byte[];之后将byte[]每个元素按照霍夫曼编码;
再后是 每8个二进制01,(以补码的形式)转为一个十进制数,存在byte[]: (byte) Integer.parseInt(temp,2);
所以现在目标是把byte[]中每一个元素(十进制形式)转为对应的二进制
package huffmanCode;
import java.io.*;
import java.util.*;
public class HuffmanCode {
public static void main(String[] args) {
String s = "i like like like java do you like java";
byte[] bytes = s.getBytes();//字符串转换为数组
/*List nodes = getNodes(bytes);
Node02 node02 = huffmanTree(nodes);
//preOrder(node02);
System.out.println("------------------");
Map codes = getCodes(node02);
//System.out.println(codes);
byte[] zip = zip(bytes, codes);
for (int i = 0; i < zip.length; i++) {
System.out.print(zip[i] + " ");
}
System.out.println();
System.out.println(zip.length);
//将以上步骤封装到一个方法中*/
/* byte[] bytes1 = huffmanZip(bytes);
System.out.println(Arrays.toString(bytes1));
System.out.println("--------------------");
byte[] decode = decode(huffmanCodes, bytes1);
//System.out.println(Arrays.toString(decode));
System.out.println("原来的字符串=" + new String(decode));*/
String src = "D:\\360Downloads\\wpcache\\360wallpaper.jpg";
String dst = "D:\\360Downloads\\wpcache\\360wallpaper.zip";
String dst1 = "D:\\360Downloads\\wpcache\\unzip1.jpg";
zipFile(src,dst);
System.out.println("压缩成功!");
unZipFile(dst,dst1);
System.out.println("解压成功");
}
/**
* 1、将每个node存入到list中,用于后续进行排序、建树等等操作
*
* @param bytes 接收字节数组
* @return 返回list结合,里面封装好了每个node信息(此时还没挂上左右节点,要在建树的时候挂上)
*/
public static List<Node02> getNodes(byte[] bytes) {
Map<Byte, Integer> map = new HashMap<>();
for (byte data : bytes) {
if (map.get(data) == null) { //通过key获取value,value是权值
map.put(data, 1);
} else {
map.put(data, map.get(data) + 1);//如果再次遍历到同样的字符,统计次数+1
}
}
//map构建完成后,以及统计好了每个字符对应的权值(出现次数),现在应该遍历该map,去new出对应的节点 ,并存到list中
List<Node02> node02List = new ArrayList<>();
//遍历去复习
for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
node02List.add(new Node02(entry.getKey(), entry.getValue()));
}
return node02List;
}
/**
* 2、构建HuffmanTree
*
* @param node02s
* @return
*/
public static Node02 huffmanTree(List<Node02> node02s) {
while (node02s.size() > 1) {
//第一步,排序
Collections.sort(node02s);
//
Node02 leftNode = node02s.get(0);
Node02 rightNode = node02s.get(1);
//注意组合成的中间节点,值是null
Node02 parent = new Node02(null, leftNode.getWeight() + rightNode.getWeight());
parent.setLeft(leftNode);
parent.setRight(rightNode);
//
node02s.add(parent);
//
node02s.remove(leftNode);
node02s.remove(rightNode);
}
return node02s.get(0);
}
/**
* 遍历
*/
public static void preOrder(Node02 root) {
if (root != null) {
root.preOrder();
}
}
//全局变量?
static StringBuilder stringBuilder = new StringBuilder();
static Map<Byte, String> huffmanCodes = new HashMap<>();
/**
* 3、将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
*
* @param node02 传入的节点
* @param code 路径: 左子结点是 0, 右子结点 1
* @param stringBuilder 该节点的value对应的哈夫曼编码
*/
public static void getCodes(Node02 node02, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
stringBuilder1.append(code);
if (node02 != null) {
//去判断是叶子还是非叶子
if (node02.getValue() == null) {
//说明不是叶子节点
//向左遍历
getCodes(node02.getLeft(), "0", stringBuilder1);
//向右遍历
getCodes(node02.getRight(), "1", stringBuilder1);
} else {
//到了叶子节点,应该把当前的stringBuilder放入到map中
huffmanCodes.put(node02.getValue(), stringBuilder1.toString());
}
}
}
/**
* 3、对上面的方法进行重载
* @param node02
* @return
*/
public static Map<Byte, String> getCodes(Node02 node02) {
if (node02 == null) {
return null;
}
//向左、向右遍历 并传入全局变量。
getCodes(node02,"0",stringBuilder);
getCodes(node02,"1",stringBuilder);
//变量完成后,全局变量map已经被改变了,直接返回
return huffmanCodes;
}
/**
* 4、编写一个方法,将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
* @param bytes 这时原始的字符串对应的 byte[]
* @param huffmanCodes 生成的赫夫曼编码map
* @return 返回赫夫曼编码处理后的 byte[]
* 举例: String content = "i like like like java do you like a java"; =》 byte[] contentBytes = content.getBytes();
* 返回的是 字符串 "1101111110111101101010011111101111011010100111111011110110101001111111000100011101110001111111000110011111111010110011110011111101111011010100111111100010001110111000"
* => 对应的 byte[] huffmanCodeBytes ,即 8位对应一个 byte,放入到 huffmanCodeBytes
* huffmanCodeBytes[0] = 10101000(补码) => byte [推导 10101000=> 10101000 - 1 => 10100111(反码)=> 11011000= -88 ]
* huffmanCodeBytes[1] = -88
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
//先得到byte数组(输入)对应的哈夫曼编码,存入到stringBuilder
StringBuilder stringBuilder2 = new StringBuilder();
for(byte b : bytes){
stringBuilder2.append(huffmanCodes.get(b));//huffmanCodes是map,通过key获取value
}
//System.out.println(stringBuilder2);//测试
//第二步得出数组大小
int length;
if (stringBuilder2.length() / 8 == 0) {
length = stringBuilder2.length() / 8;
} else {
length = stringBuilder2.length() / 8 + 1;
}
//第三步,根据数组大小,new出压缩后的数组,并将字符串转换过去
byte[] zipBytes = new byte[length];
int index = 0;
for (int i = 0; i < stringBuilder2.length(); i+=8) {
String temp;
if (i+8 > stringBuilder2.length()){
//长度不够了,直接从当前位置开始取到最后就行了。
temp = stringBuilder2.substring(i);
}else {
temp = stringBuilder2.substring(i,i+8);
}
zipBytes[index] = (byte) Integer.parseInt(temp,2);
index++;
}
return zipBytes;
}
/**
* 1234、将前面四步进行封装,只调用当前方法即可
* @param bytes 将字符串 语句 转为 byte数组 后的原始数据
* @return 返回一个压缩后的byte数组
*/
public static byte[] huffmanZip(byte[] bytes){
//1、将每个node存入到list中,用于后续进行排序、建树等等操作
List<Node02> node02s = getNodes(bytes);
//2、构建HuffmanTree
Node02 node02 = huffmanTree(node02s);
//3、将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
Map<Byte, String> codes = getCodes(node02);
//4、将字符串对应的byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码 压缩后的byte[]
byte[] zip = zip(bytes, codes);
return zip;
}
/**
* 5、将一个byte 转成一个二进制的字符串, 如果看不懂,可以参考我讲的Java基础 二进制的原码,反码,补码
* @param b 传入的 byte
* @param flag 标志是否需要补高位如果是true ,表示需要补高位,如果是false表示不补, 如果是最后一个字节,无需补高位
* @return 是该b 对应的二进制的字符串,(注意是按补码返回)
*/
private static String byteToBitString(boolean flag, byte b) {
//使用变量保存 b
int temp = b; //将 b 转成 int,int才有toBinaryString()方法
//如果是正数我们还存在补高位
if(flag) {
temp |= 256; //按位与 256 1 0000 0000 | 0000 0001 => 1 0000 0001
}
String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制的补码
if(flag) {
return str.substring(str.length() - 8);//只取8位
} else {
return str;
}
}
/**
* 6、编写一个方法,完成对压缩数据的解码
* @param huffmanCodes 赫夫曼编码表 map
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @return 就是原来的字符串对应的数组
*/
public static byte[] decode(Map<Byte,String> huffmanCodes,byte[] huffmanBytes){
StringBuilder stringBuilder1 = new StringBuilder();
//得到二进制字符串。
for (int i = 0; i < huffmanBytes.length; i++) {
byte b = huffmanBytes[i];
boolean flag = !(i == huffmanBytes.length - 1);//最后一位设为false
stringBuilder1.append(byteToBitString(flag, b));
}
//得到倒装的map
Map<String, Byte> inverseHuffmanCodes = new HashMap<>();
for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
inverseHuffmanCodes.put(entry.getValue(), entry.getKey());
}
//根据解码表,将二进制字符串转为最原始输入的字符串!
//创建要给集合,存放byte
List<Byte> list = new ArrayList<>();
/* int i = 0;
while (i < stringBuilder1.length()) {//注意!这里stringBuilder才是二进制字符串!
//练习算法!设置一个flag的思想不是一开始就有的,是写的过程中需要,想到加入的
int count = 1;
Byte value = 0;
boolean flag = true;
while (flag) {
String key = stringBuilder1.substring(i, i + count);
value = inverseHuffmanCodes.get(key);
if (value == null){
count++;
}else {
flag=false;
}
}
i += count;
list.add(value);
}*/
for(int i = 0; i < stringBuilder1.length(); ) {
int count = 1; // 小的计数器
boolean flag = true;
Byte b = null;
while(flag) {
//1010100010111...
//递增的取出 key 1
String key = stringBuilder1.substring(i, i+count);//i 不动,让count移动,指定匹配到一个字符
b = inverseHuffmanCodes.get(key);
if(b == null) {//说明没有匹配到
count++;
}else {
//匹配到
flag = false;
}
}
list.add(b);
i += count;//i 直接移动到 count
}
//当for循环结束后,我们list中就存放了所有的字符 "i like like like java do you like a java"对应的 byte[] 数组。string.getBytes()方法拿到的
//把list 中的数据放入到byte[] 并返回
byte b[] = new byte[list.size()];
for(int j = 0;j < b.length; j++) {
b[j] = list.get(j);
}
return b;
//new String(上面的数组),就能将 byte数组 转回为原来的 "i like like like java do you like a java" string语句
}
/**
* 7、对文件进行压缩
* @param srcFile
* @param dstFile
*/
public static void zipFile(String srcFile,String dstFile){
FileInputStream fis = null;
OutputStream fos = null;
ObjectOutputStream oos = null;
byte[] stringBytes = null;
byte[] huffmanZipBytes = null;
try {
fis = new FileInputStream(srcFile);
stringBytes = new byte[fis.available()];
fis.read(stringBytes);
//读完了之后应该开始压缩
huffmanZipBytes = huffmanZip(stringBytes);
//压缩完毕之后想着输出
fos = new FileOutputStream(dstFile);
oos = new ObjectOutputStream(fos);
//写进去
oos.writeObject(huffmanZipBytes);
//还有map对应的哈夫曼编码表别忘了写,否则无法解压
oos.writeObject(huffmanCodes);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
try {
fis.close();
oos.close();
fos.close();
} catch (IOException e) {
//throw new RuntimeException(e);
System.out.println(e.getMessage());
}
}
}
/**
* 8、文件解压
* @param zipFile 准备解压的文件
* @param dstFile 将文件解压到哪个路径
*/
public static void unZipFile(String zipFile,String dstFile){
InputStream is = null;
ObjectInputStream ois = null;
OutputStream fos = null;
try {
is = new FileInputStream(zipFile);
//完成流的嵌套
ois = new ObjectInputStream(is);
//读出之前存的压缩后的数据
byte[] huffmanBytes = (byte[]) ois.readObject();
//读出编码表
Map<Byte,String> huffmanCodes = (Map<Byte, String>) ois.readObject();
//进行解压
byte[] decodedBytes = decode(huffmanCodes, huffmanBytes);
//输出
fos = new FileOutputStream(dstFile);
fos.write(decodedBytes);
} catch (Exception e) {
System.out.println(e.getMessage());
}finally {
try {
if (fos!=null){
fos.close();
}
ois.close();
is.close();
} catch (IOException e2) {
//throw new RuntimeException(e);
System.out.println(e2.getMessage());
}
}
}
}
class Node02 implements Comparable<Node02> {
private Byte value;//每个符号,以ASCII形式存放
private Integer weight;//权值,即为每个符号出现的次数
private Node02 right;
private Node02 left;
@Override//left和right不要写出来,方便到时候 遍历二叉树 主要输出当前节点的信息就好了
public String toString() {
return "Node02{" +
"value=" + value +
", weight=" + weight +
'}';
}
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
public Byte getValue() {
return value;
}
public void setValue(Byte value) {
this.value = value;
}
public Integer getWeight() {
return weight;
}
public void setWeight(Integer weight) {
this.weight = weight;
}
public Node02 getRight() {
return right;
}
public void setRight(Node02 right) {
this.right = right;
}
public Node02 getLeft() {
return left;
}
public void setLeft(Node02 left) {
this.left = left;
}
public Node02(Byte value, Integer weight) {
this.value = value;
this.weight = weight;
}
@Override
public int compareTo(Node02 o) {
return this.weight - o.weight;
}
}