哈夫曼编码详解 及 Java实现

13 篇文章 0 订阅

哈夫曼编码

原理

众所周知:计算机是只看得懂二进制。一串字符,转换成二进制之后,数据是非常长的。哈夫曼编码的作用就是,根据字符出现的频率,来定义其二进制编码的长度,出现的频率越高,则编码长度越短。是不是听起来和哈夫曼树很像:权值越大,路径越短(废话,这俩明显是一个人搞出来的)。

举个例子

Hello world 转换成十进制ASCII编码为以下这串数字

72 101 108 108 111 32 119 111 114 108 100

转换成二进制,是这样的:

01001000 01100101 01101100 01101100 01101111 00100000 01110111 01101111 01110010 01101100 01100100

而如果统计各个字符出现的次数就会发现:H:1, e:1, w:1, r:1, d:1, 空格:1, o:2, l:3,

将出现的频次当做权值,画出一棵哈夫曼树如下,若将向左为0向右为1,则每个叶子节点的路径能根据出现的频次组成前缀编码(编码本身不会成为其他编码的前缀)

在这里插入图片描述

如上图所示的话,各个字母对应的编码应为:H:000, e:001, w:1100, r:1101, d:1110, 空格:1111, o:01, l:10, 经过观察你会发现,以上编码没有任何一个是其他编码的前缀,即连在一起也不会分不清,而又比ASCII码压缩了一倍以上。

实现步骤

  1. 将一串String字符串转化为byte数组中存储,此时数组中存储的是字符的ASCII码值
  2. 遍历此byte数组,将字符和每个字符出现的频次,当做每个节点的数据和权值,并将节点存进集合
  3. 依照此集合的节点,构建哈夫曼树,最终集合中仅剩一根节点
  4. 遍历此哈夫曼树的叶子节点,向左为0向右为1,求叶子节点的路径,并将其中数据(即字符的ASCII码值)与路径分别作为 key:value 存入map,此map集合就是根据你输入的字符串生成的哈夫曼码表
  5. 此时遍历最初的那个byte数组,与哈夫曼码表对照,生成一串由0、1组成的字符串。
  6. 将此字符串看成每八位为一组的二进制补码,转换byte类型存入数组中。
  7. 至此,完成了对一开始字符串的压缩

对第6步的一些额外知识

八位的二进制数转换为byte类型,首先二进制数在内存中以补码的形式存储,即 此八位二进制数为补码,若要转换成byte类型并存入数组,则需要

  1. 将补码转换为反码
  2. 将反码转换成原码
  3. 忽略符号位,将原码转换为十进制

如十进制数 1,其原码为00000001,因其为正数所以首位为0,其反码为00000001,其补码为00000001(正数的补码反码为其原码自身)。

十进制数-1,其原码为10000001,因其为负数,所以首位为1,其反码为111111110,其补码为反码+1,即11111111

更多关于原码反码补码的知识,请移步:点击查看 这篇博客写的很详细。

压缩代码实现

package huffman_code;

import java.util.*;

public class HuffmanCode {
    public static void main(String[] args) {
        String str = "Hello hello world";
        byte[] bytes = toHuffmanCode(str);
        System.out.println(Arrays.toString(bytes));
    }

    static Map<Byte,String> codeMap = new HashMap<>();
    static StringBuilder stringBuilder = new StringBuilder();
    //整合之前的方法
    public static byte[] toHuffmanCode(String str){
        byte[] bytes = str.getBytes();
        List<Node> nodeList = nodeToList(bytes);
        Node root = creatHuffmanTree(nodeList);
        toHuffmanCodeMap(root, "", stringBuilder);
        byte[] zipCode = zip(bytes, codeMap);
        return zipCode;
    }


    /**
     *       压缩哈夫曼数组为一个byte数组
     * @param bytes 需要压缩的字符串对应的byte数组
     * @param huffmanCode 根据每次字符出现的频率构建的哈夫曼编码表
     */
    public static byte[] zip(byte[] bytes, Map<Byte,String> huffmanCode){
        //将字符依照码表转换成二进制数并拼接字符串
        StringBuilder string = new StringBuilder();
        for (byte aByte : bytes) {
            string.append(huffmanCode.get(aByte));
        }
        //byte的数组长度,防止不能整除八的情况下数组长度也足够
        int len = (string.length() + 7) / 8;
        byte[] codeBytes = new byte[len];
        //统计该向codeBytes数组的哪个下标添加数据
        int index = 0;
        for (int i = 0; i < string.length(); i+=8) {
            String str;
            //防止越界,如果剩余长度不足8,则剩余的全部加入数组
            if (i+8 > string.length()){
                str = string.substring(i);
            } else {
                 str = string.substring(i, i+8);
            }
            //Integer.parseInt(str, 2),表示输出的是2进制数。
            codeBytes[index] = (byte) Integer.parseInt(str, 2);
            index++;
        }
        return codeBytes;

    }
    public static void toHuffmanCodeMap(Node node, String code, StringBuilder stringBuilder){
        //将上一次递归拼接的字符串获取
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //继续此次拼接
        stringBuilder2.append(code);
        if (node != null){
            if (node.data == null){
                //说明不是叶子节点,向左递归
                toHuffmanCodeMap(node.left, "0", stringBuilder2);
                //向右递归
                toHuffmanCodeMap(node.right, "1", stringBuilder2);
            } else {
                //没有左右子节点,说明是叶子节点
                //将上面递归拼接的路径 存入map中
                codeMap.put(node.data, String.valueOf(stringBuilder2));
            }
        }

    }
    //创建哈夫曼树
    public static Node creatHuffmanTree(List<Node> nodes){
        //当集合中的节点只剩一个的时候,说明构建完成,退出循环
        while (nodes.size() > 1){
            //首先升序排列
            Collections.sort(nodes);
            //取出最小和次小权值的两个节点,作为左子节点和右子节点
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            //新建一个父节点,权值为左右子节点权值之和
            Node parentsNode = new Node(leftNode.weight+rightNode.weight);
            //将父节点的左右子节点设置为上面的两个左右节点
            parentsNode.left = leftNode;
            parentsNode.right = rightNode;

            //将左右子节点从集合中删除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将父节点加入集合,在下次循环的时候会重新排序
            nodes.add(parentsNode);
        }
        return nodes.get(0);
    }
    //将每个字符和出现的频率作为数据、权值创建节点并储存入集合中,方便创建哈夫曼树
    public static List<Node> nodeToList(byte[] bytes){
        //定义存储字符、频次的map集合
        Map<Byte, Integer> hashMap = new HashMap<>();
        //遍历集合
        for (byte aByte : bytes) {
            //判断此字符是否为第一次出现
            Integer count = hashMap.get(aByte);
            if (count == null){
                //第一次出现
                hashMap.put(aByte, 1);
            } else {
                //不是第一次出现
                hashMap.put(aByte, count+1);
            }
        }
        //遍历map集合,将其封装成节点存入集合
        List<Node> nodeList = new ArrayList<>();
        for (Map.Entry<Byte, Integer> entry : hashMap.entrySet()) {
            Node node = new Node(entry.getKey(), entry.getValue());
            nodeList.add(node);
        }
        return nodeList;
    }
}

数据解压实现步骤

  1. 首先需要将压缩后的byte数组,转换回根据编码表编码而成的二进制的字符串。
  2. 步骤1,其中需要用到位运算的知识
  3. 然后,将编码表的key value调换,做成解码表
  4. 将二进制字符串根据解码表,解码成byte数组
  5. 将此数组转换成String字符串

关于步骤2中的一些额外知识

byte类型转换成int类型二进制数时,如果是正数,则为其二进制数。如果是负数,不足32位的用1补全,后八位为其补码。

也就是说,byte= -88,转换成int,就是111111111111111111111110101000

byte = 1,转换成int就是 1,2就是10。

在上面压缩数据的过程中,我们是截取每八位一个字符串当做二进制补码转换成byte的,因此,解压缩时也要将其转换成二进制的补码。

所以如果是正数则需要与256进行 按位或运算:1 | 256 = 000000001 | 100000000(8个0),结果为100000001,然后截取后8位则为00000001 即为1的补码。

以上步骤中的 按位或 运算的具体方法是:

参加运算的两个数,按二进制位进行“或”运算。

运算规则:参加运算的两个数只要两个数中的一个为1,结果就为1。

即 0 | 0= 0 , 1 | 0= 1 , 0 | 1= 1 , 1 | 1= 1 。

例:2 | 4 即 00000010 | 00000100 = 00000110 ,所以2 | 4的值为 6 。

还有其他位运算符,可以移步:点击查看:位运算(按位与、按位或、异或)

代码实现

package huffman_code;

import java.util.*;

public class HuffmanCode {
    public static void main(String[] args) {
        String str = "Hello hello world love haha";
        System.out.println("原数据:"+str);
        byte[] zip = zip(str);
        System.out.println("压缩后:"+Arrays.toString(zip));
        String unzip = unzip(zip);
        System.out.println("解压后:"+unzip);
    }

    static Map<Byte,String> codeMap = new HashMap<>();
    static StringBuilder stringBuilder = new StringBuilder();
    //整合解压的方法
    public static String unzip(byte[] bytes){
        String str = byteToStr(bytes);
        return decoding(str, codeMap);
    }
    //将二进制字符串解码
    public static String decoding(String hashCode, Map<Byte,String> codeMap){

        //先将编码表的key value反过来,变成解码表
        Map<String, Byte> decodingMap = new HashMap<>();
        for (Map.Entry<Byte, String> entry : codeMap.entrySet()) {
            decodingMap.put(entry.getValue(), entry.getKey());
        }
        //存放每个符号的ASCII码值
        List<Byte> byteList = new ArrayList<>();
        //用来表示开始对比的首位1,或0的下标,循环中的i为subString截取的最后一个0、1,它后面一位的下标。
        int start = 0;
        for (int i = 0; i < hashCode.length(); i++) {
            //遍历传入的二进制hashCode字符串,与解码表对比,得到一个字符,就加入到集合中
            String substring = hashCode.substring(start, i);
            if (decodingMap.get(substring) != null){
                //表示在解码表中有此字符
                byteList.add(decodingMap.get(substring));
                //让start=i,重新开始对比扫描
                start = i;
            } else if (i == hashCode.length()-1 ){
                //因为subString是左闭右开,在最后一段的时候会少截一个字符,所以需要判断一下,i是否到达最后一个字符
                substring = hashCode.substring(start);
                byteList.add(decodingMap.get(substring));
            }
        }
        //将集合中的byte取出,放入数组,方便转换成字符串
        byte[] decodingBytes = new byte[byteList.size()];
        for (int i = 0; i < decodingBytes.length; i++) {
            //遍历集合,放入数组
            decodingBytes[i] = byteList.get(i);
        }
        //将数组中的byte依照ASCII码表转换成字符串
        String decodingStr = new String(decodingBytes);
        return decodingStr;
    }

    //解码:1、将压缩的byte数组,变成二进制字符串
    public static String byteToStr(byte[] bytes){
        String string;//表示将byte转换成的String字符串
        String substring;//因为负数转换成二进制补码的时候是32位,系统会自动用1补全前面的位数,所以需要截取字符串
        StringBuilder stringBuilder = new StringBuilder();//用来拼接转换后的字符串
        for (int i = 0; i < bytes.length; i++) {
            //如果是正数,会不足八位,需要用0填充,所以使用 按位或 256运算,256的二进制位100000000,后面是8个0.
            if (bytes[i] >= 0 && i < bytes.length - 1){
                int temp = bytes[i] | 256;
                string= Integer.toBinaryString(temp);
                //按位或运算以后,是首位为1的9位二进制数,所以需要截取后八位
                substring = string.substring(string.length() - 8);
            } else if (bytes[i] >= 0 && i == bytes.length - 1){
                //表示是最后一串正数二进制,在压缩的时候可能是不足八位的,所以无需补位。
                substring = Integer.toBinaryString(bytes[i]);
            } else {
                //表示是负数,截取后八位即可
                string= Integer.toBinaryString(bytes[i]);
                substring = string.substring(string.length() - 8);
            }
            //拼接截取后的字符串
            stringBuilder.append(substring);
        }
        return stringBuilder.toString();
    }



    //整合之前的方法
    public static byte[] zip(String str){
        byte[] bytes = str.getBytes();
        List<Node> nodeList = nodeToList(bytes);
        Node root = creatHuffmanTree(nodeList);
        toHuffmanCodeMap(root, "", stringBuilder);
        byte[] zipCode = toHuffmanCodeByte(bytes, codeMap);
        return zipCode;
    }


    /**
     * 依照编码表 编码 原字符串转换而成的byte数组为二进制字符串
     * 并看做每八位为一补码,压缩其为一个byte数组
     * @param bytes 需要压缩的字符串对应的byte数组
     * @param huffmanCode 根据每次字符出现的频率构建的哈夫曼编码表
     */
    public static byte[] toHuffmanCodeByte(byte[] bytes, Map<Byte,String> huffmanCode){
        //将字符依照码表转换成二进制数并拼接字符串
        StringBuilder string = new StringBuilder();
        for (byte aByte : bytes) {
            string.append(huffmanCode.get(aByte));
        }
        //byte的数组长度,防止不能整除八的情况下数组长度也足够
        int len = (string.length() + 7) / 8;
        byte[] codeBytes = new byte[len];
        //统计该向codeBytes数组的哪个下标添加数据
        int index = 0;
        for (int i = 0; i < string.length(); i+=8) {
            String str;
            //防止越界,如果剩余长度不足8,则剩余的全部加入数组
            if (i+8 > string.length()){
                str = string.substring(i);
            } else {
                 str = string.substring(i, i+8);
            }
            //Integer.parseInt(str, 2),表示输出的是2进制数。
            codeBytes[index] = (byte) Integer.parseInt(str, 2);
            index++;
        }
        return codeBytes;

    }
    //获得哈夫曼编码表
    public static void toHuffmanCodeMap(Node node, String code, StringBuilder stringBuilder){
        //将上一次递归拼接的字符串获取
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //继续此次拼接
        stringBuilder2.append(code);
        if (node != null){
            if (node.data == null){
                //说明不是叶子节点,向左递归
                toHuffmanCodeMap(node.left, "0", stringBuilder2);
                //向右递归
                toHuffmanCodeMap(node.right, "1", stringBuilder2);
            } else {
                //没有左右子节点,说明是叶子节点
                //将上面递归拼接的路径 存入map中
                codeMap.put(node.data, String.valueOf(stringBuilder2));
            }
        }

    }
    //创建哈夫曼树
    public static Node creatHuffmanTree(List<Node> nodes){
        //当集合中的节点只剩一个的时候,说明构建完成,退出循环
        while (nodes.size() > 1){
            //首先升序排列
            Collections.sort(nodes);
            //取出最小和次小权值的两个节点,作为左子节点和右子节点
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            //新建一个父节点,权值为左右子节点权值之和
            Node parentsNode = new Node(leftNode.weight+rightNode.weight);
            //将父节点的左右子节点设置为上面的两个左右节点
            parentsNode.left = leftNode;
            parentsNode.right = rightNode;

            //将左右子节点从集合中删除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            //将父节点加入集合,在下次循环的时候会重新排序
            nodes.add(parentsNode);
        }
        return nodes.get(0);
    }
    //将每个字符和出现的频率作为数据、权值创建节点并储存入集合中,方便创建哈夫曼树
    public static List<Node> nodeToList(byte[] bytes){
        //定义存储字符、频次的map集合
        Map<Byte, Integer> hashMap = new HashMap<>();
        //遍历集合
        for (byte aByte : bytes) {
            //判断此字符是否为第一次出现
            Integer count = hashMap.get(aByte);
            if (count == null){
                //第一次出现
                hashMap.put(aByte, 1);
            } else {
                //不是第一次出现
                hashMap.put(aByte, count+1);
            }
        }
        //遍历map集合,将其封装成节点存入集合
        List<Node> nodeList = new ArrayList<>();
        for (Map.Entry<Byte, Integer> entry : hashMap.entrySet()) {
            Node node = new Node(entry.getKey(), entry.getValue());
            nodeList.add(node);
        }
        return nodeList;
    }
}
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
编码是一种数据压缩算法,用于将数据压缩为更小的数据集。在哈编码中,频率越高的字符被编码为较短的编码,频率较低的字符被编码为较长的编码。以下是一个基本的Java实现: 1. 定义一个节点类,表示哈树上的节点。 ```java class Node { char data; // 字符 int freq; // 频率 Node left, right; // 左子树和右子树 // 构造函数 public Node(char data, int freq) { this.data = data; this.freq = freq; left = null; right = null; } } ``` 2. 定义一个比较器类,用于按照频率比较节点。 ```java class NodeComparator implements Comparator<Node> { public int compare(Node node1, Node node2) { return node1.freq - node2.freq; } } ``` 3. 定义一个构建哈树的方法。 ```java public static Node buildHuffmanTree(char[] chars, int[] freqs) { PriorityQueue<Node> pq = new PriorityQueue<Node>(new NodeComparator()); // 将所有字符和频率的节点加入优先队列 for (int i = 0; i < chars.length; i++) { pq.offer(new Node(chars[i], freqs[i])); } // 构建哈树 while (pq.size() > 1) { Node left = pq.poll(); Node right = pq.poll(); Node parent = new Node('$', left.freq + right.freq); parent.left = left; parent.right = right; pq.offer(parent); } // 返回根节点 return pq.poll(); } ``` 4. 定义一个打印哈编码的方法。 ```java public static void printHuffmanCodes(Node root, String code) { if (root == null) { return; } if (root.data != '$') { System.out.println(root.data + ": " + code); } printHuffmanCodes(root.left, code + "0"); printHuffmanCodes(root.right, code + "1"); } ``` 5. 调用以上方法,实现编码。 ```java public static void main(String[] args) { char[] chars = { 'a', 'b', 'c', 'd', 'e', 'f' }; int[] freqs = { 5, 9, 12, 13, 16, 45 }; Node root = buildHuffmanTree(chars, freqs); printHuffmanCodes(root, ""); } ``` 以上代码将输出以下内容: ``` a: 1100 c: 100 b: 1110 e: 01 d: 101 f: 00 ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值