【算法】超详细哈夫曼编码JAVA解释

news2024/11/15 16:02:19

综合实验报告格式

综合实验题目

一、人员和分工

LenckCuak

二、问题描述和基本要求

1、 用哈夫曼编码设计一个压缩软件; 
2、 能对输入的任何类型的文件进行哈夫曼编码,产生编码后的文件——压缩文件; 
3、 能对输入的压缩文件进行译码,生成压缩前的文件——解压文件; 
4、 要求编码、译码效率尽可能地高;(额外加分) 
5、 撰写实验报告。 

三、工具/准备工作

2.1 综合实验中涉及的数据结构知识。

2.1.1 自定义节点

通过对基本要求的分析,确立哈夫曼树节点所需要的基本元素,即字节数据、权重、左右孩子节点。

 

2.1.2 哈夫曼编码采用“树型结构”

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若 根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。树的路径长度是从树根 到每 一结点的路径长度之和,记为WPL=(W1*L1+W2*L2+W3*L3+...+Wn*Ln),N个 权值Wi(i =1,2,...n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,...n)。可以证明哈夫曼树的WPL是最小的。

 

2.2 使用的集成开发环境。

程序使用前三大 Java IDE 之一的eclipse编写,版本为JavaSE-1.8。

四、分析与实现

下图为本文算法的架构图。

 

4.1分析综合实验项目的实现方法(围绕基本要求)

4.1.1 输入输出文件(压缩过程)

依据文档要求,用户需要输入源文件和目标文件名,那么如何将提取源文件内容是重点,常见的提取方式是提取源文件的字符,即采用char数组存储,然而这种方式只能实现对文本文件的压缩和解压,不能对非文本文件进行压缩和解压,并且要实现任何文件的哈夫曼编码,只能对源文件中的字节进行哈夫曼编码,而采用int数组在实现转换字节的理解和编程上困难,故本文采用byte数组进行模拟,并且通过输入流提取源文件的字节数组。在实现上,采用FileInputStream和FileOutputStream作为输入输出流;在提取源文件字节数组上,采用FileInputStream::available()函数来构建一个与源文件字节数组大小的字节数组,并且使用FileInputStream::read(byte[]b)来将源文件的字节数组赋值给构建的字节数组b;在构建输出文件的实现上,采用ObjectOutputStream(FileOutputStream),构造对象输出流,采用对象输出流的原因是可以将不同对象进行写入,而在提取时可以按照对象类别进行提取,而不会混乱,便于解压,然后将哈夫曼编码后的字节数组b(4.1.5)通过ObjectOutputStream::writeObject(Byte[]b)构建压缩后的文件,通过向对象输出流写入哈夫曼编码后的字节数组和哈夫曼编码字典map便于解压。

4.1.2 将源文件字节数组转化为具有权重和字节数据的节点数组

通过遍历4.1.1提取到的源文件字节数组来计算不同字节的出现频率。使用ArrayList<Node> 来存储节点,用Map<Byte,Integer>来存储不同字节对应的权重(出现频率)。其中节点由字节数据、权重组成,每个节点可以访问其左右孩子节点。

4.1.3 采用优先队列将节点数组转换为哈夫曼树,减小时间复杂度

通过将4.1.2得到的节点数组转为优先队列,由于优先队列的特殊性,在每次插入和删除节点时都会调整堆结构,从而维持整个优先队列是一个最小堆的结构。由于节点是自定义类型,所以要重载节点的比较操作。通过判断优先队列的大小是否大于1来进行,若大于1则重复下面操作:提取堆顶节点,删除堆顶节点,再提取堆顶节点,将这两个节点作为新父节点的两个孩子节点,新的父节点的数据设置为空,将父节点加入到优先队列中;若小于等于1时,则说明已构成一棵树,那么哈夫曼树构建完成。且优先队列的唯一节点即为哈夫曼树的根节点。

采用优先队列使得建树的时间复杂度减小为O(nlog(n)),相比于原参考资料和大多资料的建树时间复杂度(n^2log(n))更加迅速。

4.1.4 生成哈夫曼编码

通过将4.1.3构造好的哈夫曼树生成哈夫曼编码map,其中key=节点的字节值,value=生成的字符串编码。在实现上,采用StringBuilder构造器来拼接哈夫曼编码字符串,而不采取String,拼接速度比String更加迅速。通过递归进行向哈夫曼树的左右孩子节点进行遍历,并且向左延伸时哈夫曼编码加“0”,向右时哈夫曼编码加“1” 。判断是否为叶子节点的依据是根据上述4.1.3构建父节点时将父节点的数据置为空。当递归到叶子节点时,将节点的字节值作为哈夫曼编码map的key,将对应的哈夫曼编码字符串作为map的value。同时此处也将生成字典文件。

4.1.5 将源文件的字节数组替换为经哈夫曼编码后的字节数组

使用4.1.4得到的哈夫曼编码对4.1.1提取到的源文件字节数组进行一一替换,从而实现哈夫曼压缩。在实现上,使用StringBuilder构造器实现哈夫曼编码字符串的拼接。同时新建一个字节数组用以输出压缩后的文件,新建字节数组的大小依据源文件字节数组的哈夫曼编码字符串的长度构造,由于哈夫曼编码是0101的二进制数据,而1个字节含有8个二进制数,所以构造的新字节数组的长度为(哈夫曼编码字符串长度+7)/8,这样的做法不会丢弃任何二进制数,并且最大限度避免了空间浪费。例如:1个二进制数(1),若length=builder.length()/8=1/8=0 则转换为0字节而length = (builder.length() + 7) / 8=8/8=1则转换为1个字节而当7个二进制时(1111000),length = (builder.length() + 7) / 8=14/8=1,转换为1个字节,不会转换为2个字节,这样就不会浪费空间。在将哈夫曼编码字符串转换为字节数组的操作上,由于本文是采用模拟的方式,所以全程都依据模拟的方式进行,而不能直接用字符串转为字节数组的内置函数转换。通过遍历哈夫曼编码字符串,依次截取8个二进制数通过(byte)Integer.parseInt(string,2)函数将其转为一个字节,并存储在新建的字节数组中;若哈夫曼编码最后的二进制字符串不足8个时,也是如此。这样就构建好了哈夫曼编码后的字节数组。

4.1.6 输入输出文件(解压过程)

与4.1.1一样,采用输入输出流分别对压缩文件和目标文件进行输入输出。在提取哈夫曼编码后的字节数组上,采用对象流内置函数和强转(byte[])ObjectInputStream::readObject()提取字节数组。由于4.1.1对象流的特殊性,可以将哈夫曼字典数据加入到压缩文件中,从而可以使用对象流提取出字典数据,可以避免再一次扫描源文件或额外的字典文件。具体方法是(Map<Byte,String>)ObjectInputStream::readObject()。最后通过对象流将4.1.7得到的源文件字节数组写入到文件中,得到解压后的文件。

4.1.7 将哈夫曼编码后的字节数组转为哈夫曼编码前的字节数组(源文件字节数组)

通过对4.1.6得到的哈夫曼字典map和哈夫曼编码后的字节数组可以得到源文件字节数组。在实现上,先将哈夫曼编码后的字节数组转为二进制字符串,其中通过遍历哈夫曼编码后的字节数组并使用StringBuilder构造器对每个字节生成的二进制字符串进行拼接;在将字节转为二进制字符串的实现上,首先判断该字节是否为正数,若为正数则将该字节补高位,即该字节与256进行或运算,然后使用Integer.toBinaryString(byte)进行转换为二进制字符串,最后截取将该字符串截取最后8个字符作为转换结果。这样就可以得到未压缩的文件经哈夫曼编码后的二进制字符串,最后通过新的map对哈夫曼字典进行键值反转,得到映射关系为二进制字符串到字节的逆哈夫曼编码字典。然后采用前缀编码方式对哈夫曼编码后的二进制字符串进行替换,得到字节存储在列表之中,最后将列表的字节存放在字节数组中。其中在将哈夫曼编码后的二进制字符串转为字节的前缀编码实现上,通过对二进制字符串进行遍历,使用两个变量记录二进制字符串的位置,当最前的几个二进制能够通过逆哈夫曼字典查询到数据时,即将该位置记录下来,从而实现前缀编码。

4.2采用适当的数据结构与算法(Why、What、How)

4.2.1 输入输出文件

用户输入源文件和目标文件名,分别赋值于zipFile函数的srcFile和dstFile字符串中。通过创建输入流(下图红色矩形框)和输出流(下图绿色矩形框)来接收源文件和写入文件,其中写入文件使用对象流(下图黄色矩形框)写入,采用对象流写入的原因是对象流可以将不同对象进行写入,并且在解压时可以按照对象类别进行提取,提取的内容只会包含同一种类别,本文通过向目标文件路径写入哈夫曼二进制编码转换后的字节数组和huffmanCodes(哈夫曼编码表)其中key=二进制数据,value=哈夫曼编码,写入这个的原因是后面解压时可以直接提取字典,便于解压。

通过FileInputStream::read(byte[]b)可以将源文件的字节数据赋值给字节数组b中。

具体实现方法如下图:其中的函数功能和原理在下面截图均有详细注释

 

4.2.2 将源文件字节数组转化为具有权重和字节数据的节点数组

具体算法:

 

节点的数据结构:

 

4.2.3 采用优先队列将节点数组转换为哈夫曼树,减小时间复杂度

下图为建树过程:

 

下图为使节点可比较的过程:

 

4.2.4 生成哈夫曼编码

 

4.2.5 将源文件的字节数组替换为经哈夫曼编码后的字节数组

 

4.2.6 输入输出文件(解压过程)

 

4.2.7 将哈夫曼编码后的字节数组转为哈夫曼编码前的字节数组(源文件字节数组)

 

五、测试与结论

1.对图像文件的压缩和解压

 

如图所示,将原文件(1.png)成功压缩为1.zip,后成功解压文件(1.zip)为2.png

 

上图所示,即为原文件(1.png)和压缩再解压后的文件(2.png)对比示意图

2.对纯文本文件的压缩与解压

 

如图所示,将原文件(t1.txt)成功压缩为t1.zip,后成功解压文件(t1.zip)为t2.txt

 

上图所示,即为原文件(t1.txt)和压缩再解压后的文件(t2.txt)对比示意图

3.对word文档的压缩和解压

 

如上图所示,成功将word文件(w1.docx)成功压缩为word1.zip,后成功解压文件为w2.docx

 

如上图所示,即为原文件和压缩解压后的文件对比示意图

4.对音频文件的压缩和解压

 

如上图所示,成功将原文件(music3.mp3)进行压缩为music3.zip后,成功地将music3.zip解压为music4.mp3

 

如图所示,这是原文件music3.mp3和压缩和解压后的文件music4.mp3的音频对比图

5.对视频文件的压缩和解压

 

如上图所示,成功将原文件(s1.mp4)进行压缩为s1.zip后,成功地将s1.zip解压为s2.mp4

 

如图所示,这是原文件s1.mp4和压缩和解压后的文件s2.mp4的视频对比图

六、综合实验总结

主要说明算法的特点,你进行了哪些功能扩展,特别是重点说明独创或创新的部分,相关课程设计项目最有价值的内容,在哪些方面需要进一步了解或得到帮助,以及编程实现课程设计的的感悟等内容。

注:如没有某些内容(例如没有功能功展),则不填写相应内容。

(一)、功能拓展与创新

1. 在文件显示上通过设计字典文件和压缩率增强可视化。

本算法针对参考资料,自行设计了一个字典文件,可以将需要压缩的文件的字节所对应的权重和哈夫曼编码依次写在这个字典文件中,如下图:

 

例如在需要压缩的11.txt文件中,输入一下简单的内容

 

对应的字典文件dictAndFrequent.txt内容如下:其中data为ascii码,weight为权重即频率,箭头所指向的为哈夫曼编码。data没有显示使用字符表示的原因是针对图片和视频以及其他非文本内容而言,可以更加直观的看到data的字节值,而不是乱码。

 

其中对应的解压文件2.txt内容如下:

 

与11.txt内容完全一致,并且该算法在此文件的压缩率如下图:

 

意味着节省了绝大部分内存。(压缩率越低,节省空间越多)

2. 减小了其中选择最小权重节点的时间复杂度。

参考资料中的代码采用Collection.sort()进行排序,而此方法的时间复杂度为O(nlog(n)),在建哈夫曼树(createHuffmanTree)这个函数的复杂度为O(n^2log(n)),而本文采用了PriorityQueue进行优化,其中PriorityQueue::poll()和PriorityQueue::add()的复杂度均为O(log(n)),且每次插入节点都可以调整堆结构,使堆一直维持最小堆的结构,从而每次只需提取根节点即可寻找到最小节点,从而将createHuffmanTree的时间复杂度减小为O(nlog(n))。

 

3. 减小了获取哈夫曼编码的时间复杂度

在将源文件字节数组转换为哈夫曼编码二进制字符串时,本文采用StringBuilder进行拼接,拼接速度比使用String类更加迅速。时间复杂度上,String类的时间复杂度为O(xn^2),x为需要拼接的字符串长度,n为拼接操作;而StringBuilder::append(String w)的时间复杂度是O(n),更加迅速。

 

4. 更方便地解压文件

在压缩时,本文使用对象流将不同的对象(字节数组和map<Byte,String>)输入到压缩文件中,从而在解压文件时,无需额外的字典文件进行解压,只需在压缩文件中直接提取((Map<Byte,String>)ObjectInputStream::readObject())即可。

 

(二)、课程设计项目最有价值的内容

本文以java为系统开发的基础语言,以哈夫曼编码构成哈夫曼树,以及哈夫曼树逆向解码过程为本次文件压缩解压程序设计的思想,设计并完成了基于哈夫曼编码对各类文件(纯文本文件,word文档,mp3音频文件,MP4视频文件,png图像文件)压缩解压的功能。并选用代表文件对其核心的文件压缩解压功能进行了测试,各类文件通过本次所设计系统压缩功能的压缩后,所得到的目标文件与原文件大小基本一致,且感官效果一致。通过对原文件和压缩解压文件进行比较,确定本次所设计的文件压缩解压功能,具有无损压缩和解压的特性,在压缩和解压的过程中,并不会存在数据的损坏与丢失。因此通过哈夫曼编码与哈夫曼原理所设计出的文件压缩解压系统,也是对需要压缩的文件的数据进行采集,并将采集后的数据通过哈夫曼编码进行一一转化,使编码符号与数字字符产生一一对应的关系而不会因丢失文件的数据,导致文件的变形或受损。

同时对于程序算法效率的提升也有显著价值,在将节点list转换为哈夫曼树的函数中,采用了PriorityQueue进行优化,将生成哈夫曼树的时间复杂度由O(n^2log(n))减小到O(nlog(n))

(三)、哪方面进一步了解或得到帮助

对于无损压缩方式的改进

直接将原文件数据先进行改进压缩改进,可以将数据替换成数据+重复次数,例如,文件的内容是AAAAABBBBCCCCCCCC,就可以替换成5A4B8C,将17个字符压缩为6个字符,大大减少了存储空间,但是这种情况比较使用于重复性大且连续重复的情况。

多线程优化

如果处理的文件较大,则单线程的设计模式效率就会很低。压缩一个文件看可能需要几分钟甚至更久,由于长时间的压缩时间不能满足实际开发需要,所以可以引入多线程的设计模式。例如使用javaSE11的多线程开发加快压缩和解压速率。

 

例如伪代码使用thread类来进行并发操作,同时使用两个线程让数据转换成节点和创建哈夫曼树同时进行,这样可以大大加快压缩速率。

(四)、编程实现课程设计的感悟

通过实现原理为哈夫曼编码的压缩解压程序,对国内外现有的压缩解压软件原理有了了解(大多数基于LZW码的改进算法的基础上开发的),同时对于哈夫曼编码有更加深刻的认识,哈夫曼编码作为一种变长编码技术,由于它能充分利用短码,所以同样可以根据它来开发压缩和解压软件,并且通过哈夫曼编码实现的压缩与解压软件更加简单易用,安全保密性能更好。

七、源码

package huffman;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;

public class Node implements Comparable<Node>{
    public Byte data;
    //使用字节表示的原因:字节可以表示所有文件,计算机所能表示的最小单位。1字节可以存储8bit,1字节表示最大的二进制数为11111111=十进制数255,
    //一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,都一个一个的字节,那么传输时一样如此。所以,字节流可以传输任意文件数据
    public int weight;
    public Node left;
    public Node right;
    public static String dictFile;
    public static String frequentFile;
    static FileOutputStream dictOS = null;
    static byte[] b;//读取文件的原字节数组
    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;
    }
    
    
    //原理:将文件中的所有字节进行计数,1个字节中表示的不同二进制数值通过映射关系放入map中,即1字节中相同的二进制数值越多,编码越短
    //函数功能:将字节数组转换成一个节点,存放在List集合中,每个节点由该数据和出现频率(即权值)组成。
    private static ArrayList<Node> getList(byte[]bytes){
        ArrayList<Node> nodes = new ArrayList<>();
        Map<Byte,Integer> map = new HashMap<>();//map映射每个字节(键/key)出现的次数(值/value)
        for(byte b : bytes){
            Integer count = map.get(b);
            if(count == null){
                map.put(b,1);
            }else
                map.put(b,map.get(b) + 1);
        }
        //遍历map
        for(Map.Entry<Byte,Integer> entry : map.entrySet()){//entry.getKey()得到map的键,entry.getValue()得到map的值
            nodes.add(new Node(entry.getKey(),entry.getValue()));
        }
        return nodes;
    }
    
    
    
    //原理:通过对节点数据频率进行排序,出现越少的两个节点先组成一颗树,并移除原先的两个节点
    //函数功能:将节点list转化为哈夫曼树,返回根节点
    private static Node createHuffmanTree(List<Node> nodes){
    	PriorityQueue<Node>nodeQueue=new PriorityQueue<>(nodes);
        while(nodeQueue.size() > 1){//直到只有一个根节点
//            Collections.sort(nodes);
//            Node leftNode = nodes.get(0);
//            Node rightNode = nodes.get(1);
        	Node leftNode=nodeQueue.poll();
        	Node rightNode = nodeQueue.poll();
            Node parent = new Node(null,leftNode.weight + rightNode.weight);//生成的父节点无数据,用以后面判断是否为叶子节点
            parent.left = leftNode;
            parent.right = rightNode;
            nodeQueue.add(parent);
//            nodes.remove(leftNode);
//            nodes.remove(rightNode);
//            nodes.add(parent);
        }
//        return nodes.get(0);
        return nodeQueue.peek();
    }
    static Map<Byte,String> huffmanCodes = new HashMap<>();
    static StringBuilder stringBuilder = new StringBuilder();//StringBuilder可以构造一个快速字符串生成对象
 
    
    
    //函数功能:通过构造好的哈夫曼树生成哈夫曼编码map,key=字节中的二进制数  value=生成的字符串编码
    private static Map<Byte,String> getCodes(Node root){
        if (root == null)
            return null;
        getCodes(root,"",stringBuilder);
        return huffmanCodes;
    }
 
    //根据哈夫曼树获得哈夫曼编码
    private static void getCodes(Node node,String code,StringBuilder stringBuilder){
        StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
        stringBuilder1.append(code);
        if(node.data == null) {//非叶子节点
            //向左递归
            getCodes(node.left,"0",stringBuilder1);//左分支则添加0
            //向右递归
            getCodes(node.right,"1",stringBuilder1);//右分支则添加1
        }else {	
        	String codeString=stringBuilder1.toString();
            huffmanCodes.put(node.data,codeString);//遇到叶子节点,则添加map,key=1字节中的二进制数据,value=字符串编码
            
            codeString+="\n";
            String To="->";
            String NodeData=node.toString();
            
            byte[]dictCode=codeString.getBytes();
            try {
            	dictOS.write(NodeData.getBytes());
            	dictOS.write(To.getBytes());
				dictOS.write(dictCode);
			} catch (IOException e) {
				// TODO 自动生成的 catch 块
				e.printStackTrace();
			}
        }
    }
    
    //根据原字节数组和哈夫曼编码表对数据进行压缩
    //函数功能:通过对原先的字节数组进行哈夫曼编码,然后将生成的哈夫曼编码(二进制)字符串转为字节数组
    private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
        StringBuilder builder = new StringBuilder();
        StringBuilder builder1 = new StringBuilder();
        for(byte b : bytes){
            builder.append(huffmanCodes.get(b));//将原字节数组用哈夫曼编码表示,存放再builder字符串中
        }
       
        //加7的原因是最大限度保证二进制数不丢失,且不浪费空间
        //例如:1个二进制数(1),若length=builder.length()/8=1/8=0 则转换为0字节
        //而length = (builder.length() + 7) / 8=8/8=1则转换为1个字节
        //而当7个二进制时(1111000),length = (builder.length() + 7) / 8=14/8=1,转换为1个字节,不会转换为2个字节,这样就不会浪费空间
        int length = (builder.length() + 7) / 8 ;//因为哈夫曼编码是二进制数,将二进制转化为字节
        System.out.println("压缩率:"+(length*1.0/bytes.length)*100+"%");
        

        byte[] by = new byte[length];
        int index = 0; 
        String str;
        for (int i = 0; i < builder.length(); i+= 8) {//将用哈夫曼编码(二进制)表示的字符串转为字节数组
            if((i + 8) > builder.length()){
                str = builder.substring(i);//从i开始往后截取
                by[index] = (byte)Integer.parseInt(str,2);//将str中的字符串("10101...")转为2进制数,然后转换为1字节(8个二进制转为1个字节,多余的不要),并放入字节数组by中
                index++;
            }else {
                str = builder.substring(i, i + 8);
                by[index] = (byte)Integer.parseInt(str,2);
                index++;
            }
        }
        return by;
    }
    
    //函数功能:通过原字节数组得到哈夫曼二进制编码转换后的字节数组
    private static byte[] huffmanZip(byte[] bytes){
        //获取通过字节数组转化为节点数组的ArrayList<Node>集合,其中每个节点由二进制数据和权重组成
        ArrayList<Node> nodes = getList(bytes);
        //通过节点数组获取哈夫曼树
        Node root = createHuffmanTree(nodes);
        //通过哈夫曼树获取哈夫曼编码表
        Map<Byte,String>huffmanCodes = getCodes(root);
        //对原字节数组进行哈夫曼编码,生成的哈夫曼编码组成的二进制字符串,得到二进制字符串转换得到的字节数组
        byte[]huffmanCodesBytes = zip(bytes,huffmanCodes);
        return huffmanCodesBytes;
    }
    /**
     * 将字节类型的十进制转换为二进制的字符串类型
     * @param flag 是否是最后一位,最后一位不需要补高位
     * @param b 带转换的字节
     * @return
     */
    private static String byteToBitString(boolean flag,byte b){
        int temp = b;
        if(flag){
            temp |= 256;//正数需要补高位;
        }
        String str = Integer.toBinaryString(temp);
        if(flag){
            return str.substring(str.length() - 8);
        }else
            return str;
    }
    /**
     * 解压
     * @param huffmanCodesBytes 哈夫曼编码后的字节数组
     * @param huffmanCodes 哈夫曼编码表 key=二进制数据,value=哈夫曼编码
     * @return 哈夫曼编码前的字节数组
     */
    public static byte[] deCode(byte[]huffmanCodesBytes,Map<Byte,String> huffmanCodes){
        boolean flag = true;
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < huffmanCodesBytes.length; i++) {//将哈夫曼编码后的字节数组转为二进制字符串
            byte b = huffmanCodesBytes[i];
            if(i == huffmanCodesBytes.length - 1)
                flag = false;
            builder.append(byteToBitString(flag,b));
        }
        String str = builder.toString();
        //将hashMap反转,得到map,key=哈夫曼编码,value=1字节中的二进制数据
        Map<String,Byte> map = new HashMap<>();
        for (Map.Entry<Byte,String> entry : huffmanCodes.entrySet()){
            map.put(entry.getValue(),entry.getKey());
        }
        ArrayList<Byte> list = new ArrayList<>();
        int start = 0;
        int end = 1;
        while(start < str.length()){//将哈夫曼编码后的二进制字符串转为编码前的数据,并存放在字节数组list中
            while(end < str.length() && map.get(str.substring(start,end)) == null){//获取前缀编码
                end++;
            }
            list.add(map.get(str.substring(start,end)));//将前缀编码转为1字节中的二进制数据
            start = end;
        }
        byte[] b = new byte[list.size()];
        for (int i = 0; i < b.length; i++) {//将list数组转为字节数组
            if(list != null) {
                b[i] = list.get(i);
            }
        }
        return b;
    }
    /**
     * 压缩文件
     * @param srcFile 需要压缩的文件路径
     * @param dstFile 压缩后的文件路径
     * @return 
     */
    public static void zipFile(String srcFile,String dstFile){
        //创建输入流
        FileInputStream is = null;
        //创建输出流和对象输出流
        FileOutputStream os = null;
        ObjectOutputStream oos = null;
        try {
            is = new FileInputStream(srcFile);
            b = new byte[is.available()];//is.available()返回文件剩余的字节数,这里刚开始读取,所以返回的是文件所有的字节数
            is.read(b);//读取b.length个字节的数据,并存储到字节数组b中
            os = new FileOutputStream(dstFile);
            oos = new ObjectOutputStream(os);
            byte[] huffmanBytes = huffmanZip(b);//通过原字节数组b得到哈夫曼二进制编码转换后的字节数组
            oos.writeObject(huffmanBytes);//向目标文件路径写入哈夫曼二进制编码转换后的字节数组
            oos.writeObject(huffmanCodes);//写入huffmanCodes 哈夫曼编码表 key=二进制数据,value=哈夫曼编码,写入这个的原因是后面解压时可以直接提取字典,便于解压
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                oos.close();
                os.close();
                is.close();
                dictOS.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    /**
     * 解压文件
     * @param zipFile 需要解压的文件路径
     * @param dstFile 解压后的文件路径
     * @return 
     */
    public static void unZipFile(String zipFile,String dstFile){
        //创建输入流和对象输入流
        FileInputStream is = null;
        ObjectInputStream ois = null;	
        //创建输出流
        FileOutputStream os = null;
        try {
            is = new FileInputStream(zipFile);
            ois = new ObjectInputStream(is);
            byte[] huffmanBytes = (byte[])ois.readObject();//将哈夫曼编码后的文件转为字节数组
            Map<Byte,String> huffmanCodes = (Map<Byte,String>)ois.readObject();//将哈夫曼编码后的文件转为哈夫曼字典,key=1字节的二进制数据,value=哈夫曼二进制编码字符串
            //ObjectInputStream::readObject()根据类的对象,类的签名,类的非静态字段的值等读取对象,意味着可以读取到不同的对象,并且不会乱
            byte[] bytes = deCode(huffmanBytes,huffmanCodes);//解压,得到哈夫曼编码前的字节数组
            os = new FileOutputStream(dstFile);
            os.write(bytes);//写入哈夫曼编码前的字节数组,即未被压缩的文件
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                os.close();
                ois.close();
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
//        String constant = "i like like like java do you like a java";
//        System.out.println(constant);
//        byte[] constantBytes = constant.getBytes();
//        byte[] huffmanCodesBytes = huffmanZip(constantBytes);
//        Map<Byte, String> huffmanCodes = getCodes(createHuffmanTree(getList(constantBytes)));
//        byte[] sourceBytes = deCode(huffmanCodesBytes, huffmanCodes);
//        System.out.println(new String(sourceBytes));
//        
        //测试压缩文件
        String srcFile = "d://Test//11.txt";//要压缩的文件路径
        String dstFile = "d://Test//1.huff";//压缩后的文件路径
        dictFile="d://Test//dictAndFrequent.txt";//字典和频率文件,压缩的字节与对应的哈夫曼编码
        try {
			dictOS = new FileOutputStream(dictFile);
		} catch (FileNotFoundException e) {
			// TODO 自动生成的 catch 块
			e.printStackTrace();
		}
        zipFile(srcFile,dstFile);
        System.out.println("压缩成功!");
 
        //测试解压文件
        String zipFile = "d://Test//1.huff";//要解压的文件路径
        String dst1File = "d://Test//2.txt";//解压后的文件路径
        unZipFile(zipFile,dst1File);
        System.out.println("解压成功!");
    }
}

八、参考资料

赫夫曼编码实现二进制文件压缩解压_程序dunk的博客-CSDN博客_二进制数据压缩

在参考资料的基础上做了详细解释与优化,减小了参考资料的时间复杂度,优化了界面操作。

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

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

相关文章

【机器学习】李宏毅-食物图像分类器

李宏毅-食物图像分类器1 实验目的 掌握使用Pytorch的使用方法&#xff1a; Pytorch的安装以及环境搭建Pytorch处理数据Pytorch计算梯度以及搭建神经网络Pytorch训练模型 并使用Pytorch来训练CNN模型&#xff0c;实作一个食物的图像分类器。 2 实验要求 可以使用tensorflow或…

print()函数的使用

一、print()函数共三种类型的使用方法二、代码展示

Excel求解运输问题——以福斯特公司问题为例

目录 1.1 问题 福斯特问题例 1.2 数学模型 1.3 excel求解 第一步&#xff1a;建立一个工作表 第二步&#xff1a;求解器求解 1.1 问题 运输问题通常出现在计划货物配送机从供给地区到达需求地区之间的服务中&#xff0c;一般供给地区货物数量有限&#xff0c;需求地区货物…

时间序列分析原理

一、定义 时间序列&#xff0c;是指将某种现象某一个统计指标在不同时间上的各个数值&#xff0c;按时间先后顺序排列而形成的序列 生活中各领域各行业有很多时间序列的数据&#xff0c;销售额&#xff0c;顾客数&#xff0c;访问量&#xff0c;股价&#xff0c;油价&#xff…

k8s 部署canal admin及server单机服务

目录 1. 前言 2. 部署canal-admin 2.1 数据库初始化 2.2 canal-admin k8s yaml部署文件 3. 部署canal-server单机 1. 前言 canal官方文档&#xff1a;https://github.com/alibaba/canal/wiki 主要用途是基于 MySQL 数据库增量日志解析&#xff0c;提供增量数据订阅和消费。…

一文读懂深度学习中文本处理的4种方式

一、序言文本处理方式是深度学习领域中自然语言处理的基础&#xff0c;即把文本转变成计算机识别的语言过程。转变之后才能用算法做后续的文本分析和理解。所以有必要了解文本处理的几种方式&#xff0c;做到对不同的场景采用不同的处理方式。常见的文本处理方式有独热编码(one…

OSI参考模型与TCP/IP模型比较

TCP/IP模型 — TCP/IP协议簇 TCP/IP分为两种体系结构&#xff0c;一种是分为四层网络接口层、网络层、传输层和应用层&#xff1b;另外一种是分为五层物理层、数据链路层、网络层、传输层和应用层。这两种体系结构都对。 TCP/IP四层模型 — TCP/IP标准模型 TCP/IP五层模型 — T…

hive数据仓库搭建

一、虚拟机安装CentOS7并配置共享文件夹 二、CentOS 7 上hadoop伪分布式搭建全流程完整教程 三、本机使用python操作hdfs搭建及常见问题 四、mapreduce搭建 五、mapper-reducer编程搭建 六、hive数据仓库安装 hive数据仓库搭建一、hive数据仓库安装1.1下载hive安装包1.2修改配置…

2022年亚太杯APMCM数学建模大赛C题全球是否变暖求解全过程文档及程序

2022年亚太杯APMCM数学建模大赛 C题 全球是否变暖 为方便各位阅览及了解掌握亚太杯的写作技巧&#xff0c;这里非技术使用中文&#xff0c;公式部分由于翻译过程繁琐使用英文来撰写此文章. 原题再现&#xff1a; 加拿大的49.6C创造了地球北纬50以上地区的气温新纪录&#xf…

SpringBoot的字符画(banner.txt)

好多小伙伴最近问我如何在启动项目的时候&#xff0c;在控制台打印如下的图案logo 其实很简单&#xff0c;Springboot提供了很方便的操作&#xff0c;在resource目录先新建一个banner.txt 然后将我们想要的图案拼接即可&#xff0c;如下图&#xff1a; 配置完成后正常启动项目…

TorchServe 详解:5 步将模型部署到生产环境

内容导读 TorchServe 自 2020 年 4 月推出至今&#xff0c;经历了 2 年多的发展&#xff0c;变得愈发成熟和稳定&#xff0c;本文将对 TorchServe 进行全面介绍。 TorchServe 是 PyTorch 中将模型部署到生产环境的首选解决方案。它是一个性能良好且可扩展的工具&#xff0c;用 …

【Java】多线程详解

目录 一、线程简介 进程&#xff08;Process &#xff09;与 线程&#xff08;Thread&#xff09; 二、线程创建 1、线程Thread 1.1. 步骤 1.2 应用 1.3 案例&#xff1a;下载图片 2、实现Runnable接口 2.1 步骤 2.2 应用 3.小结 3. 实现Callable接口&#xff08;了解…

利用Github账号实现(多个)个人网站

创建仓库 命名一定要是<username>.github.io这种形式username就是自己github账号的用户名。因为我已经有这个仓库&#xff0c;所以这里的报错可以忽略。 往仓库放入静态html文件 命名为index.html&#xff0c;随便放点内容。 在设置里面开启github page 这里可以选…

verilog学习笔记- 2)时序约束文件

目录 为什么要创建时序约束文件&#xff1f; 时序&#xff1a; 创建时序约束文件&#xff1a; 为什么要创建时序约束文件&#xff1f; 对于一些简单的工程时序的要求并不是特别的严格&#xff0c;添不添加时序约束文件影响不大。但是对于一些复杂的工程&#xff0c;涉及到高…

【SpringBoot应用篇】SpringBoot 启动扩展点/常用接口

【SpringBoot应用篇】SpringBoot 启动扩展点/常用接口SpringBoot常用的接口ApplicationContextInitializerBeanDefinitionRegistryPostProcessorBeanFactoryPostProcessorInstantiationAwareBeanPostProcessorSmartInstantiationAwareBeanPostProcessorBeanFactoryAwareApplica…

Dom 重点核心

关于dom操作&#xff0c;主要针对元素的操作。 主要有创建&#xff0c;增&#xff0c;删&#xff0c;改&#xff0c;查&#xff0c;属性操作&#xff0c;事件操作。 一、创建 1.document.write 2.innerHTML 3.createElement 二、增 1.appendChild&#xff08;在后面添加&am…

【iOS】块与大中枢派发

文章目录[TOC](文章目录)前言理解“块”这一概念块的基础知识块的内部结构全局块&#xff0c;栈块&#xff0c;堆块为常用的块类型创建typedef用handler块降低代码分散程度用块引用其所属对象时不要出现保留环多用派发系列&#xff0c;少用同步锁多用GCD&#xff0c;少用perfor…

Linux/ARM下QT MQTT库的编译安装

&#x1f482; 个人主页:风间琉璃&#x1f91f; 版权: 本文由【风间琉璃】原创、在CSDN首发、需要转载请联系博主&#x1f4ac; 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦目录 一、 Linux 环境下编译安装 二、 ARM Linux 环境下安装 一、 Linux 环境下…

破解分布式光伏运维难题,光伏+屋面数字化监控融合是关键

2022年5月&#xff0c;在东南地区某城市一幢写字楼上&#xff0c;技术人员们正不辞辛苦爬上数十米高的墙面&#xff0c;对写字楼的屋面和墙面进行勘察。随后&#xff0c;他们准备赶在台风季来临之前完成该写字楼的建筑光伏一体化项目安装与部署。 这是森特士兴集团股份有限公司…

fixed:error:0308010C:digital envelope routines::unsupported

目录1.故障现场2. 问题分析3. 修复方案4. 参考文献1.故障现场 最近由于一些原因&#xff0c;从Mac OSX 迁移到 Windows 平台&#xff0c;在尝试运行基于vue-element-admin 项目时&#xff0c;发生了如下异常&#xff1a; error:0308010C:digital envelope routines::unsuppor…