赫夫曼树的创建,赫夫曼编码的原理及使用

目录

一、创建赫夫曼树

代码实现:最后返回值式创建好的赫夫曼树的顶点

对int[] arr = {13, 7, 8, 3, 29, 6, 1};进行赫夫曼树,我们创建好的node数组依次是这样变化

创建节点:节点有前序遍历方法,之后遍历树时只需利用树的根节点来调用遍历方法

遍历方法:

二、赫夫曼编码的应用(压缩->还原)

代码实现

把字符串转为字节数组

把字节数组转为带有字节和此字节出现次数的list集合并返回

创建赫夫曼树并返回根节点(遍历用)

生产赫夫曼编码(使用Map来存储对应关系)

将原来的字符串按照赫夫曼编码组合起来(用StringBuilder)并以八位一分割转为10进制数字构成的数组

还原:根据赫夫曼编码和数字数组还原为二进制编码,再将Map翻转,依据Map把二进制编码转为Ascii码,再根据Ascii码还原为英文的字符串

在将字节转为二进制字符串时有一点要注意,传输过来是补码,需要分情况还原为源码

三、赫夫曼编码的使用例子

压缩

解压


 

一、创建赫夫曼树

构成赫夫曼树的步骤:

1) 从小到大进行排序, 将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树

2) 取出根节点权值最小的两颗二叉树

3) 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和 

4) 再将这颗新的二叉树,以根节点的权值大小 再次排序, 不断重复  1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗赫夫曼树

代码实现:最后返回值式创建好的赫夫曼树的顶点

public static Node createHuffmanTree(int[] arr) {
        //数组不支持排序,我们把数组元素装到Node中,然后把这些Node装到list中
        ArrayList<Node> list = new ArrayList<>();
        for (int i = 0; i < arr.length ; i++) {
            list.add(new Node(arr[i]));//节点的创建只有一个左指针和右指针以及自己的值
        }

        //循环操作的过程
        while (list.size() > 1) {
            //排序:从小到大
            Collections.sort(list);
            //取出根节点权值最小的两个二叉树,此时一个节点就是一个二叉树
            //①取出第一个值
            Node leftNode = list.get(0);
            //②取出第二个值
            Node rightNode = list.get(1);
            //③构建成树
            Node parent = new Node(leftNode.value + rightNode.value);
            parent.left = leftNode;
            parent.right = rightNode;
            //从list中删除leftNode,rightNode
            list.remove(leftNode);
            list.remove(rightNode);
            //把parent在加入到list中
            list.add(parent);

            System.out.println(list);
        }
//得到了赫夫曼树的根节点
        return list.get(0);
   }
}

对int[] arr = {13, 7, 8, 3, 29, 6, 1};进行赫夫曼树,我们创建好的node数组依次是这样变化

[Node{value=6}, Node{value=7}, Node{value=8}, Node{value=13}, Node{value=29}, Node{value=4}]
[Node{value=7}, Node{value=8}, Node{value=13}, Node{value=29}, Node{value=10}]
[Node{value=10}, Node{value=13}, Node{value=29}, Node{value=15}]
[Node{value=15}, Node{value=29}, Node{value=23}]
[Node{value=29}, Node{value=38}]
[Node{value=67}]

创建节点:节点有前序遍历方法,之后遍历树时只需利用树的根节点来调用遍历方法

//创建节点类
//为了让节点实现排序,所以让它实现comparable接口
class Node implements Comparable<Node> {


    int value;
    Node left;
    Node right;

    //前序遍历的方法
    public void perOrder() {
        System.out.println(this);
        if (this.left != null) {
            this.left.perOrder();
        }
        if (this.right != null) {
            this.right.perOrder();
        }
    }


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

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

    @Override
    public int compareTo(Node n) {
        return this.value - n.value;
    }
}

遍历方法:

 public static void perOrder(Node root) {
    if (root != null) {
            root.perOrder();
        } else {
            System.out.println("空的树,没法遍历");
     }
 }

遍历结果:

Node{value=67}
Node{value=29}
Node{value=38}
Node{value=15}
Node{value=7}
Node{value=8}
Node{value=23}
Node{value=10}
Node{value=4}
Node{value=1}
Node{value=3}
Node{value=6}
Node{value=13}

二、赫夫曼编码的应用(压缩->还原)

1) 传输一个字符串:i like like like java do you like a java  (包括空格在内有40个字符)

2)通信领域中信息的处理方式1-定长编码

对应的Ascii码为: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

3)此时转为二进制为:01101001 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 00100000 01101010 01100001 01110110 01100001 00100000 01100100 01101111 00100000 01111001 01101111 01110101 00100000 01101100 01101001 01101011 01100101 00100000 01100001 00100000 01101010 01100001 01110110 01100001 (共有359位)

4)  通信领域中信息的处理方式2-变长编码

        d:1 y:1 u:1 j:2  v:2  o:2  l:4  k:4  e:4 i:5  a:5   :9  // 各个字符对应的个数

        按照上面字符出现的次数构建一颗赫夫曼树, 次数作为权值

5)在赫夫曼树的基础上左边用0表示,右边用1表示        

6) 此时每一个字母有了自己对应的二进制编码,而且编码有一个特点:从第一位开始不会有重复的,每一个都是独立的,不会出现长的编码包含短的编码的情况

o: 1000   u: 10010  d: 100110  y: 100111  i: 101

a : 110     k: 1110    e: 1111       j: 0000       v: 0001

l: 001          : 01

7)  按照上面的赫夫曼编码,我们的"i like like like java do you like a java"   字符串对应的编码为 (注意这里我们使用的无损压缩)

1010100110111101111010011011110111101001101111011110100001100001110011001111000011001111000100100100110111101111011100100001100001110  通过赫夫曼编码处理  长度为  133

8)此时赫夫曼编码的作用体现出来了,因为计算机底层传输都是用的二进制编码,我们把原先的355位压缩到了133位

9)此时传输并不奏效,反而加大了工作量,原先的40个字符,现在有133个,但是二进制数可以八位为一体成为字节,所以变为[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]为17位

10)把这17为数字和赫夫曼编码集一同发送,按照赫夫曼编码集还原,从而实现数据的压缩--->还原

代码实现

把字符串转为字节数组

String str = "i like like like java do you like a java";
  byte[] bytes = str.getBytes();

此时字符串变为:

[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]

把字节数组转为带有字节和此字节出现次数的list集合并返回

public static List<Node> toList(byte[] bytes) {

        ArrayList<Node> list = new ArrayList<>();
//        遍历bytes,统计每一个byte出现的次数,用map的key和value表示
        HashMap<Byte, Integer> map = new HashMap<>();

        for (Byte b : bytes) {
            Integer count = map.get(b);
            if (count == null) {
                map.put(b, 1);
            } else {
                map.put(b, count + 1);
            }
        }

//        把每一个键值对转成Node对象,并加入到nodes集合中
        for (Map.Entry<Byte, Integer> e : map.entrySet()) {
            list.add(new Node(e.getKey(), e.getValue()));
        }

        return list;
    }

 此时list为:

[Node{data=32, weight=9}, Node{data=97, weight=5}, Node{data=100, weight=1}, Node{data=101, weight=4}, Node{data=117, weight=1}, Node{data=118, weight=2}, Node{data=105, weight=5}, Node{data=121, weight=1}, Node{data=106, weight=2}, Node{data=107, weight=4}, Node{data=108, weight=4}, Node{data=111, weight=2}]

创建赫夫曼树并返回根节点(遍历用)

 public static Node creadHuffmanTree(List<Node> list) {

        while (list.size() > 1) {
            Collections.sort(list);

            Node leftNode = list.get(0);
            Node rightNode = list.get(1);

            Node parent = new Node(null, leftNode.weight + rightNode.weight);

            parent.left = leftNode;
            parent.right = rightNode;

            list.remove(leftNode);
            list.remove(rightNode);

            list.add(parent);
        }

        return list.get(0);
    }

根节点为:Node{data=null, weight=40}

生产赫夫曼编码(使用Map来存储对应关系)

//重载以下方法
    public static Map<Byte, String> creatHuffmanCodes(Node root) {
        if (root != null) {
            creatHuffmaCodes(root.left, "0", stringBuilder);
            creatHuffmaCodes(root.right, "1", stringBuilder);
            return huffmanCodes;
        } else {
            return null;
        }

    }

    //    生成赫夫曼树对应的赫夫曼编码
//    思路:先考虑生成的码存放在哪里:我们用Map<Byte,String>来存放
    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
    //          在生成赫夫曼编码的过程中,需要不断的对路径进行拼接:我们定义StringBuilder来存放叶子节点的路径
    static StringBuilder stringBuilder = new StringBuilder();

    public static void creatHuffmaCodes(Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        stringBuilder2.append(code);
        if (node != null) {//有节点才处理
            if (node.data == null) {//非叶子节点
                //向左递归
                creatHuffmaCodes(node.left, "0", stringBuilder2);
                //向右递归
                creatHuffmaCodes(node.right, "1", stringBuilder2);
            } else {//叶子节点
                huffmanCodes.put(node.data, stringBuilder2.toString());
            }
        }
    }

此时赫夫曼编码为:

{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011} 

将原来的字符串按照赫夫曼编码组合起来(用StringBuilder)并以八位一分割转为10进制数字构成的数组

public static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        StringBuilder stringBuilder = new StringBuilder();

        for (byte b : bytes) {
            stringBuilder.append(huffmanCodes.get(b));
        }
        System.out.println("stringBuilder:" + stringBuilder);

        int len;//处理后byte[]的长度
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }

        byte[] by = new byte[len];
        int index = 0;
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            String substring;
            if (i + 8 > stringBuilder.length()) {
                substring = stringBuilder.substring(i);
            } else {
                substring = stringBuilder.substring(i, i + 8);
            }

            //子字符串转成byte,放到by中
            by[index] = (byte) Integer.parseInt(substring, 2);
            index++;
        }
        return by;
    }

此时StringBuilder(二进制编码)为:

1010100010111111110010001011111111001000101111111100100101001101110001110000011011101000111100101000101111111100110001001010011011100

转为数字数组为:

[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]

还原:根据赫夫曼编码和数字数组还原为二进制编码,再将Map翻转,依据Map把二进制编码转为Ascii码,再根据Ascii码还原为英文的字符串

public static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
        //先把数字变成二进制数组
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < huffmanBytes.length; i++) {
            //判断是不是最后一个字节
            boolean flag = (i == huffmanBytes.length - 1);
            stringBuilder.append(bytesToBiteString(!flag, huffmanBytes[i]));
        }
        System.out.println("还原回来的:" + stringBuilder.toString());

        //把二进制字符串按照赫夫曼编码表进行解码
        //把赫夫曼编码表进行调换,反向查询
        HashMap<String, Byte> map = new HashMap<String, Byte>();
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }
        System.out.println("还原回来的:" + map);

        //创建一个集合,存放byte
        ArrayList<Byte> list = new ArrayList<>();
        //遍历StringBuilder,与map的key匹配
        for (int i = 0; i < stringBuilder.length(); ) {
            int count = 1;//count是用来遍历的,i直接跳到count的位置即可
            boolean flag = true;
            Byte b = null;
            while (flag) {
                String substring = stringBuilder.substring(i, i + count);
                b = map.get(substring);
                if (b == null) {//说明没有匹配到
                    count++;
                } else {
                    flag = false;
                }
            }
            list.add(b);
            i += count;
        }
        System.out.println("临时的ArraryList:" + list);
        //for循环结束以后,list中存放了:所有的字符:"i like like like java do you like a java";
        //把list中的数据放到byte[]中并返回
        byte[] bytes = new byte[list.size()];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = list.get(i);
        }
        return bytes;
    }

在将字节转为二进制字符串时有一点要注意,传输过来是补码,需要分情况还原为源码

 /**
     * 把一个字节转为二进制字符串
     *
     * @param flag 表示是否需要补高位,如果是true则需要补高位,反之不需要
     * @param b    传入的一个byte
     * @return b对应的二进制的字符串(补码形式)
     */
    public static String bytesToBiteString(boolean flag, byte b) {
        int temp = b;//将b转为int类型,因为Integer有toBinaryString()
        //如果是正数,要补高位
        if (flag) {
            temp |= 256;//按位与 1 0000 0000 | 0000 0001 =>1 0000 0001
        }
        String s = Integer.toBinaryString(temp);//返回的是temp对应的二进制的补码
        if (flag) {
            return s.substring(s.length() - 8);
        } else {
            return s;
        }
    }

三、赫夫曼编码的使用例子

我们上述的代码实现了压缩与还原,将其封装后便成了赫夫曼编码压缩文件和还原文件的两个方法

又因为赫夫曼编码是字节码,所以既可以实现文件的压缩,又可以实现图片、视频的压缩。有兴趣可以试试

文件的压缩传输解压需要用到io流

压缩

/**
     * @param srcFile 你传入的希望压缩的文件的全路径
     * @param dstFile 我们压缩后将压缩文件放到哪个目录
     */
    public static void zipFile(String srcFile, String dstFile) {

        //创建输出流
        OutputStream os = null;
        ObjectOutputStream oos = null;
        //创建文件的输入流
        FileInputStream is = null;
        try {
            //创建文件的输入流
            is = new FileInputStream(srcFile);
            //创建一个和源文件大小一样的byte[]
            byte[] b = new byte[is.available()];
            //读取文件
            is.read(b);
            //直接对源文件压缩
            byte[] huffmanBytes = getHuffmanCodesBytes(b);
            //创建文件的输出流, 存放压缩文件
            os = new FileOutputStream(dstFile);
            //创建一个和文件输出流关联的ObjectOutputStream
            oos = new ObjectOutputStream(os);
            //把 赫夫曼编码后的字节数组写入压缩文件
            oos.writeObject(huffmanBytes); //我们是把
            //这里我们以对象流的方式写入 赫夫曼编码,是为了以后我们恢复源文件时使用
            //注意一定要把赫夫曼编码 写入压缩文件
            oos.writeObject(huffmanCodes);
        } catch (Exception e) {
            // TODO: handle exception
            System.out.println(e.getMessage());
        } finally {
            try {
                is.close();
                oos.close();
                os.close();
            } catch (Exception e) {
                // TODO: handle exception
                System.out.println(e.getMessage());
            }
        }

    }

解压

   /**
     * @param zipFile 准备解压的文件
     * @param dstFile 将文件解压到哪个路径
     */
    public static void unZipFile(String zipFile, String dstFile) {

        //定义文件输入流
        InputStream is = null;
        //定义一个对象输入流
        ObjectInputStream ois = null;
        //定义文件的输出流
        OutputStream os = null;
        try {
            //创建文件输入流
            is = new FileInputStream(zipFile);
            //创建一个和  is关联的对象输入流
            ois = new ObjectInputStream(is);
            //读取byte数组  huffmanBytes
            byte[] huffmanBytes = (byte[]) ois.readObject();
            //读取赫夫曼编码表
            Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
            //解码
            byte[] bytes = decode(huffmanCodes, huffmanBytes);
            //将bytes 数组写入到目标文件
            os = new FileOutputStream(dstFile);
            //写数据到 dstFile 文件
            os.write(bytes);
        } catch (Exception e) {
            // TODO: handle exception
            System.out.println(e.getMessage());
        } finally {

            try {
                os.close();
                ois.close();
                is.close();
            } catch (Exception e2) {
                // TODO: handle exception
                System.out.println(e2.getMessage());
            }

        }
    }

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java塑造中...

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值