JAVA数据结构与算法:哈夫曼树的创建、哈夫曼编码、思路分析、代码实现

哈夫曼树

哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。

带权路径总和最小的就是哈夫曼树。即 WPL=叶子节点权*路径+…+叶子节点权*路径
在这里插入图片描述

中间的树就是哈夫曼树。

. 创建哈夫曼树

思路分析:

  • 将数列从小到大排序,此时每个数据就是一个节点
  • 取出前两个节点,作为子节点,计算出父节点的权值(就是两个节点的权值和)
  • 下一步就是将计算出的新父节点的权值放入数列中,重新排序,返回第二步
  • 往复,最终会得到一个哈夫曼树
    在这里插入图片描述
public class HuffmanTree {
    public static void main(String[] args) {
        int arr[] ={3,6,15,20};
        Node huffmanTree = createHuffmanTree(arr);
        if (huffmanTree!=null){
            huffmanTree.preOrder();
        }
    }

    public static Node createHuffmanTree(int[] arr){
        // 创建一个集合,存入创建的节点
        List<Node> nodeList = new ArrayList<>();
        for (int item : arr) {
            nodeList.add(new Node(item));
        }

        // 因为每次都会remove一些节点,最终会在list中剩下一个节点,这个节点就是根节点
        while (nodeList.size() > 1){
            // 从小到达排序list
            Collections.sort(nodeList);

            // 取出前两个最小的,第一个作为左节点,第二个作为右节点
            Node leftNode = nodeList.get(0);
            Node rightNode = nodeList.get(1);

            // 将权重+路径和赋值给父节点,将父节点的左右节点挂上
            Node parentNode = new Node(leftNode.getValue()+rightNode.getValue());
            parentNode.setLeft(leftNode);
            parentNode.setRight(rightNode);

            // 移除最小的两个节点,将父节点放入list集合中,进行下一轮
            nodeList.remove(leftNode);
            nodeList.remove(rightNode);
            nodeList.add(parentNode);
        }
        // 返回最终的根节点
        return nodeList.get(0);
    }
}

class Node implements Comparable<Node>{
    private int value;
    private Node left;
    private Node right;

    public Node(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public Node getLeft() {
        return left;
    }

    public void setLeft(Node left) {
        this.left = left;
    }

    public Node getRight() {
        return right;
    }

    public void setRight(Node right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    // 从小到大排序
    @Override
    public int compareTo(Node node) {
        return this.value - node.value;
    }

    // 前序遍历
    public void preOrder(){
        System.out.println(this.toString());
        if (this.left!=null){
            this.left.preOrder();
        }
        if (this.right!=null){
            this.right.preOrder();
        }
    }
}

结果:

Node{value=44}
Node{value=20}
Node{value=24}
Node{value=9}
Node{value=3}
Node{value=6}
Node{value=15}

哈夫曼编码

哈夫曼编码的优点:

  • 能够压缩20%~90%的数据
  • 无损压缩
  • 此编码满足前缀编码,不会造成匹配多义性

需要注意的是:

  • 在数据排序时不稳定而结果会造成最终的编码结果不同,但是编码后的长度是一样的。
  • 数据重复不多,压缩效果不明显。

. 思路分析

这里有一串数据:“i love love love java you”

  • 先统计数据中每个字符出现的次数,即:

    i:1 l:3 o:4 v:4 e:3 j:1 a:2 y:1 u:1 :5

    i有1个,l有3个,o有4个,v有4个,e有3个,j有1个,a有2个,y有1个,u有1个,空格有5个

  • 然后个数就是权重,根据次构建一个哈夫曼树

  • 通过创建成功的哈夫曼树,到达每个节点的路径就是该字母的编码,从根节点出发,往左走就是0,往右走就是1

  • 将遍历哈夫曼树,得出每个叶子节点也就是每个字符的哈夫曼编码,存储到map集合中形成编码表,然后字符串根据编码表找到对应的编码字符串,每8位一组存储到byte数组中,形成编码后的结果。

  • 解码,将byte数组,每一个数转换成8位的二进制字符串,扫描字符串依次与哈夫曼编码表的反转map比较,找到对应的字节输出。

. 代码实现

public class HuffmanCode {
    public static void main(String[] args) {
        String content = "i love love love java you";
        System.out.println("字符串:"+content);
        byte[] encode = encode(content);
        System.out.println("压缩后得到的哈夫曼编码字节数组:"+Arrays.toString(encode));
        byte[] decode = decode(encode);
        System.out.println("解码后:"+new String(decode));
    }

    // 获得一个哈夫曼树的根节点
    public static CodeNode getHuffmanTree(byte[] contentBytes){
        // 将字符串转换成字节,统计每个字节出现的次数存储到map中
        Map<Byte, Integer> contentMap = new HashMap<>();
        for (byte contentByte : contentBytes) {
            if (contentMap.get(contentByte) == null){
                contentMap.put(contentByte,1);
            }else {
                contentMap.put(contentByte,contentMap.get(contentByte)+1);
            }
        }

        // 遍历map,每个entry创建一个codeNode节点,存储到list中便于创建哈夫曼树
        List<CodeNode> codeNodeList = new ArrayList<>();
        for(Map.Entry<Byte,Integer> mapEntry: contentMap.entrySet()){
            codeNodeList.add(new CodeNode(mapEntry.getKey(),mapEntry.getValue()));
        }

        // 创建哈夫曼树
        while (codeNodeList.size() > 1){
            Collections.sort(codeNodeList);

            CodeNode leftNode = codeNodeList.get(0);
            CodeNode rightNode = codeNodeList.get(1);
            CodeNode parentNode = new CodeNode(null,leftNode.getWeight()+rightNode.getWeight());
            parentNode.setLeft(leftNode);
            parentNode.setRight(rightNode);

            codeNodeList.remove(leftNode);
            codeNodeList.remove(rightNode);
            codeNodeList.add(parentNode);
        }
        return codeNodeList.get(0);
    }

    // 根据哈夫曼树,左路为0,右路为1,获得编码表
    static Map<Byte, String> codeMap = new HashMap<>();
    public static Map<Byte,String> getCodeMap(CodeNode node, String code, StringBuilder stringBuilder){
        StringBuilder strBuilder = new StringBuilder(stringBuilder);
        strBuilder.append(code);
        if (node != null){
            if (node.getData() == null){
                // 向左
                getCodeMap(node.getLeft(),"0",strBuilder);
                // 向右
                getCodeMap(node.getRight(),"1",strBuilder);
            }else {
                codeMap.put(node.getData(),strBuilder.toString());
            }
        }
        return codeMap;
    }

    // codeSize记录编码字符串的长度,用于解决编码后最后一个数存在01110这种类似情况的编码
    // 如果存在这种01110编码,在转为byte时存入14这个数字
    // 而在解码的时候,因为14是数组的最后一个,在转为二进制字符串如果直接追加1110,在对照编码表的时候
    // 会出错,因此需要判断在追加1110后,解析后的编码长度是不是加密时的编码长度一样,
    // 如果不一样就需要在1110前面追加0,直到与加密时的长度一致,才可以解析成功
    static int codeSize = 0;
    
    // 获取字符串的最终编码
    public static byte[] encode(String content){
        byte[] contentBytes = content.getBytes();
        // 创建哈夫曼树
        CodeNode huffmanTree = getHuffmanTree(contentBytes);

        // 获得哈夫曼树的编码表
        Map<Byte, String> codeMap = getCodeMap(huffmanTree, "", new StringBuilder());

        // 获得字符串的编码字符串
        StringBuilder encodeBuilder = new StringBuilder();
        for (byte contentByte : contentBytes) {
            encodeBuilder.append(codeMap.get(contentByte));
        }

        // 将编码字符串8位一组存入byte数组中
        String encodeString = encodeBuilder.toString();
        codeSize = encodeString.length(); // 存储编码有多少位
        // System.out.println(encodeString);
        // 输出:101010001011111111001000101111111100...
        // 获取前8位,10101000字符串转换成字节
        // Integer.parseInt("10101000",2):将10101000以2进制为基准解析成十进制的int类型,
        // 这里的10101000为补码,反码为10101000-1=10100111,正码为11011000,最高位为符号位得出-88
        int size = (encodeString.length()+7)/8;
        byte[] codeBytes = new byte[size];
        int count = 0;
        for (int i = 0; i < codeBytes.length; i++) {
            if (count+8 < encodeString.length()){
                codeBytes[i] = (byte) Integer.parseInt(encodeString.substring(count,count+8),2);
                count+=8;
            }else {
                codeBytes[i] = (byte) Integer.parseInt(encodeString.substring(count),2);
            }
        }
        return codeBytes;
    }

    // 反转编码表,用于解析使用
    public static Map<String,Byte> getReCodeMap(){
        Map<String, Byte> reCodeMap = new HashMap<>();
        for (Map.Entry<Byte, String> entry : codeMap.entrySet()) {
            reCodeMap.put(entry.getValue(),entry.getKey());
        }
        return reCodeMap;
    }

    public static byte[] decode(byte[] encode){
        StringBuilder decodeBuilder = new StringBuilder();
        Map<String, Byte> reCodeMap = getReCodeMap();
        // 获取哈夫曼编码字符串
        for (int i = 0; i < encode.length; i++) {
            if (encode[i] < 0){
                String str = Integer.toBinaryString(encode[i]);
                decodeBuilder.append(str.substring(str.length()-8));
            }else if (i != encode.length-1){
                // 如果是正数并且不是数组最后一位数,需要给正数补位,用256与运算补位即可
                int temp = encode[i] | 256;
                // 256的二进制是1 0000 0000与正数与运算
                // 比如256与77二进制100 1101与运算 得到1 0100 1101,截取8位即可
                String str = Integer.toBinaryString(temp);
                decodeBuilder.append(str.substring(str.length()-8));
            }else {
                // 最后一位不需要转换成8位数,直接添加到末尾即可
                String lastCode = Integer.toBinaryString(encode[i]);
                // 判断最后一个数转成二进制代码后,解析二进制编码总长度是否与加密时候一致
                if (codeSize != (i*8+lastCode.length())){
                    // 不一致,就在最后一个数前面加差数个的0字符串,然后重置最后一个编码
                    int gap = codeSize- (i*8+lastCode.length());
                    String str = "";
                    for (int j = 0; j < gap; j++) {
                        str+="0";
                    }
                    lastCode = str + lastCode;
                }
                decodeBuilder.append(lastCode);
            }
        }
        // System.out.println(decodeBuilder.toString());

        // 扫描字符串进行解析
        String decodeStr = decodeBuilder.toString();
        List<Byte> decodeList = new ArrayList<>();
        for (int i = 0; i < decodeStr.length(); ) {
            int count = 1;
            while (true){
                String sub = decodeStr.substring(i, i + count);
                Byte aByte = reCodeMap.get(sub);
                if (aByte!=null){
                    decodeList.add(aByte);
                    break;
                }else {
                    count++;
                }
            }
            i = i+count;
        }

        byte[] decode = new byte[decodeList.size()];
        int index = 0;
        for (Byte aByte : decodeList) {
            decode[index++] = aByte;
        }

        return decode;
    }

}

// 创建节点
class CodeNode implements Comparable<CodeNode>{
    private Byte data;  // 数据
    private int weight;  // 数据在content中出现的次数,权重
    private CodeNode left;
    private CodeNode right;

    public CodeNode(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    public Byte getData() {
        return data;
    }

    public void setData(Byte data) {
        this.data = data;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    public CodeNode getLeft() {
        return left;
    }

    public void setLeft(CodeNode left) {
        this.left = left;
    }

    public CodeNode getRight() {
        return right;
    }

    public void setRight(CodeNode right) {
        this.right = right;
    }

    @Override
    public String toString() {
        return "CodeNode{" +
                "data=" + data +
                ", weight=" + weight +
                '}';
    }

    @Override
    public int compareTo(CodeNode o) {
        return this.weight - o.weight;
    }

    public void preOrder(){
        System.out.println(this.toString());
        if (this.left!=null){
            this.left.preOrder();
        }
        if (this.right!=null){
            this.right.preOrder();
        }
    }
}

结果:

字符串:i love love love java you
压缩后得到的哈夫曼编码字节数组:[-7, 53, 100, -43, -109, 86, 47, 94, 19, 30]
解码后:i love love love java you
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值