四方显神

导航

数据结构019_数据压缩(赫夫曼编码)

 一、基本介绍

  • 赫夫曼编码也翻译为哈夫曼编码(HuffmanCoding),是一种编码方式,也是一种程序算法。
  • 赫夫曼编码是赫夫曼树在电讯通信中的经典应用之一。
  • 赫夫曼编码也广泛用于文件压缩。其压缩率通常在20%~90%之间。
  • 赫夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法称为最佳编码。

二、原理剖析

通信领域中信息处理的方式

1)定长编码

2)变长编码:按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小。

比如:could you find a way to let me down slowly,比如空格出现9次,编码为0,其他依次类推。0、1、10、11、100、101........

3)赫夫曼编码

按照字符出现的次数构建一棵赫夫曼树,次数作为权值。根据赫夫曼树规定编码,向左的路径规定为0,向右为1。赫夫曼编码满足前缀编码。

字符的编码都不能是其他字符编码的前缀。符合此要求的编码叫做前缀编码,即不能匹配到重复的编码。(就是比如1是10,11的前缀,这样就可能存在二义性 )

注意:这个赫夫曼树根据排序方法不同,这样对应的赫夫曼编码也不完全一样。但WPL是一样的,最后生成的赫夫曼编码长度也是一样的。

三、实例

对字符串"i like like like java do you like a java"用赫夫曼编码进行数据压缩处理。(我还是要用老师这个辣鸡字符串,些出来也好对一下运行结果)

步骤:

1.创建字符串对应赫夫曼树

2.赫夫曼树生成赫夫曼编码

3.数据压缩-赫夫曼编码字节数据

4.方法封装-赫夫曼字节数组封装

【完整代码过阵子再拿出来码一遍!】

步骤一生成赫夫曼树代码(想看完整代码看步骤二):

package com.njcx.huffman;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.ws.EndpointReference;

import org.w3c.dom.traversal.NodeIterator;

public class HuffmanCode {
    // 1.创建字符串对应的赫夫曼树
    // 思路:(1)Node{data(存放数据),weight(权值),left,right}
    // (2)得到字符串对应的bete[]数组
    // (3)编写一个方法将准备构建赫夫曼树的Node节点放到list中,
    // 形式[Node[data=97,weight=5],node[]ata=32,weight=9...]
    // 体现d:1,y:1,u:1,......
    // (4)可以通过list创建对应的赫夫曼树
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();
        System.out.println(contentBytes.length);// 40

        List<Node> nodes = getNodes(contentBytes);
        System.out.println("nodes=" + nodes);

        // 测试一把创建的二叉树
        System.out.println("赫夫曼树:");
        Node huffmanTree = createHuffmanTree(nodes);
        huffmanTree.preOrder();
    }

    public static void preOrder(Node root) {
        if (root != null)
            root.preOrder();
        else
            System.out.println("空树");
    }

    /**
     * 
     * @param bytes
     *            接收一个字符数组
     * @return 返回的就是List,形式的Node[data='97',weight=5]
     */
    private static List<Node> getNodes(byte[] bytes) {

        // 创建一个arrayList
        ArrayList<Node> nodes = new ArrayList<Node>();

        // 存储每个byte出现的次数,用map存储,比较巧妙
        // 遍历bytes,统计每个byte出现的的次数,map[key,value]
        Map<Byte, Integer> counts = new HashMap<>();
        for (byte b : bytes) {
            Integer count = counts.get(b);// map.get(key)返回这个key对应的value
            if (count == null) { // map里还没有这个字符数据
                counts.put(b, 1);
            } else {
                counts.put(b, count + 1);// 我工作中遇到过这种,那时候我使用的是list
            }
        }

        // 把每个键值对转成一个Node对象,并加入到nodes集合
        // 遍历map【这里我比较陌生,在后面一篇博客里会单独做一下这个的笔记】
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    // 通过list创建对应的赫夫曼树
    private static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            // 排序 从小到大
            Collections.sort(nodes);
            // 取出两棵最小的二叉树
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            // 创建一个新的二叉树,没有data只有权值
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            // 移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            // 加入
            nodes.add(parent);
        }
        return nodes.get(0); // 最后只有一个节点,哈夫曼树的根结点
    }

}

// Node类,带数据和权值
class Node implements Comparable<Node> {
    Byte data;// 存放数据本身,比如'a'的data97
    int weight;// 权值,表示字符出现的次数
    Node left;
    Node right;

    // 这里data是Byte,可以是null,如果是byte,就不可以是null了
    public Node(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    @Override
    public int compareTo(Node o) {
        // 从小到大排序 这里又忘了,当前值小于o的值返回的是负数
        return this.weight - o.weight;
    }

    @Override
    public String toString() {
        return "Node [data=" + data + ", weight=" + weight + "]";
    }

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

}

 运行结果:

40
nodes=[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]]
赫夫曼树:
Node [data=null, weight=40]
Node [data=null, weight=17]
Node [data=null, weight=8]
Node [data=108, weight=4]
Node [data=null, weight=4]
Node [data=106, weight=2]
Node [data=111, weight=2]
Node [data=32, weight=9]
Node [data=null, weight=23]
Node [data=null, weight=10]
Node [data=97, weight=5]
Node [data=105, weight=5]
Node [data=null, weight=13]
Node [data=null, weight=5]
Node [data=null, weight=2]
Node [data=100, weight=1]
Node [data=117, weight=1]
Node [data=null, weight=3]
Node [data=121, weight=1]
Node [data=118, weight=2]
Node [data=null, weight=8]
Node [data=101, weight=4]
Node [data=107, weight=4]

步骤二:(生成赫夫曼编码、使用赫夫曼编码生成赫夫曼编码数据[包含赫夫曼编码字节数组,赫夫曼编码字节数组封装])

这里我就有点懵了,每一步都知道,但是代码太多了,那种全局概念非常模糊,就好像走迷宫的人只知道当前的路。

 

package com.njcx.huffman;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.ws.EndpointReference;

import org.w3c.dom.traversal.NodeIterator;

public class HuffmanCode {
    // 1.创建字符串对应的赫夫曼树
    // 思路:(1)Node{data(存放数据),weight(权值),left,right}
    // (2)得到字符串对应的bete[]数组
    // (3)编写一个方法将准备构建赫夫曼树的Node节点放到list中,
    // 形式[Node[data=97,weight=5],node[]ata=32,weight=9...]
    // 体现d:1,y:1,u:1,......
    // (4)可以通过list创建对应的赫夫曼树
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();
        System.out.println(contentBytes.length);// 40

        /*
         * 下面的代码都是分布过程,把这些封装到下面的方法中这里就不要了
         * 
         * List<Node> nodes = getNodes(contentBytes);
         * System.out.println("nodes=" + nodes); // 测试一把创建的二叉树
         * System.out.println("赫夫曼树:"); Node huffmanTreeRoot =
         * createHuffmanTree(nodes); huffmanTreeRoot.preOrder();
         * 
         * // // 测试一把是否生成了对应的赫夫曼编码 // getCodes(huffmanTreeRoot, "", sb); //
         * System.out.println("生成的赫夫曼编码表" + huffmanCodes); // //
         * 运行结果:生成的赫夫曼编码表{32=01, 97=100, 100=11000, 117=11001, 101=1110, // //
         * 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, //
         * 111=0011}
         * 
         * // 使用重载过的方法测试是否生成了对应的赫夫曼编码 Map<Byte, String> huffmancodes =
         * getCodes(huffmanTreeRoot); System.out.println("生成的赫夫曼编码表" +
         * huffmancodes);
         * 
         * // 测试 byte[] huffmanCodesByte = zip(contentBytes, huffmancodes);
         * System.out.println("huffmanCodesByte=" +
         * Arrays.toString(huffmanCodesByte));// 17 // 发送huffmanCodeBytes 数组
         * 
         */

        byte[] huffmanCodesBytes = huffmanZip(contentBytes);
        System.out.println("压缩后的结果是:" + Arrays.toString(huffmanCodesBytes));
        System.out.println("长度=" + huffmanCodesBytes.length);
    }

    /**
     * 使用一个方法将前面的方法封装,便于调用.
     * 
     * @param bytes
     *            原始的字符串对应的字节数组
     * @return 返回的是经过和和赫夫曼编码处理后的字节数组(压缩后的数组)
     */
    private static byte[] huffmanZip(byte[] bytes) {
        // 第一步
        List<Node> nodes = getNodes(bytes);
        // 第二步,根据nodes创建赫夫曼树
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        // 第三步,根据赫夫曼树生成对应的赫夫曼编码
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        // 第四步,根据生成的赫夫曼编码得到压缩后的赫夫曼编码字节数组
        byte[] huffmanCodeBytes = zip(bytes, huffmanCodes);

        return huffmanCodeBytes;
    }

    /**
     * 接收一个字符数组返回一个list
     * 
     * @param bytes
     *            接收一个字符数组
     * @return 返回的就是List,形式的Node[data='97',weight=5]
     */
    private static List<Node> getNodes(byte[] bytes) {

        // 创建一个arrayList
        ArrayList<Node> nodes = new ArrayList<Node>();

        // 存储每个byte出现的次数,用map存储,比较巧妙
        // 遍历bytes,统计每个byte出现的的次数,map[key,value]
        Map<Byte, Integer> counts = new HashMap<>();
        for (byte b : bytes) {
            Integer count = counts.get(b);// map.get(key)返回这个key对应的value
            if (count == null) { // map里还没有这个字符数据
                counts.put(b, 1);
            } else {
                counts.put(b, count + 1);// 我工作中遇到过这种,那时候我使用的是list
            }
        }

        // 把每个键值对转成一个Node对象,并加入到nodes集合
        // 遍历map【这里我比较陌生,在后面一篇博客里会单独做一下这个的笔记】
        for (Map.Entry<Byte, Integer> entry : counts.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }

    /**
     * 通过list创建对应的赫夫曼树
     * 
     * @param nodes
     * @return
     */
    private static Node createHuffmanTree(List<Node> nodes) {
        while (nodes.size() > 1) {
            // 排序 从小到大
            Collections.sort(nodes);
            // 取出两棵最小的二叉树
            Node leftNode = nodes.get(0);
            Node rightNode = nodes.get(1);
            // 创建一个新的二叉树,没有data只有权值
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            // 移除
            nodes.remove(leftNode);
            nodes.remove(rightNode);
            // 加入
            nodes.add(parent);
        }
        return nodes.get(0); // 最后只有一个节点,哈夫曼树的根结点
    }

    // 生成赫夫曼对应的赫夫曼编码
    // 思路:1.将赫夫曼编码表存放在Map<Byte,String>
    // 形式大概是 32->01,97->100,100->11000,
    static Map<Byte, String> huffmanCodes = new HashMap<Byte, String>();
    // 2. 在生成赫夫曼编码表时需要不停地拼接路径,定义一个StringBuilder存储某个叶子节点的路径
    static StringBuilder sb = new StringBuilder();

    /**
     * 功能:将传入的node节点的所有叶子节点的赫夫曼编码得到,并放入到huffmanCodes集合中
     * 
     * @param node
     *            传入的节点
     * @param code
     *            路径,左子节点是0,右子节点是1
     * @param sb
     *            用于拼接路径
     */
    private static void getCodes(Node node, String code, StringBuilder sb) {
        StringBuilder sb2 = new StringBuilder(sb);
        // 将code加入到sb2
        sb2.append(code);
        if (node != null) {// 如果node==null不处理
            // 判断当前node是叶子节点还是非叶子节点
            if (node.data == null) {// 非叶子节点
                // 递归处理
                // 向左递归
                getCodes(node.left, "0", sb2);
                // 向右递归
                getCodes(node.right, "1", sb2);
            } else { // 叶子节点
                        // 表示找到了某个叶叶子节点的路径
                huffmanCodes.put(node.data, sb2.toString());
            }
        }
    }

    /**
     * 功能:为了调用方便,重载getCodes方法
     * 
     * @param root
     * @return
     */
    private static Map<Byte, String> getCodes(Node root) {
        if (root == null)
            return null;
        getCodes(root.left, "0", sb);
        getCodes(root.right, "1", sb);
        return huffmanCodes;
    }

    /**
     * 将字符串对应的byte[]数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的byte数组
     * 
     * @param bytes
     *            原始的字符串对应的byte[]
     * @param huffmanCodes
     *            生成的赫夫曼编码map
     * @return 返回赫夫曼编码处理后的一个byte[] 举例:String content
     *         =101010001011111111001000101111111100100010111111110010010100110111
     *         +0001110000011011101000111100101000101111111100110001001010011011100
     *         "i like like like java do you like a java" =>byte[] contentBytes
     *         = content.getByte[] 返回的是字符串对应的byte[],字符串是: =>对应的byte[]
     *         huffmanCodeBytes,即8位对应一个byte,放入huffmanCodeBytes[]
     *         huffmanCodeBytes[0]=10101000(补码)=>byte[推导10101000(补码)=>10100111(
     *         反码)=>11011000(原码)=>-88]
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        // 先利用赫夫曼编码表huffmanCodes将bytes转成赫夫曼编码对应的字符串
        StringBuilder sb = new StringBuilder();
        // 遍历bytes数组
        for (byte b : bytes) {
            sb.append(huffmanCodes.get(b));
        }

        // System.out.println("测试stringbuilder=" + sb.toString());

        // 将stringbuilder对应的字符串转成byte[]

        // 统计返回的byte[] huffmanCodeBytes长度
        // int len;
        // if(sb.length()%8==0){
        // len = sb.length()/8;
        // }else{
        // len=sb.length()/8+1;
        // }
        // 上面的六行可以用一句话写
        // int len = sb.length() % 8 == 0 ? sb.length() / 8 : sb.length() / 8 +
        // 1;
        // 也不是这个意思,太投机了,是真的用另一种方式的一句话:
        int len = (sb.length() + 7) / 8;// 【*】

        // 创建一个存储压缩后的byte[]
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0;// 纪录是第几个byte

        // 步长为8,因为时每8位对应一个byte,所以步长是8
        for (int i = 0; i < sb.length(); i += 8) {
            String strByte;
            if (i + 8 > sb.length()) { // 不够八位
                strByte = sb.substring(i);
            } else {
                strByte = sb.substring(i, i + 8);
            }

            // 将strByte转成一个byte,放入到huffmanCodeBytes中
            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);// 【*】
            index++;
        }
        return huffmanCodeBytes;
    }

    public static void preOrder(Node root) {
        if (root != null)
            root.preOrder();
        else
            System.out.println("空树");
    }

}

// Node类,带数据和权值
class Node implements Comparable<Node> {
    Byte data;// 存放数据本身,比如'a'的data97
    int weight;// 权值,表示字符出现的次数
    Node left;
    Node right;

    // 这里data是Byte,可以是null,如果是byte,就不可以是null了
    public Node(Byte data, int weight) {
        this.data = data;
        this.weight = weight;
    }

    @Override
    public int compareTo(Node o) {
        // 从小到大排序 这里又忘了,当前值小于o的值返回的是负数
        return this.weight - o.weight;
    }

    @Override
    public String toString() {
        return "Node [data=" + data + ", weight=" + weight + "]";
    }

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

}

 

 运行结果:

40
压缩后的结果是:[-88, -65, -56, -65, -56, -65, -55, 77, -57, 6, -24, -14, -117, -4, -60, -90, 28]
长度=17

 

posted on 2020-11-04 21:39  szdbjooo  阅读(350)  评论(0编辑  收藏  举报