(八)二叉树—赫夫曼编码应用(文件压缩)

这篇文章展示了一个用Java实现的文件压缩程序,它基于哈夫曼编码原理。程序首先将文件读入字节数组,然后通过哈夫曼编码压缩字节数组,将压缩后的字节数组和哈夫曼编码存储到文件。解压缩过程则是反向操作,从文件中读取哈夫曼编码和压缩数据,解码并写回文件。代码中包含了创建哈夫曼树、编码和解码的步骤。
摘要由CSDN通过智能技术生成

1、基本介绍

2、应用实践(文件压缩)

package tree;

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

public class FileCompression {
    public static Map<Byte, String> huffmanCodes; //  用于存放哈弗曼编码
    public static byte[] huffmanCodesBytes;       //  用于存放哈夫曼编码字节数组(压缩后的字节数组)
    public static void main(String[] args) {
        String srcFile = "f://1.txt";
        String midFile = "f://2.txt";
//        String destFile = "f://3.txt";

        //  压缩文件
       if (zipFile(srcFile, midFile)) {
           System.out.println("压缩成功!");
       } else {
           System.out.println("压缩失败!");
       }

        //  解压文件
//        if (unZipFile(desFile, destFile)) {
//            System.out.println("解压成功!");
//        } else {
//            System.out.println("解压失败!");
//        }
    }

    public static boolean zipFile(String srcFile, String desFile) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        ObjectOutputStream oss = null;  //  以对象的形式写出到文件中
        try {
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(desFile);
            oss = new ObjectOutputStream(fos);
            byte[] bytes = new byte[fis.available()]; // 用于暂存读出来的数据
            fis.read(bytes);                          // 读取数据放到字节数组
            huffmanZip(bytes);
            oss.writeObject(huffmanCodesBytes); // 将压缩后的字节数组写出到文件
            oss.writeObject(huffmanCodes);      // 将哈夫曼编码写出到文件,用于后面对文件的解压
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                oss.close();
                fos.close();
                fis.close();
                return true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    public static boolean unZipFile(String zipFile, String destFile) {
        FileInputStream fis = null;
        ObjectInputStream ois = null;  //  以对象的形式写出到文件中
        FileOutputStream fos = null;
        try {
            fis = new FileInputStream(zipFile);
            ois = new ObjectInputStream(fis);
            fos = new FileOutputStream(destFile);
            byte[] huffmanByte = (byte[])ois.readObject();                  // 将哈弗曼编码字节数组(压缩数组)读取出来
            Map<Byte, String> huffmanCode = (Map<Byte, String>) ois.readObject();// 将哈弗曼编码独取出来
            byte[] b = huffmanUnZip(huffmanByte, huffmanCode); // 返回解压后的字节数组
            fos.write(b); // 将字节数组写出到文件
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                fos.close();
                ois.close();
                fis.close();
                return true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    //  哈夫曼编码压缩
    public static void huffmanZip(byte[] srcBytes) {

        //  2、将字节数组 转为 类型为Node的List集合
        List<ZNode> nodes = getNodes(srcBytes);

        //  3、根据List集合构建哈夫曼树
        HuffmanTrees HuffmanTrees = createHuffmanTrees(nodes);

        //  4、根据哈夫曼树,生成对应的哈夫曼编码
        huffmanCodes = getCodes(HuffmanTrees.root, "", stringBuilder);// 根节点没有路径,设置为空

        //  5、根据哈夫曼编码表,将原字节数组压缩,并返回压缩后的以补码形式表示的哈夫曼编码字节数组
        huffmanCodesBytes = zip(srcBytes, huffmanCodes); //  此时有17个字符
    }


    //  哈夫曼编码解压
    public static byte[] huffmanUnZip(byte[] huffmanByte, Map<Byte, String> huffmanCode) {
        return unzip(huffmanByte, huffmanCode);
    }

//---------------------------------------------------------------------------------------------------

    //  将被压缩字节数组的每一位字符转为0、1字符串
    public static String toBinaryString(byte b, boolean flag) {
        /*
                个人理解:
                    1、遍历数组每一位时,对负数需要截取后8位,同时还需要考虑对字节数组中的正数进行高位补齐
                    2、但当遍历到字节数组最后一位时:
                                2、1.如果是正数,则无需补齐
                                2、2.如果是负数,则需要截取后八位

         */
        int temp = b;       // 将其转为int类型,用于获取对应的二进制字符串
        String str = null;  // 存放二进制字符串
        if (flag) {         // 为true:表示该字符为字节数组的最后一位,如果是正数,表示该字符无需高位补齐。如果是负数,则需要进行截取
            if (temp < 0) {
                str = Integer.toBinaryString(temp);
                return str.substring(str.length() - 8);  // 截取后8位
            } else {
                return Integer.toBinaryString(temp);
            }
        } else {
            temp |= 256;    // 为false,表该字符不是字节数组的最后一位
            str = Integer.toBinaryString(temp);
            return str.substring(str.length() - 8);  // 截取后8位
        }
    }

    private static byte[] unzip(byte[] huffmanByte, Map<Byte, String> huffmanCode) {
        //  1、将哈夫曼编码字节数组huffmanCodesBytes的每一个字符(-88, -65, -56, -65, -56)拼接成二进制字符串(10101000......)
        StringBuilder stringBuilder = new StringBuilder(); // 用于拼接二进制字符串
        for (int i = 0; i < huffmanByte.length; i++) {
            boolean flag = (i == huffmanByte.length - 1); // 字节数组最后一位需特殊处理,要考虑其是否满八位的情况
            stringBuilder.append(toBinaryString(huffmanByte[i], flag)); // 逐个读取字符,从而获取到对应的二进制字符串
        }

        //  2、按照哈夫曼编码,对二进制字符串进行解码
        //  因为要查字符串多个二进制位 对应 的字符,而原哈夫曼编码集合形式是<Byte,String>,所以需要将哈夫曼编码表进行反转
        Map<String, Byte> temp = new HashMap<>();
        for (Map.Entry entry : huffmanCode.entrySet()) {
            temp.put((String) entry.getValue(), (Byte)entry.getKey());
        }

        List<Byte> ch = new ArrayList<>();
        //  3、遍历二进制字符串,从而获取到对应的字符
        for (int i = 0; i < stringBuilder.length(); ) {
            //  通过循环不断地截取对应的二进制个数,判断temp集合中是否有该哈夫曼编码对应的字符
            int count = 1;
            boolean flag = true;
            Byte b = null;
            while (flag) {
                String key = stringBuilder.substring(i, i + count); // 一个一个二进制位匹配
                b = temp.get(key);    // 判断key这个哈夫曼编码在temp集合中是否存在
                if (b == null) {
                    count++;          // 如果不存在,则遍历到下一个二进制位
                } else {
                    flag = false;     // 如果存在,则已经获取到一个哈夫曼编码对应的字符,则退出循环
                }
            }
            ch.add(b);
            i += count; //  i移动到count的位置,寻找下一个哈夫曼编码对应的字符
        }

        //  将List集合中的数据放到字节数组,并返回
        byte[] bytes = new byte[ch.size()];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = ch.get(i);
        }
        return bytes;
    }


    /*
        1、根据哈夫曼编码集合huffmanCodes的哈夫曼编码,将原始的字节数组strBytes每一个字符拼接成一个由0、1组成的字符串
        2、然后将字符串按8位进行分割,再转成十进制,返回byte类型的数组
     */
    private static byte[] zip(byte[] strBytes, Map<Byte, String> huffmanCodes) {
        StringBuilder stringBuilder = new StringBuilder(); // 用于拼接字节数组每一个字符所对应的哈夫曼编码
        int len = 0;    //  统计哈夫曼编码字节数组的长度
        int index = 0;  //  作为哈夫曼编码字节数组的下标,用于存放1个字节对应的数据
        String temp;    //  暂存所截取的二进制字符串

        //  1、将字节数组转换成由0、1组成的哈夫曼编码字符串
        for (byte b : strBytes) {
            //  获取字节数组的每一个字符,然后去哈夫曼编码集合中,获取该字符对应的哈夫曼编码(0、1字符串)
            stringBuilder.append(huffmanCodes.get(b));
        }

        //  2、将哈夫曼编码字符串中的0、1按8位进行分割
        if (stringBuilder.length() % 8 == 0) {  // 判断该哈夫曼编码字符串可以组成多少个十进制数
            len = stringBuilder.length() / 8;
        } else {
            len = stringBuilder.length() / 8 + 1;
        }
        byte[] huffmanCodeBytes = new byte[len];   // 存放哈夫曼编码字符串对应的以补码形式表示的字符
        for (int i = 0; i < stringBuilder.length(); ) {
            if (i + 8 > stringBuilder.length()) {  // 考虑到字符串末尾时,不足八位的情况
                temp = stringBuilder.substring(i); // 则从当前位开始截取,直到字符串末尾
                huffmanCodeBytes[index] = (byte) Integer.parseInt(temp, 2); // 将其转为二进制的形式
                break;
            }
            temp = stringBuilder.substring(i, i + 8);
            huffmanCodeBytes[index] = (byte)Integer.parseInt(temp, 2);      // 将其转为二进制的形式
            i+=8;   //  每8位存放一个字符
            index++;
        }

        return huffmanCodeBytes; //  将对应的哈夫曼编码字节数组返回(全为补码)
    }

    //  下面两个东西用于生成对应的哈夫曼编码时使用:
    public static Map<Byte, String> huffmanCode = new HashMap<>();    // 用于暂存存放哈夫曼编码
    public static StringBuilder stringBuilder = new StringBuilder();  // 用于暂存并拼接路径上的0、1个数
    private static Map<Byte, String> getCodes(ZNode node, String code, StringBuilder stringBuilder) {
        //  用于拼接每个叶子节点上的0、1
        StringBuilder stringBuilder2 = new StringBuilder(stringBuilder);
        //  将路径上的0、1拼接到StringBuilder
        stringBuilder2.append(code);
        //  处理不为null的节点
        if (node != null) {
            //  为null时,表明该节点是非叶子节点,则递归统计根节点到该节点路径上0、1的个数
            if (node.data == null) {
                getCodes(node.left, "0", stringBuilder2);
                getCodes(node.right, "1", stringBuilder2);
            } else {
                // 不为null时,则表明该节点是叶子节点,这时将根节点到当前叶子结点上的哈夫曼编码添加到map集合中
                huffmanCode.put(node.data, stringBuilder2.toString());
            }
        }
        return huffmanCode;
    }

    private static HuffmanTrees createHuffmanTrees(List<ZNode> nodes) {
        while (nodes.size() > 1) {
            //  1、对List<Node>集合进行排序
            Collections.sort(nodes);

            //  2、取出两棵权值最小的二叉树
            ZNode node1 = nodes.get(0);
            ZNode node2 = nodes.get(1);

            //  3、构建一个棵新的二叉树,并重新添加到List集合
            ZNode root = new ZNode(null, node1.weight + node2.weight);
            root.left = node1;
            root.right = node2;
            nodes.add(root);

            //  4、将原来那两棵二叉树从List集合中移除
            nodes.remove(node1);
            nodes.remove(node2);
        }

        //  5、当List集合仅剩一个元素时,该元素即为哈夫曼树的根节点
        return new HuffmanTrees(nodes.get(0));
    }

    private static List<ZNode> getNodes(byte[] strBytes) {
        List<ZNode> nodes = new ArrayList<>();     // 用于存放Node节点的List集合
        Map<Byte, Integer> map = new HashMap<>();  // 用于暂存每个字符及出现次数的映射关系

        for (byte b : strBytes) {
            Integer count = map.get(b);            // 判断当前字符在map中是否有值
            if (Objects.isNull(count)) {                   // 返回结果为null,表示当前字符还未放入map集合
                map.put(b, 1);                     // 将当前字符放入map集合,key为字符本身,value为字符出现的次数
            } else {
                map.put(b, count + 1);             // 不为null,表明map集合中已有该字符,则将其出现次数加1并再次放到map集合中
            }
        }

        //  将map中每一个[key为字符,value为字符出现个数]的元素 转化为Node对象 并放入到List集合中
        for (Map.Entry entry : map.entrySet()) {
            nodes.add(new ZNode((Byte) entry.getKey(), (Integer) entry.getValue()));
        }

        return nodes;
    }

}

class HuffmanTrees {
    public ZNode root;

    public HuffmanTrees(ZNode root) {
        this.root = root;
    }

    //  前序遍历
    public void preOrderTraverse() {
        if (this.root == null) {
            return;
        } else {
            System.out.print("前序遍历:");
            this.root.preOrderTraverse();
        }
    }
}

class ZNode implements Comparable<ZNode> { // 对ZNode对象进行集合排序
    public Byte data;  // 字符对应的ASCII
    public Integer weight; // 权值
    public ZNode left;
    public ZNode right;

    public ZNode(Byte data, Integer weight) {
        this.data = data;
        this.weight = weight;
        this.left = null;
        this.right = null;
    }


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

    @Override
    public int compareTo(ZNode o) {
        //  升序排序
        return this.weight - o.weight;
    }


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

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

来得晚一些也行

观众老爷,请赏~

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

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

打赏作者

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

抵扣说明:

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

余额充值