霍夫曼编码
以此谨记自己学习java心得
昨天学了霍夫曼编码,霍夫曼编码是在霍夫曼树的基础上对叶子结点进行二进制编码。昨天的心得也已经介绍了霍夫曼树,针对于霍夫曼编码的具体应用,比如说一串字符串如,“i like like like java do you like a java”(这串代码有很多是重复的字母),压缩率很高。如果要将上面的字符串进行霍夫曼编码压缩,首先需要统计出每个字母出现的次数,比如说 a出现5次 ,空格出现9次。这里的5和9这些次数,我令它作为霍夫曼树的叶子结点的权值。经过一个算法,可得以上字母在字符串中出现的次数。前提:每一个霍夫曼树都有一个结点,结点拥有属于自己的类。
以下代码为结点类
class Code implements Comparable<Code> {
public Byte data;
public int count;
public Code left;
public Code right;
public Code(Byte data, int count) {
this.data = data;
this.count = count;
}
public Code() {
}
@Override
public String toString() {
return "Code{" +
"data='" + data + '\'' +
", count=" + count +
'}';
}
@Override
public int compareTo(Code o) {
return this.count - o.count;
}
}
注:Commpable接口是用于集合的自动排序Sort(因为霍夫曼树创建的前提是数组是有序的)
以下代码是计算字符串中字母出现的次数,并保存在一个数组中
/**
* 获取字符串中重复出现的次数,并创建对象存于一个集合中,并返回
*
* @param bytes
* @return
*/
public static ArrayList getCodes(byte[] bytes) {
ArrayList<Code> list = new ArrayList<>();
HashMap<Byte, Integer> hashMap = new HashMap<Byte, Integer>();
HashMap<Code, Integer> map = new HashMap<>();
for (int i = 0; i < bytes.length; i++) {
Integer count = hashMap.get(bytes[i]);//get方法是获取key所对应的value值
if (count == null) {
hashMap.put(bytes[i], 1);
} else {
hashMap.put(bytes[i], count + 1);
}
}
for (Map.Entry<Byte, Integer> entry : hashMap.entrySet()) {
// System.out.println(entry.getKey()+"\t"+entry.getValue());
Code code = new Code(entry.getKey(), entry.getValue());
// map.put(code,entry.getValue());
list.add(code);
}
return list;
}
现在已经获取到字符串中出现字母的次数,注意因为我之前的字符串转换为Byte后是根据ASCII码改的,比如说‘a’就对应的是ASCII中的97。
在我们获取了集合后,已经有一些数字,也就是集合中count的大小。如果按照霍夫曼编码,那么空格 data=‘32’,count=9则应该离根节点近一点,
字母 d data=‘100’,count=1则应该离根节点最远。
以下代码是让这个集合创建出霍夫曼表,因为创建霍夫曼表我这里设定的权值为集合中的count,所以有些结点的data有可能是null。
代码如下,以下代码是创建霍夫曼树的代码
在这里插入代码片/**
* 将传入的集合 按照集合内对象的count的大小来排列为一个霍夫曼树
*
* @param arrayList
* @return
*/
public static Code createrTree(ArrayList<Code> arrayList) {
while (arrayList.size() > 1) {
Code code0 = arrayList.get(0);
Code code1 = arrayList.get(1);
Code code = new Code(null, code0.count + code1.count);
arrayList.add(code);
code.left = code0;
code.right = code1;
arrayList.remove(code0);
arrayList.remove(code1);
Collections.sort(arrayList);
}
prologue(arrayList.get(0));
return arrayList.get(0);
}
这里有一个霍夫曼编码很巧妙的计算,因为保存后的文件是以二进制方式,每一个字母都应该有属于自己的二进制编码,那么原则上应该让出现次数多的空格拥有比较少的二进制编码,出现次数仅仅只有一次的字母d 应该拥有较多的二进制编码,而恰好霍夫曼树中 空格代表的count =9 离根节点近,字母d代表的count=1离根节点很远,那么我们可以根据遍历,找到每一个叶子结点,因为叶子结点才是我们所需要的有意义的data!=null,count!=null。现规定从根节点开始往左寻找叶子结点,则令一个字符穿StringBuild +“0”,若往右寻找则令这个StringBuild+“1”,然后再将这个StringBuild赋给当前结点,令一个HashMap保存起来,key=集合的data,value=所对应的StringBuild。
代码如下`
public static void createCode(Code code, String s, StringBuilder stringBuilder) {
StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
stringBuilder1.append(s);
if (code != null) {
if (code.data == null) {
createCode(code.left, "0", stringBuilder1);
createCode(code.right, "1", stringBuilder1);
} else {
map.put(code.data, stringBuilder1.toString());
}
}
}
接下来我们已经获取了每一个字母所对应的霍夫曼编码,然后回到原来的字符串“i like like like java do you like a java”,用上面的霍夫曼编码来替换字符串里面的字母。
代码如下
static HashMap<Byte, String> map = new HashMap<>();
public static void createCode(Code code, String s, StringBuilder stringBuilder) {
StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
stringBuilder1.append(s);
if (code != null) {
if (code.data == null) {
createCode(code.left, "0", stringBuilder1);
createCode(code.right, "1", stringBuilder1);
} else {
map.put(code.data, stringBuilder1.toString());
}
}
}
String bytes = "i like like like java do you like a java";
StringBuilder strCode = new StringBuilder();
byte[] bytes1 = bytes.getBytes();//将上面的字符串转成ASCII码
for (byte b : bytes1) {
strCode.append(map.get(b));
}
注:map里面的数据就是上一张小小的那一张图片,也就是
{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}
所以进行到这一步,得到结果strCode一串二进制编码
1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100。因为我们原来的字符串只有40位,而现在却有二进制数字133位,反而更多了,所以我们要再对这一串二进制编码进行压缩,以8位8位来进行压缩,如刚开始的10101000压缩为-88。注意这里要考虑到二进制的原码,反码,补码。转换如下图
具体转换代码,将传入的一个字符串,根据二进制转换到一个byte数组里
代码如下
public static byte[] strToByte(StringBuilder stringBuilder) {
int len;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length() / 8;
} else {
len = stringBuilder.length() / 8 + 1;
}
byte[] bytes = new byte[len];
int count = 0;
for (int i = 0; i < stringBuilder.length(); i = i + 8) {
String str;
if (i + 8 < stringBuilder.length()) {
str = stringBuilder.substring(i, i + 8);
} else {
str = stringBuilder.substring(i);
}
bytes[count] = (byte) Integer.parseInt(str, 2);//如果是省略2,则是默认10进制,加上2就是2进制
count++;
}
return bytes;
}
得到的byte数组是
[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
所以我们现在已经将字符串i like like like java do you like a java,转换为byte数组[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]。从原来的40个字符数转到现在的17个数组数。完成了霍夫曼编码的压缩过程。
霍夫曼编码的解码
我们得到的数组是[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28],打个比方说别人给我们这个数组,再给我们一串霍夫曼编码(每一个字符对应的编码),让我们去解码。也就是说现在给的是一个数组[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28],和一串编码{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}。
首先要将这个数组转换成二进制,由于我们之前是8位8位编制的,现在再8位8位解开
代码如下
/**
* byte数组字节转为二进制字符串
*
* @param flag 如果已经到了数据的最后一位,则令flag=false,否则仍会输出8位的二进制,实际上却只需要正常的位数即可
* @param bytes
* @return
*/
public static String byteToString(Boolean flag, byte bytes) {
int temp = bytes;
if (flag) {
temp = temp | 256;
}
String string = Integer.toBinaryString(temp);
if (flag) {
return string.substring(string.length() - 8);
} else
return string;
}
特别说明,当-1传进来时,会出现11111111,这个是补码,可以自行翻译成原码,而传入正数1时,获得的是1,而我们需要的是00000001,这时候只需要将正数于256按位与,也就是|,因为256=111111111,与之后的结果再获取后8位就行,考虑到如果字符串不是8的整数,如果按照这个代码继续执行,会使数组最后一个的数字也具有8个数字,所以我们需要传入flag,当已经进行到数组最后一个数字时,flag=false。
当执行完后获得字符串1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100,和之前的一模一样。接下来就是按照已经给的条件{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011}来进行翻译
将这个字符串翻译成一个数组
代码如下
/**
* 将得到的01010101这种形式的二进制字符转为byte型数组,内容是对前面二进制字符按照之前得到的哈夫曼编码进行转换,如01转为32
* @param bytes
* @param map
*/
public static void BinaryStringToString(byte[] bytes, HashMap<Byte, String> map) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
if (i == bytes.length - 1) {
builder.append(byteToString(false, bytes[i]));
} else {
builder.append(byteToString(true, bytes[i]));
}
}
System.out.println(builder);
// builder.delete(builder.length()-3,builder.length()-1);
// builder.replace(130,131,"00011");
// builder.append("00011");
// System.out.println(builder);
HashMap<String, Byte> hashMap = new HashMap<>();
int count;
for (Map.Entry<Byte, String> entry : map.entrySet()) {
hashMap.put(entry.getValue(), entry.getKey());
}
// StringBuilder builder1 = new StringBuilder();
ArrayList<Byte> list = new ArrayList<>();
for (int i = 0; i < builder.length(); ) {
boolean flag1 = true;
count=0;
while (flag1) {
if (i+count>=builder.length()){
break;
}
count++;
String data = builder.substring(i , i+count);
Byte aByte = hashMap.get(data);
if (aByte!=null){
list.add(aByte);
flag1=false;
}
}
i+=count;
}
System.out.println(list);
byte [] bytes1=new byte[list.size()];
for (int i = 0; i < list.size(); i++) {
bytes1[i]=list.get(i);
}
System.out.println(new String(bytes1));
}
这里的代码思路核心是新创建一个HashMap,里面的Key是原来的value,也就是已给的条件是32对应01,新的HashMap是01对应32,
再由以获得的二进制字符串,一个个对比,传入新的byte数组,可得结果
[105, 32, 108, 105, 107, 101, 32, 108, 105, 107, 101, 32, 108, 105, 107, 101, 32, 106, 97, 118, 97, 32, 100, 111, 32, 121, 111, 117, 32, 108, 105, 107, 101, 32, 97, 32, 106, 97, 118, 97]
这个就是ASCII码表对应的数字
byte [] bytes1=new byte[list.size()];
for (int i = 0; i < list.size(); i++) {
bytes1[i]=list.get(i);
}
System.out.println(new String(bytes1));
再由这个代码将ASCII转换成字母,就可得到最开始的字符串
“i like like like java do you like a java”。到此为止,霍夫曼编码的压缩与解压,已经完成。
代码量挺多的,虽然是我自己写的心得,但是对于外人来讲还是挺难理解的,只有我自己看得懂。也应该是自己水平还不够吧,还不能真正理解,不能够和别人仔细的讲清楚,继续加油!