1.哈夫曼编码原理
哈夫曼编码是由哈弗曼树得到的编码。哈夫曼树属于二叉树,即树的结点最多拥有2个孩子结点。若该二叉树带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
哈夫曼编码压缩的原理就是将出现频率较高的字母用更少的空间表示,从而实现压缩的效果。
2.构建哈夫曼树
构造哈夫曼编码即构造哈弗曼树。其中每个树的结点含有对应的数据和权重。为了构造哈夫曼树还要实现Comparable接口,方便后面的排序。
public class HuffmenNode implements Comparable<HuffmenNode> {
public HuffmenNode left;
public HuffmenNode right;
public HuffmenNode parent;
public int data;
private byte weight;
}
之后构树的具体步骤为:
1.将结点按字母出现频率从小到大排序。对每个字母都创建一个结点,
2. 每次取出当前集合中出现频率最低的两个字母,生成他们的父节点,父节点的频率为两字母频率之和
3.删除掉集合中的两个子节点,并将父节点加入到集合中。
4.重复步骤1-3,直到集合中剩下最后一个节点,即为哈夫曼树的根节点。
//根据节点列表 创建哈夫曼树
private static HuffmenNode createHuffmenTree(List<HuffmenNode> nodeList) {
int length = nodeList.size();
while (length > 1) {
HuffmenNode huffmenNode01 = nodeList.get(length - 1);
HuffmenNode huffmenNode02 = nodeList.get(length - 2);
HuffmenNode huffmenNodeNew = new HuffmenNode(null, huffmenNode01.weight + huffmenNode02.weight);
huffmenNodeNew.leftNode = huffmenNode01;
huffmenNodeNew.rightNode = huffmenNode02;
nodeList.remove(huffmenNode01);
nodeList.remove(huffmenNode02);
nodeList.add(huffmenNodeNew);
Collections.sort(nodeList);
length = nodeList.size();
}
return nodeList.get(0);
}
3.根据哈弗曼树获得对应的编码
从根节点开始遍历整棵树,获得每个节点对应的哈夫曼编码。将字母对应的字节和其编码存入哈希表中。
private static void getCodes(HuffmenNode node, String s, StringBuffer sb) {
StringBuffer tempSb = new StringBuffer(sb);
tempSb.append(s);
if (node.data == null) {
getCodes(node.leftNode, "0", tempSb);
getCodes(node.rightNode, "1", tempSb);
} else {
mapCode.put(node.data, tempSb.toString());
}
}
4.将对应的文件进行压缩
传入的参数为目标字节数组和对应的哈夫曼编码表。返回的值为哈夫曼编码表对应的字节数组。
由于经过哈夫曼编码后获得的二进制字符串可能不再可以被8整除,在结尾时采用了低位补0的方式
private static byte[] encodeByHuffmenCode(byte[] bytes, Map<Byte, String> huffmenCodeMap) {
//将bytes转换成二进制字符串
StringBuffer sb = new StringBuffer();
for (byte b : bytes) {
String str = huffmenCodeMap.get(b);
sb.append(str);
}
// System.out.println(sb.toString());
//将二进制字符串转变为处理后的byte
int len = sb.length();
int newLenght = (len % 8 == 0) ? (len / 8) : (len / 8 + 1);
byte[] targetBytes = new byte[newLenght];
for (int i = 0; i < targetBytes.length; i++) {
if ((i + 1) * 8 > len) {
targetBytes[i] = (byte) Integer.parseInt(sb.substring(i * 8), 2);
} else {
targetBytes[i] = (byte) Integer.parseInt(sb.substring(i * 8, (i + 1) * 8), 2);
}
}
return targetBytes;
}
5.存储压缩后的文件。
利用序列化存储的方式,将文件对应的哈夫曼编码表和文件压缩字节数组存入新文件中。
对于序列化(ObjectOutputStream/ObjectInputStream):存入的顺序和读出的顺序要保证一样,同时读取时要进行指明类型的转化.
一个自己写的例子:
public static void main(String[] args) {
byte[] byarray = {10,20,30,40};
String s = "你好我是周杰伦";
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
try {
OutputStream out = new FileOutputStream("C:\\Users\\ZDX\\Desktop\\新建文件夹\\1110.txt");
ObjectOutputStream obout = new ObjectOutputStream(out);
obout.writeObject(byarray);
obout.writeObject(s);
obout.writeObject(list);
} catch (Exception e) {
e.printStackTrace();
}
}
读:
public static void main(String[] args) {
try {
InputStream input = new FileInputStream("C:\\Users\\ZDX\\Desktop\\新建文件夹\\1110.txt");
ObjectInputStream objinput = new ObjectInputStream(input);
//按顺序读
byte[] byarray = (byte[]) objinput.readObject();
String s = (String) objinput.readObject();
List<Integer> list = (List<Integer>) objinput.readObject();
System.out.println(s);
for (Integer i :list) {
System.out.print(i+ " ");
}
} catch (Exception e) {
e.printStackTrace();
}
}
结果为:
6.解压文件:
其具体为:
1.首先从文件中读取出编码表和目标字节数组。
2.完成转换:字节数组→二进制字符串
3.根据哈夫曼编码,读取二进制字符串,得到原文件。
由字符串到字节列表的转化:遍历字符串,一旦发现匹配的编码则输出。
private static List<Byte> getSource(String codeStr, Map<String, Byte> byteMap) {
List<Byte> tempList = new ArrayList<>();
for (int i = 0; i < codeStr.length(); ) {
int count = 1;
boolean flag = true;
Byte b = null;
while (flag) {
String key = codeStr.substring(i, i + count);
b = byteMap.get(key);
if (b == null) {
count++;
} else {
flag = false;
}
}
tempList.add(b);
i += count;
}
return tempList;
}
注意:对于最后一个补0得到的字节的处理:
将原字节首先用转化成int型(32位)再与256进行或运算,然后截取最后八位的数据,从而得到元数据。
private static String byteToString(byte target) {
int temp = target;
temp |= 256;
String str = Integer.toBinaryString(temp);
return str.substring(str.length() - 8);
}