赫夫曼编码之文件压缩与解压详解

赫夫曼编码之文件压缩与解压详解

说明

  1. 使用赫夫曼编码对文件进行压缩与解压,与对文本文件的操作是一样的,只是加入了IO流的相关操作
  2. 赫夫曼编码对于重复率较高的二进制文件压缩效率较高,但是如果二进制文件重复率不高,则基本没有压缩效率
  3. 注意使用赫夫曼压缩后的文件,必须使用赫夫曼再进行解压,其他解压工具不提供技术支持
  4. 源码见下

源码及分析

//将文件进行压缩
    /**
     *
     * @param srcFile 要压缩的文件路径
     * @param dstFile 压缩后的文件路径
     */
    public static void fileZip(String srcFile, String dstFile){
        FileInputStream fis = null;
        OutputStream os = null;
        ObjectOutputStream oos = null;
        try {
            //文件输入流
            fis = new FileInputStream(srcFile);
            //创建和文件大小相同的字节数组
            byte[] b = new byte[fis.available()];
            //读取文件
            fis.read(b);
            //将读到的文件进行压缩
            byte[] huffmanZipBytes = huffmanZip(b);
            //文件输出流
            os = new FileOutputStream(dstFile);
            oos = new ObjectOutputStream(os);
            //将压缩后的字节数组写入磁盘
            oos.writeObject(huffmanZipBytes);
            //将编码表写入磁盘
            oos.writeObject(huffmanCodes);

        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                //释放资源
                fis.close();
                os.close();
                oos.close();
            } catch (IOException 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[] huffmanByte = (byte[]) ois.readObject();
            //读取编码表
            Map<Byte,String> huffmanCodes = (Map<Byte, String>)ois.readObject();
            //解码
            byte[] src = decode(huffmanCodes, huffmanByte);
            //文件输出流
            os = new FileOutputStream(dstFile);
            //将解压后的文件存储到磁盘
            os.write(src);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                //释放资源
                os.close();
                ois.close();
                is.close();
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }
    }

赫夫曼编码应用完整代码

package algorithm.tree.huffmancode;

import java.io.*;
import java.util.*;

/**
 * @author AIMX_INFO
 * @version 1.0
 */
@SuppressWarnings("ALL")
public class HuffmanCode {
    public static void main(String[] args) {
        //测试
        //String content = "i like like like java do you like a java";
        String content = "what is the most important is learning study";
        System.out.println("编码前: " + content);
        //将字符串转字节数组
        byte[] bytes = content.getBytes();
        //压缩
        byte[] huffmanCodeByte = huffmanZip(bytes);
        //查看效果
        System.out.println("编码后: "+Arrays.toString(huffmanCodeByte));

        byte[] source = decode(huffmanCodes, huffmanCodeByte);
        System.out.println("解码后: "+new String(source));

        //文件压缩测试
        String srcFile = "D:\\src.png";
        String dstFile = "D:\\dst.zip";
        fileZip(srcFile,dstFile);
        System.out.println("压缩成功");

        //解压文件测试
        String zipFile = "D:\\dst.zip";
        String dstFile2 = "D:\\src2.png";
        unZipFile(zipFile,dstFile2);
        System.out.println("解压成功");
    }

    //将压缩的文件进行解压

    /**
     *
     * @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[] huffmanByte = (byte[]) ois.readObject();
            //读取编码表
            Map<Byte,String> huffmanCodes = (Map<Byte, String>)ois.readObject();
            //解码
            byte[] src = decode(huffmanCodes, huffmanByte);
            //文件输出流
            os = new FileOutputStream(dstFile);
            //将解压后的文件存储到磁盘
            os.write(src);
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                //释放资源
                os.close();
                ois.close();
                is.close();
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }
    }

    //将文件进行压缩
    /**
     *
     * @param srcFile 要压缩的文件路径
     * @param dstFile 压缩后的文件路径
     */
    public static void fileZip(String srcFile, String dstFile){
        FileInputStream fis = null;
        OutputStream os = null;
        ObjectOutputStream oos = null;
        try {
            //文件输入流
            fis = new FileInputStream(srcFile);
            //创建和文件大小相同的字节数组
            byte[] b = new byte[fis.available()];
            //读取文件
            fis.read(b);
            //将读到的文件进行压缩
            byte[] huffmanZipBytes = huffmanZip(b);
            //文件输出流
            os = new FileOutputStream(dstFile);
            oos = new ObjectOutputStream(os);
            //将压缩后的字节数组写入磁盘
            oos.writeObject(huffmanZipBytes);
            //将编码表写入磁盘
            oos.writeObject(huffmanCodes);

        } catch (Exception e) {
            System.out.println(e.getMessage());
        } finally {
            try {
                //释放资源
                fis.close();
                os.close();
                oos.close();
            } catch (IOException e) {
                System.out.println(e.getMessage());
            }
        }
    }

    //将压缩的所有方法封装
    public static byte[] huffmanZip(byte[] bytes){
        //获取节点
        List<Node> node = getNode(bytes);
        //生成赫夫曼树
        Node huffmanTreeRoot = createHuffmanTree(node);
        //生成赫夫曼编码
        Map<Byte, String> huffmanCodes = getCodes(huffmanTreeRoot);
        //压缩
        byte[] huffmanCodeByte = huffmanCodeByte(bytes, huffmanCodes);
        return huffmanCodeByte;
    }

    //根据字节数组和赫夫曼编码表将字符串压缩
    /**
     *
     * @param bytes 要根据赫夫曼编码表编码的字符数组
     * @param huffmanCodes 赫夫曼编码表
     * @return 返回压缩后的结果
     */
    public static byte[] huffmanCodeByte(byte[] bytes, Map<Byte, String> huffmanCodes) {
        //创建可变长字符串,存储编码后的字符串
        StringBuilder stringBuilder = new StringBuilder();
        //遍历字符数组,实现字符数组中元素的编码
        for (byte b : bytes) {
            stringBuilder.append(huffmanCodes.get(b));
        }
        //编码完成后将二进制转换为十进制完成压缩
        //计算以8位为一组压缩需要的字符数组空间
        int len;
        if (stringBuilder.length() % 8 == 0) {
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }
        //创建huffmanCodeByte字节数组存放转换后的结果
        byte[] huffmanCodeByte = new byte[len];
        //辅助变量
        int index = 0;
        //遍历二进制编码,8位一组转换为十进制进行存储
        for (int i = 0; i < stringBuilder.length(); i += 8) {
            String bys;
            //注意最后一个字符可能不满8位
            if (i + 8 > stringBuilder.length()) {
                bys = stringBuilder.substring(i);
            } else {
                bys = stringBuilder.substring(i, i + 8);
            }
            //添加
            huffmanCodeByte[index] = (byte) Integer.parseInt(bys, 2);
            index++;
        }
        return huffmanCodeByte;
    }

    //前序遍历的方法
    public static void preOrder(Node root) {
        if (root != null) {
            root.preOrder();
        } else {
            System.out.println("赫夫曼树为空...");
        }
    }

    //将生成的赫夫曼树进行编码,将结果存储到集合中
    //可变字符串用于码值拼接
    static StringBuilder stringBuilder = new StringBuilder();
    //创建HashMap用于存储赫夫曼编码
    static HashMap<Byte, String> huffmanCodes = new HashMap<>();

    /**
     * 将赫夫曼树的所有叶子节点生成编码存储在集合huffmanCodes中
     *
     * @param node          开始编码的节点
     * @param code          码值 向左为0, 向右为1
     * @param stringBuilder 码值的拼接
     */
    public static void getCodes(Node node, String code, StringBuilder stringBuilder) {
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //拼接
        stringBuilder2.append(code);
        //判断node是否为空
        if (node != null) {
            //再判断当前节点是叶子节点还是非叶子节点
            if (node.data == null) {
                //如果是非叶子节点则递归处理
                //向左递归
                getCodes(node.left, "0", stringBuilder2);
                //向右递归
                getCodes(node.right, "1", stringBuilder2);
            } else {
                //如果是叶子节点,则将编码添加到集合中
                huffmanCodes.put(node.data, stringBuilder2.toString());
            }
        }
    }

    //编写生成赫夫曼编码的重载方法
    public static Map<Byte, String> getCodes(Node root) {
        if (root != null) {
            //向左递归
            getCodes(root.left, "0", stringBuilder);
            //向右递归
            getCodes(root.right, "1", stringBuilder);
        } else {
            System.out.println("赫夫曼树为空");
        }
        return huffmanCodes;
    }

    //创建赫夫曼树
    public static Node createHuffmanTree(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.right = rightNode;
            parent.left = leftNode;
            list.add(parent);
            list.remove(leftNode);
            list.remove(rightNode);
        }
        return list.get(0);
    }

    //编写方法将字节数组中的字符及其权值封装为一个node存储到集合中
    public static List<Node> getNode(byte[] bytes) {
        //创建集合保存封装好的对象
        List<Node> nodes = new ArrayList<>();
        //创建HashMap保存字符及其出现的次数
        HashMap<Byte, Integer> map = new HashMap<>();
        for (byte b : bytes) {
            Integer integer = map.get(b);
            if (integer == null) {
                map.put(b, 1);
            } else {
                map.put(b, integer + 1);
            }
        }
        //遍历结束后字节数组中的字符及其对应次数已经存储在HashMap的键和值中
        for (Map.Entry<Byte, Integer> entry : map.entrySet()) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }
    /**
     * @param b    要转换的byte数
     * @param flag 该数是否需要高位补0
     * @return 返回转换后的字符串
     */
    public static String byteToBitString(byte b, boolean flag) {
        //定义变量int保存byte b,因为Integer类的转二进制方法
        int tmp = b;
        //如果需要高位补零  说明: 正数需要高位补零,但是最后一个正数不需要
        if (flag) {
            //按位或
            tmp = tmp | 256;
        }
        //将整数转为二进制字符串,返回对应的补码
        String str = Integer.toBinaryString(tmp);
        //转换为的二进制位长为计算机位长,32位或者64位,因此需要截取后八位
        //如果不是最后一位,截取后八位
        if (flag) {
            return str.substring(str.length() - 8);
        } else {
            return str;
        }
    }
    //将编码后的字符数组进行解码

    /**
     * @param huffmanCodes 赫夫曼编码表
     * @param huffmanBytes 赫夫曼编码
     * @return 返回解码后的字符串
     */
    public static byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
        //定义StringBuilder拼接
        StringBuilder stringBuilder = new StringBuilder();
        //遍历赫夫曼编码中的 byte,将其转换为二进制字符串
        for (int i = 0; i < huffmanBytes.length; i++) {
            byte b = huffmanBytes[i];
            //如果时是最后一个byte,则不需要高位补零
            boolean flag = (i == huffmanBytes.length - 1);
            String str = byteToBitString(b, !flag);
            stringBuilder.append(str);
        }
        //根据赫夫曼编码表的映射关系,将编码转换为对应字符
        //将赫夫曼编码表中的键和值反转
        Map<String, Byte> map = new HashMap<>();
        for (Map.Entry<Byte, String> entry : huffmanCodes.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }
        //创建集合,存放Byte
        ArrayList<Byte> list = new ArrayList<>();
        //遍历二进制字符串,解码
        for (int i = 0; i < stringBuilder.length();) {
            //变量count保存有效的二进制位数
            int count = 1;
            //flag保存当前字符串是否匹配
            boolean flag = true;
            //b为当前匹配到的值
            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;
        }
        //循环结束后已经将解码后的字符存储到集合中
        byte[] bytes = new byte[list.size()];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = list.get(i);
        }
        return bytes;
    }
}

//节点类
class Node implements Comparable<Node> {
    //数据,字符对应的ASICII码
    Byte data;
    //权值
    int weight;
    Node left;
    Node right;

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

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

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

    @Override
    public int compareTo(Node o) {
        return this.weight - o.weight;
    }
}
posted @ 2021-06-09 16:18  mx_info  阅读(252)  评论(0)    收藏  举报