【数据结构与算法】赫夫曼编码

赫夫曼编码

基本介绍

  1. 赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法。
  2. 赫夫曼编码是赫夫曼树在电讯通信中的经典应用之一,
  3. 赫夫曼编码广泛地应用于数据文件压缩,其压缩率通常在 20%~90% 之间。
  4. 赫夫曼编码是可变字长编码(VLC)的一种,Huffman 于 1952 年提出的一种编码。

注意:这个赫夫曼树根据排序方法不同,也可能不太一样,这样对应的赫夫曼编码也不完全一样,但是 wpl 是一样的,都是最小的。

数据压缩

思路分析

功能:根据赫夫曼编码压缩数据的原理,需要创建"i like like like java do you like a java"对应的赫夫曼树

思路:

  1. Node {data(存放数据),weight(权值),left,right}
  2. 得到"i like like like java do you like a java"对应的 byte[] 数组
  3. 编写一个方法将准备构建赫夫曼树的 Node 节点放到 List,形式:[Node[data = 97,weight = 5],…],体现 d:1 y:1 u:1 j:2 v:2 o:2 l:4 k:4 e:4 i:5 a:5 [空格]:9
  4. 可以通过 List 创建对应的赫夫曼树
  5. 将字符和权值加入到 List 集合
  6. 将得到的赫夫曼树的所有叶子结点的赫夫曼编码得到,并放入到 huffmanCodes 集合,得到赫夫曼编码表(规定:左子节点的路径为 0,右子节点的路径为 1)
  7. 将字符串对应的数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的数组

代码实现

public class HuffmanCode {
    public static void main(String[] args) {
        String content = "i like like like java do you like a java";
        byte[] contentBytes = content.getBytes();

        byte[] huffmanCodeBytes = huffmanZip(contentBytes);
        System.out.println(Arrays.toString(huffmanCodeBytes));
    }

    /**
     * 封装方法,便于调用
     *
     * @param bytes 原始的字符串对应的字节数组
     * @return 是经过赫夫曼编码压缩过后的字节数组
     */
    private static byte[] huffmanZip(byte[] bytes) {
        // 将字符和权值加入到 List 集合
        List<Node> nodes = getNodes(bytes);
        // 通过 List 创建对应的赫夫曼树
        Node huffmanTreeRoot = createHuffmanTree(nodes);
        // 生成赫夫曼编码
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        // 根据生成的赫夫曼编码,将字符数组进行压缩,得到压缩后的字符数组
        return zip(bytes, huffmanCodes);
    }

    /**
     * 编写方法,将字符串对应的 byte[] 数组,通过生成的赫夫曼编码表,返回一个赫夫曼编码压缩后的 byte[]
     *
     * @param bytes        原始字符串对应的 byte[]
     * @param huffmanCodes 生成的赫夫曼编码 map
     * @return 返回赫夫曼编码处理后的 byte[]
     */
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        // 1. 利用 StringBuilder 将 bytes 转成赫夫曼编码
        StringBuilder stringBuilder = new StringBuilder();
        // 遍历 bytes
        for (byte b : bytes) {
            stringBuilder.append(huffmanCodes.get(b));
        }
        // 将拼接的字符串转为 byte[]
        // 统计返回 byte[] huffmanCodeBytes 长度
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }
        // 创建存储压缩后的 byte 数组
        byte[] huffmanCodeBytes = new byte[len];
        int index = 0; // 记录是第几个 byte
        for (int i = 0; i < stringBuilder.length(); i += 8) { // 因为是每 8 位对应一个 byte,所以步长为 8
            String stByte;
            if (i + 8 > stringBuilder.length()) { // 不够 8 位
                stByte = stringBuilder.substring(i);
            } else {
                stByte = stringBuilder.substring(i, i + 8);
            }
            // 将 strByte 构成一个 byte,放入到 huffmanCodeBytes
            huffmanCodeBytes[index] = (byte) Integer.parseInt(stByte, 2);
            index++;
        }
        return huffmanCodeBytes;
    }


    // 生成赫夫曼树对应的赫夫曼编码
    // 思路
    // 1. 将赫夫曼编码表存放在 Map 形式
    static Map<Byte, String> huffmanCodes = new HashMap<>();
    // 2. 在生成赫夫曼编码表时,需要去拼接路径,,定义一个 StringBuilder 存储某个叶子结点的路径
    static StringBuilder stringBuilder = new StringBuilder();

    // 重载 getCodes
    private static Map<Byte, String> getCodes(Node root) {
        if (root == null) {
            return null;
        } else {
            // 处理 root 的左子树
            getCodes(root.left, "0", stringBuilder);
            // 处理 root 的右子树
            getCodes(root.right, "1", stringBuilder);
            return huffmanCodes;
        }
    }

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

    // 将字符和权值加入到 List 集合
    private static List<Node> getNodes(byte[] bytes) {
        // 1.创建一个 ArrayList
        ArrayList<Node> nodes = new ArrayList<>();

        // 遍历 bytes,统计每个字符出现的次数
        HashMap<Byte, Integer> count = new HashMap<>();
        for (byte b : bytes) {
            count.put(b, count.containsKey(b) ? count.get(b) + 1 : 1);
        }
        // 把每个键值对转为 Node 对象 并加入 nodes 集合
        for (Map.Entry<Byte, Integer> entry : count.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.remove(0);
            // 取出第二棵最小二叉树
            Node rightNode = nodes.remove(0);
            // 创建一棵新的二叉树,他的根节点没有 data 只有权值
            Node parent = new Node(null, leftNode.weight + rightNode.weight);
            parent.left = leftNode;
            parent.right = rightNode;
            // 将新的二叉树加入到 Node
            nodes.add(parent);
        }
        // nodes 最后的节点就是根节点
        return nodes.get(0);
    }
}

// 创建 Node,存放数据和权值
class Node implements Comparable<Node> {
    Byte data; // 存放字符本身
    int weight; // 权值,表示字符出现的次数
    Node left;
    Node right;

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

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

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

数据解压

思路分析

  1. 将压缩后的数组转成二进制字符串
  2. 通过赫夫曼编码表,将二进制字符串重新转换成原来的字符串

代码实现

// 解压
// 思路:
// 1. 将 huffmanCodeBytes 转成赫夫曼编码对应的二进制字符串
// 2. 赫夫曼编码对应的二进制字符串对照赫夫曼编码转成原来的字符串

/**
 * 编写一个方法完成解码
 *
 * @param huffmanCodes 赫夫曼编码表
 * @param huffmanBytes 赫夫曼编码得到的字节数组
 * @return 返回原来的字符串对应的数组
 */
private static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
    // 1. 先得到 HuffmanBytes 对应的二进制的字符串
    StringBuilder stringBuilder = new StringBuilder();
    // 2. 将 byte 数组换成二进制字符串
    for (int i = 0; i < huffmanBytes.length; i++) {
        // 判断是不是最后一个字节
        boolean flag = (i == huffmanBytes.length - 1);
        stringBuilder.append(byteBitString(!flag, huffmanBytes[i]));
    }
    // 把字符串指定的赫夫曼编码进行解码
    // 把赫夫曼编码表进行调换
    Map<String, Byte> map = new HashMap<>();
    for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
        map.put(entry.getValue(), entry.getKey());
    }
    // 创建集合,存放 byte
    List<Byte> list = new ArrayList<>();
    // i 可以理解成索引,扫描 stringBuilder
    for (int i = 0; i < stringBuilder.length(); ) {
        int count = 1; // 计数器,用于扫描
        boolean flag = true;
        Byte b = null;
        while (flag) {
            String key = stringBuilder.substring(i, i + count);
            b = map.get(key);
            if (b == null) { // 没有匹配到
                count++;
            } else { // 匹配到了
                flag = false;
            }
        }
        list.add(b);
        i += count; // i 直接移动到 count
    }
    // 当 for 循环结束后,我们 list 中就存放了所有的字符
    // 把 list 放入 byte 数组
    byte[] b = new byte[list.size()];
    for (int i = 0; i < b.length; i++) {
        b[i] = list.get(i);
    }
    return b;
}

/**
 * 将一个 byte 转换成二进制的字符串
 *
 * @param b    需要转换的 byte
 * @param flag 标识是否需要补高位
 * @return 返回二进制字符串
 */
private static String byteBitString(boolean flag, byte b) {
    // 使用变量保存 b
    int temp = b; // 将 b 转为 int
    // 如果是整数我们需要补高位
    if (flag) {
        temp |= 256;
    }
    String str = Integer.toBinaryString(temp); // 返回的是 temp 对应的二进制补码
    if (flag) {
        return str.substring(str.length() - 8);
    } else {
        return str;
    }
}

文件压缩

/**
 * 编写方法,将一个文件进行压缩
 *
 * @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[] huffmanZip = huffmanZip(b);
        // 创建文件输出流
        os = new FileOutputStream(dstFile);
        // 创建一个和文件输出流关联的 ObjectOutputStream
        oos = new ObjectOutputStream(os);
        // 把赫夫曼编码后的数组写入压缩文件
        oos.writeObject(huffmanZip);
        // 这里以对象流的方式写入赫夫曼编码,是为了以后恢复源文件时使用
        oos.writeObject(huffmanCodes);
    } catch (Exception e) {
        System.out.println(e.getMessage());
    } finally {
        try {
            is.close();
            os.close();
            oos.close();
        } catch (Exception e) {
            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);
        // 创建对象输入流
        ois = new ObjectInputStream(is);
        // 读取 byte 数组
        byte[] huffmanBytes = (byte[]) ois.readObject();
        // 读取赫夫曼编码表
        Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();

        // 解码
        byte[] bytes = decode(huffmanCodes, huffmanBytes);
        // 将 bytes 写入目标文件
        os = new FileOutputStream(dstFile);
        // 写数据到文件
        os.write(bytes);
    } catch (Exception e) {
        System.out.println(e.getMessage());
    } finally {
        try {
            os.close();
            ois.close();
            is.close();
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }

    }
}

你可能感兴趣的:(数据结构和算法,java,数据结构,开发语言)