霍夫曼编码

霍夫曼编码

以此谨记自己学习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”。到此为止,霍夫曼编码的压缩与解压,已经完成。
代码量挺多的,虽然是我自己写的心得,但是对于外人来讲还是挺难理解的,只有我自己看得懂。也应该是自己水平还不够吧,还不能真正理解,不能够和别人仔细的讲清楚,继续加油!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值