[ 数据结构 ] 赫夫曼编码--------数据、文件压缩解压

news2024/11/17 15:56:22

0 引出

image-20230108172422920.png

如上图:给定字符串按定长编码处理,最终对应二进制长度为359

思考:如何压缩,将359有效降低? ----回顾:赫夫曼树

1 数据压缩

  1. 拿到数据(字符串)的第一反应,虽然知道应该也像上面一样转为字节数组,但就不知道该怎么办了?
  2. 统计数组中各字节使用的次数,将次数作为权值,字节值作为数据,创建节点,用于构建赫夫曼树,思考为什么创建赫夫曼树?
  3. 使用赫夫曼树的目的? 答案是前缀编码,因为当你知道哪个字节出现的多,你本能就想把较小的编码分给它,举例:数据统计得到100个a,50个b,10个c,那么你会这样编码:a=0,b=1,c=10,那么当你解压时会很头痛,解压遇到10,你知道这是ba还是c吗?
  4. 通过赫夫曼树得到的编码,能避免一个字节的编码是另一个编码的前缀,比如:b的编码是c的前缀
  5. 赫夫曼树构建完毕,如何得到编码表? 编写递归(回溯)方法,收集所有叶子节点的路径表示的二进制数,向左则路径追加0,向右则追加1,直到遇到叶子节点,得到编码表{32=01, 97=100, 100=11000, 117=11001, 101=1110, 118=11011, 105=101, 121=11010, 106=0010, 107=1111, 108=000, 111=0011},下图图解(图为引用,所以与本文有出入,但不影响理解)

image-20230108201819516.png

  1. 有了编码表,对照将第一步中的字节数组转换,得到二进制序列(长度133),按8位一次转为字节数组,压缩完毕
  2. 最终统计:二进制序列压缩比133/359,对应的字节数组压缩比17/40

2 数据解压缩

  1. 拿到压缩后的字节数组,和编码表,如何恢复呢?
  2. 首先,将字节数组中的每个字节转为二进制字符串,并追加为一个长二进制字符串
  3. 处理编码表,将键值位置互换,准备匹配
  4. 对照处理后的编码表,对二进制字符串暴力匹配,将匹配的字节收集到数组中
  5. 利用结果数组创建字符串,完成解压缩

3 文件压缩与恢复

压缩:

  1. 通过字节输入流读取外部文件,并将内容存放在字节数组中
  2. 对字节数组使用第一章的赫夫曼压缩,得到压缩后字节数组
  3. 将压缩后的字节数组序列化到外部路径
  4. 完成文件的压缩,得到压缩文件

恢复:

  1. 将4中的压缩文件反序列化,得到压缩后的字节数组
  2. 对压缩后的字节数组调用第二章的解压方法,恢复到原来的字节数组
  3. 将原字节数组通过字节输出流,写出到外部路径
  4. 完成文件的解压,得到原大小文件

4 代码合集

//赫夫曼压缩
public class App03_HuffmanCode {
    public static void main(String[] args) {
        //得到待压缩字符串,转字节数组->统计各字节的使用次数(权值),构建树节点->创建赫夫曼树->生成赫夫曼编码表->对字节数组进行压缩
        String str = "i like like like java do you like a java";
        byte[] bytes = str.getBytes();
        //压缩
        byte[] huffmanCodeBytes = huffmanZip(bytes);
        System.out.println(result);
        System.out.println(Arrays.toString(huffmanCodeBytes));
        //解压
        System.out.println(new String(decode(result, huffmanCodeBytes)));


        //压缩文件
//        zipFile();
        //解压文件
//        unzipFile();


        //测试
//        System.out.println((byte)Integer.parseInt("10101000",2));
//        System.out.println(byteToBitString((byte) -90, 8));
//        System.out.println(byteToBitString((byte) 28, 6));
//        System.out.println(byteToBitString((byte) 1, 8));
    }

    //解压文件
    public static void unzipFile(String src, String dest) {
        FileInputStream is = null;
        FileOutputStream os = null;
        ObjectInputStream ois = null;
        try {
            is = new FileInputStream(src);
            ois = new ObjectInputStream(is);
            byte[] hmBytes = (byte[]) ois.readObject();
            HashMap<Byte, String> hmCode = (HashMap<Byte, String>) ois.readObject();
            byte[] bytes = decode(hmCode, hmBytes);
            os = new FileOutputStream(dest);
            os.write(bytes);
        } catch (Exception e) {
            e.getMessage();
        } finally {
            try {
                os.close();
                ois.close();
                is.close();
                System.out.println("解压成功~");
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }

    //压缩文件
    public static void zipFile(String src, String dest) {
        FileInputStream is = null;
        FileOutputStream os = null;
        ObjectOutputStream oos = null;
        try {
            is = new FileInputStream(src);
            byte[] bytes = new byte[is.available()];
            is.read(bytes);
            byte[] hmBytes = huffmanZip(bytes);
            os = new FileOutputStream(dest);
            oos = new ObjectOutputStream(os);
            oos.writeObject(hmBytes);
            oos.writeObject(result);
        } catch (Exception e) {
            e.getMessage();
        } finally {
            try {
                oos.close();
                os.close();
                is.close();
                System.out.println("压缩成功~");
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }

    //解压缩:使用赫夫曼编码表对压缩字节数组解压缩
    public static byte[] decode(Map<Byte, String> code, byte[] huffmanBytes) {
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < huffmanBytes.length; i++) {
            int len = (i == (huffmanBytes.length - 1)) ? 5 : 8;//这里为什么是5需要知道原二进制码字符串的总长
            stringBuilder.append(byteToBitString(huffmanBytes[i], len));
        }
//        System.out.println(stringBuilder.toString());

        HashMap<String, Byte> map = new HashMap<>();
        for (Map.Entry<Byte, String> entry : code.entrySet()) {
            map.put(entry.getValue(), entry.getKey());
        }
//        System.out.println(map);
        ArrayList<Byte> list = new ArrayList<>();
        int index = 0;
        for (int i = 1; i <= stringBuilder.length(); i++) {
            if (map.get(stringBuilder.substring(index, i)) != null) {
                list.add(map.get(stringBuilder.substring(index, i)));
                index = i;//左闭右开,i需要取到huffmanBytes.length
            }
        }
        byte[] bytes = new byte[list.size()];
        for (int i = 0; i < list.size(); i++) {
            bytes[i] = list.get(i);
        }
        return bytes;
    }

    //解码工具:将byte转二进制字符串
    //该方法仅对8位的byte转换有效,至于不足8位需考虑去高位,而且得知道保留几位,28=00011100=011100
    public static String byteToBitString(byte b,int len) {
        //转换后问题:负数需要截取,正数需要补
        //做法:先按位与再转再截取低8位
        int temp = b;
        temp |= 256;
        String str = Integer.toBinaryString(temp);
        String s = str.substring(str.length() - 8);
        return s.substring(s.length() - len);
    }

    //赫夫曼压缩(封装):直接压缩字节数组并返回压缩后的
    public static byte[] huffmanZip(byte[] bytes) {
        List<Node> nodes = getNodes(bytes);
        Node root = createHuffmanTree(nodes);
        getCodesFromRoot(root);
        return zip(bytes, result);
    }

    //压缩之四:将字节数组对照编码表转二进制,再转回字节数组
    public static byte[] zip(byte[] bytes, HashMap<Byte, String> hashMap) {
        StringBuilder stringBuilder = new StringBuilder();
        for (byte b : bytes) {
            stringBuilder.append(hashMap.get(b));
        }

        //133位二进制序列
//        System.out.println(stringBuilder.toString());

        int len = (stringBuilder.length() + 7) / 8;
        byte[] bys = new byte[len];
        int index = 0;
        if (stringBuilder.length() % 8 == 0) {
            for (int i = 0; i< stringBuilder.length(); i += 8) {
                //八位二进制取真值(补码求源码),借用Integer的API
                bys[index++] = (byte) Integer.parseInt(stringBuilder.substring(i, i + 8),2);
            }
        } else {
            int i = 0;
            while (i+8 < stringBuilder.length()) {
                bys[index++] = (byte) Integer.parseInt(stringBuilder.substring(i, i + 8),2);
                i += 8;
            }
            //不足8位则高位补0
            bys[len-1]=(byte) Integer.parseInt(stringBuilder.substring(i),2);
        }
        return bys;
    }


    //压缩之三:getCodes编码方法的封装
    public static void getCodesFromRoot(Node root) {
        if (root != null) {
            getCodes(root.getLeft(),"0");
            path.delete(path.length() - 1, path.length());
            //这里不是递归调用,不会对上面的代码造成影响,因为找没找到都结束了,所以可以不回溯
            //可以理解为:最后一道大题都解完了(所有路径收集完毕),辅助线擦不擦无所谓了
            getCodes(root.getRight(),"1");
            path.delete(path.length() - 1, path.length());
        }
    }

    //路径
    private static StringBuilder path = new StringBuilder("");
    //结果集
    private static HashMap<Byte, String> result = new HashMap<>();
    //压缩之三:收集叶子的路径作为编码,即生成编码表
    //节点为空之前,先到叶子节点,递归直接终止了,因此可以不加节点的空校验
    public static void getCodes(Node node,String code) {
        path.append(code);

        //当前为叶子节点
        if (node.getData()!= null) {
            result.put(node.getData(), path.toString());
            return;
        }
        getCodes(node.getLeft(),"0");
        //为什么需要回溯?
        //只要进入调用的方法,路径就会变化,方法有2种操作,1:递归终止,取消路径变化;2:继续递归,暂时保留路径变化
        //因此需要回溯以取消对该节点的操作
        //简单理解就是解几何题过程中,需要画辅助线,如果解题完成,则需要擦除辅助线,否则需要不停画辅助线以解出答案
        path.delete(path.length() - 1, path.length());
        getCodes(node.getRight(),"1");
        //这里不算解题完成吗,为什么辅助线不能擦?  上一行执行完毕,只能说明找到一个叶子节点,即仅拿到一条路径
        path.delete(path.length() - 1, path.length());


    }

    //压缩之二:创建赫夫曼树
    public static Node createHuffmanTree(List<Node> list) {
        while (list.size() > 1) {
            Collections.sort(list);
            Node left = list.get(0);
            Node right = list.get(1);
            Node parent = new Node(null, left.getweight() + right.getweight());
            parent.setLeft(left);
            parent.setRight(right);
            list.remove(left);
            list.remove(right);
            list.add(parent);
        }
        return list.get(0);
    }

    //压缩之一:字节数组转为节点集合
    public static List<Node> getNodes(byte[] bytes) {
        ArrayList<Node> nodes = new ArrayList<>();
        HashMap<Byte, Integer> temp = new HashMap<>();
        for (byte b : bytes) {
            temp.put(b, temp.get(b) == null ? 1 : temp.get(b) + 1);
        }
        Set<Map.Entry<Byte, Integer>> entries = temp.entrySet();
        for (Map.Entry<Byte, Integer> entry : entries) {
            nodes.add(new Node(entry.getKey(), entry.getValue()));
        }
        return nodes;
    }
}

class Node implements Comparable<Node>{
    private Byte data;
    private int weight;
    private Node left;
    private Node right;

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

    public Byte getData() {
        return data;
    }

    public void setData(Byte data) {
        this.data = data;
    }

    public int getweight() {
        return weight;
    }

    public void setweight(int weight) {
        this.weight = weight;
    }

    public Node getLeft() {
        return left;
    }

    public void setLeft(Node left) {
        this.left = left;
    }

    public Node getRight() {
        return right;
    }

    public void setRight(Node right) {
        this.right = right;
    }

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

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

    public void preOrder() {
        System.out.println(this);
        if (this.left != null) {
            left.preOrder();
        }
        if (this.right != null) {
            right.preOrder();
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/148656.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023.1.8 学习周报

文章目录摘要文献阅读1.题目2.摘要3.介绍4.论文主要贡献5.相关工作5.1 序列感知的推荐系统5.2 神经注意模型6.模型&#xff1a;ATTREC6.1 序列推荐6.2 基于Self-Attention的用户短期兴趣建模6.3 用户长期兴趣建模6.4 模型学习7.实验7.1 数据集7.2 评估指标7.3 模型比较7.4 实验…

SSO单点登录实例详解(前端传Code授权登录)

什么是 SSO&#xff08;单点登录&#xff09; SSO 英文全称 Single Sign On&#xff0c;单点登录。SSO 是在多个应用系统中&#xff0c;用户只需要登录一次就可以访问所有相互信任的应用系统。 单点登录流程 单点登录大致流程如下所示&#xff1a; 单点登录详细流程&#x…

【自学C++】C++变量初始化

C变量初始化 C变量初始化教程 变量 的初始化就是在定义变量的同时&#xff0c;给变量设置一个初始值&#xff0c;在 C 中&#xff0c;如果定义变量没有初始化&#xff0c;那么变量有可能会被赋值也有可能不会赋值。 如果是定义的 全局变量 或者 静态变量&#xff0c;未初始化…

2022年语音合成(TTS)和语音识别(ASR)年度总结

论文统计每月更新一次&#xff0c;主要跟踪语音合成和语音识别的发展状况(很多文章都是在会议后才发出&#xff0c;但不影响统计。统计过程难免存在疏漏&#xff0c;因此统计结果仅供参考。所有文章语音合成领域统计列表请访问http://yqli.tech/page/tts_paper.html&#xff0c…

绝大多数人远远低估了软件开发的难度

给你付钱了&#xff0c;你应该把软件做好&#xff01; 这个话相当于&#xff1a; 给你付钱了&#xff0c;你应该把月亮摘下来&#xff01; 趣讲大白话&#xff1a;臣妾做不到 ********** 软件是特殊商品服务 可以说很难有标准 开发的难度取决于需求多少&#xff0c;技术难度&a…

Java Map集合的介绍和使用

什么是Map类型的集合 介绍 1.用于保存具有映射关系的数据&#xff08;key——value&#xff09;。 2.Map中的key和value可以是任意的类型的数据。 3.Map中的key值不允许重复。 4.Map中的value值可以重复。 5.一般常用string作为value的key。 6.key和value之间存在一一对…

如何进行地图SDK开发(二)——示例文档

概述 前面的文章文章我们写到了SDK的开发以及ak认证的实现&#xff0c;在本文我们继续讲讲地图SDK开发中的示例文档的实现。 技术点 vue3viteelement-plusmonaco-editor 实现后效果 实现 1. 工程初始化 1.1 搭建工程 搭建工程的过程请参照博文(使用vite搭建vue3项目&…

javaEE初阶 — 线程池

文章目录线程池1 什么是线程池2 标准库中的线程池2.1 什么是工厂模式2.2 如何使用标准库中的线程池完成任务2.3 ThreadPoolExecutor 构造方法的解释3 实现一个线程池线程池 1 什么是线程池 随着并发程度的提高&#xff0c;随着对性能要求标准的提高会发现&#xff0c;好像线程…

[cpp进阶]C++异常

文章目录C语言传统处理错误的方式C异常概念C异常使用异常的抛出和捕获异常的重新抛出异常安全异常规范自定义异常体系C标准库的异常体系异常的优缺点C语言传统处理错误的方式 传统的错误处理机制&#xff1a; 终止程序。assert断言直接终止程序。缺点&#xff1a;过于粗暴&am…

Fiddler抓取手机APP报文

Http协议代理工具有很多&#xff0c;比如Burp Suite、Charles、Jmeter、Fiddler等&#xff0c;它们都可以用来抓取APP报文&#xff0c;其中charles和Burp Suite是收费的&#xff0c;Jmeter主要用来做接口测试&#xff0c;而Fiddler提供了免费版&#xff0c;本文记录一下在Windo…

位运算做加法,桶排序找消失元素,名次与真假表示,杨氏矩阵,字符串左旋(外加两道智力题)

Tips 1. 2. 3. 大小端字节序存储这种顺序只有在放进去暂时存储的时候是这样的&#xff0c;但是一旦我里面的数据需要参与什么运算之类的&#xff0c;会“拿出来”先恢复到原先的位置再参与运算&#xff0c;因此&#xff0c;大小端字节序存储的什么顺序不影响移位运算等等…

【案例教程】CLUE模型构建方法、模型验证及土地利用变化情景预测实践技术

【前沿】&#xff1a;土地利用/土地覆盖数据是生态、环境和气象等领域众多模型的重要输入参数之一。基于遥感影像解译&#xff0c;可获取历史或当前任何一个区域的土地利用/土地覆盖数据&#xff0c;用于评估区域的生态环境变化、评价重大生态工程建设成效等。借助CLUE模型&…

声音产生感知简记

声音产生 人的发音器官包括:肺、气管、声带、喉、咽、鼻腔、口腔、唇。肺部产生的气流冲击声带,产生震动。 声带每开启和闭合一次的时间是基音周期(Pitch period,T),其到数为基音频率(F.=1/T,基频),范围在70-450Hz。基频越高,声音越尖细,如小孩的声音比大人尖,就是…

编译错误2

本文迁移自本人网易博客&#xff0c;写于2015年11月25日&#xff0c;编译错误2 - lysygyy的日志 - 网易博客 (163.com)1、error C2059:语法错误&#xff1a;“<L_TYPE_RAW>”error C2238:意外的标记位于“;”之前.错误代码定位于&#xff1a;BOOL TreeView_GetCheckState…

excel函数公式:常用高频公式应用总结 上篇

公式1&#xff1a;条件计数条件计数在Excel的应用中十分常见&#xff0c;例如统计人员名单中的女性人数&#xff0c;就是条件计数的典型代表。条件计数需要用到COUNTIF函数&#xff0c;函数结构为COUNTIF(统计区域,条件)&#xff0c;在本例第一个公式COUNTIF(B:B,G2)中&#xf…

《栈~~队列~~优先级队列》

目录 前言&#xff1a; 1.stack 1.stack的介绍 2.stack的使用&#xff1a; 3.stack的模拟实现 4.有关stack的oj笔试题 2.queue 1.队列的介绍 2.队列的使用 3.队列的模拟实现 4.有关队列的oj笔试题 3.priority_queue 1.优先级队列的介绍 2.优先级队列的使用 3.优先级队列的模拟实…

挥别2022,坦迎2023。

第一章&#xff1a;CSDN&#xff0c;我来啦&#xff01;第一节&#xff1a;初遇&#xff01;2022-08-13&#xff0c;我和CSDN相遇啦&#xff01;CSDN&#xff0c;你好呀&#xff01;2022年8月13日&#xff0c;是我与你相遇的日子。这是一个值得纪念的时刻。从此之后&#xff0c…

English Learning - L1-10 时态(下) 2023.1.5 周四

English Learning - L1-10 时态&#xff08;下&#xff09; 2023.1.5 周四8 时态8.3 完成时态核心思想&#xff1a;回首往事&#xff08;一&#xff09;现在完成时核心思想用法延续动作延续时间 “动作一直持续了。。。”延续动&#xff08;无延续时间&#xff09; “做过。。…

AtCoder Beginner Contest 284 A - E

题目地址&#xff1a;AtCoder Beginner Contest 284 - AtCoder 一个不知名大学生&#xff0c;江湖人称菜狗 original author: jacky Li Email : 3435673055qq.com Time of completion&#xff1a;2023.1.8 Last edited: 2023.1.8 目录 题目地址&#xff1a;AtCoder Beginner C…

基于FPGA的UDP 通信(一)

引言手头的FPGA开发板上有一个千兆网口&#xff0c;最近准备做一下以太网通信的内容。本文先介绍基本的理论知识。FPGA芯片型号&#xff1a;xc7a35tfgg484-2网口芯片&#xff08;PHY&#xff09;&#xff1a;RTL8211网络接口&#xff1a;RJ45简述以太网什么以太网&#xff1f;以…