myZip 자체 제작 압축 소프트웨어(허프만 트리 기반으로 완성)----샤오멍에게 새로운 것을 가르치기 위해 반 리터의 피를 토했습니다(。→‿←。)(손이 있는 한 나를 때리지 않을 것입니다 )

시연 영상 링크 : 클릭하시면 보실 수 있습니다!
여기에 이미지 설명 삽입

원칙:

UTF-8 인코딩 방법에 따르면 문자는 종종 여러 바이트를 차지하므로 이진 비트로 표현하면(또한 이진 비트를 사용하여 디코딩할 수 있음) 공간을 크게 절약할 수 있습니다.
그리고 발생횟수를 이용하여 허프만트리를 구축하여 대표이진코드를 구하기 때문에(조금생각해봐도 되나, 어떻게 구하는가?), 발생빈도가 높을수록 대표이진코드가 짧아지므로, 더 우수한 압축 효과

Created with Raphaël 2.3.0 得到文件的文本 借助HashMap,key存字符,value存出现次数,便于得到权值 借助于哈夫曼树(依据权值), 得到字符编码 编码写入压缩文件(注意同时写入所需的hashMap) 读取出压缩的编码内容,根据hashMap译码出来,写到解压文件
지식 포인트 근본적인
파일 입출력 입력 및 출력 스트림을 사용하여 완료(OutputStream/InputStream), 바이트 단위 읽기 및 쓰기 작업, 쓰기 및 읽기 방법
해시 테이블 키를 사용하여 값의 연결된 목록 배열을 찾으십시오. 제가 작성한 기사를 읽을 수 있습니다: hashMap
허프만 트리 각 데이터의 가중치에 따라 트리를 구축합니다. 즉, 가중치가 가장 작은 두 값을 루트 노드가 가중치의 합인 트리로 병합한 다음 부모 노드의 가중치를 나머지 노드에 넣습니다. 데이터, 루프는 숫자만 있을 때까지 계속됩니다.
문자열 연결 기본 논리는 스플라이싱할 때마다 StringBuilder 개체가 생성되어야 하며 그런 다음 Copy 메서드(문자열 순회)가 개체에 할당되고 스플라이싱 후에 값을 문자열로 반환한다는 것입니다.

성취하다:

압축 방법:

1. 문서를 읽고 hashMap<>을 빌드합니다.

입력 및 출력 스트림을 사용하려고 할 때마다 예외를 throw하도록 주의해야 합니다.
동시에 코드 세그먼트에 작성된 방법으로 텍스트를 얻을 수 있습니다.
마지막으로 hashMap의 사용법도 매우 간단하여 먼저 문자열의 문자배열을 순회하여 해당 문자(키)의 존재여부를 확인한 후 가중치(발생횟수)를 수정하여 저장하면 됩니다. 입금 후 Set을 사용하여 초기 노드를 매핑하고 생성합니다.

//读取待压缩文件
    private String readWaitCop(File wait) throws IOException {
    
    
        //读取文件内容
        FileInputStream fip = new FileInputStream(wait);
        byte[] buf = new byte[fip.available()];
        //**重中之重,一定要先读文本内容,填充这个byte数组,不然出大乱子——————大冤种的劝告 ~_~。
        /**
         * 在文件读写里:string 和 byte 可以互换,达到input/output都成为可阅读文本
         * 写文件:fop.write(String.getBytes());
         * 读文件: fip.read(bytes[]);
         *         new String(bytes[])
         */
        fip.read(buf);
        return new String(buf);
    }
    
     //获取权值和内容,构建初始节点
    private String initSet(File wait) throws IOException {
    
    
        String str = readWaitCop(wait);
        HashMap<Character,Integer> hashMap = new HashMap<Character,Integer>();
        char[] chars = str.toCharArray();
        for (char cc:chars) {
    
    
            String sinStr = String.valueOf(cc);
            if(hashMap.containsKey(cc)){
    
    
                hashMap.put(cc, hashMap.get(cc) + 1);
            }else{
    
    
                hashMap.put(cc,1);
            }
        }
        Set<Map.Entry<Character, Integer>> entrys = hashMap.entrySet();
        for(Map.Entry<Character, Integer> entry:entrys) {
    
    
            Node node = new Node(null,null, entry.getKey(), entry.getValue(), null);
            arrayList.add(node);
        }
        return str;

2. 허프만 트리를 가져옵니다.

먼저 노드 설정을 이해해야 합니다. 내 내부 노드 클래스를 볼 수 있습니다. 왼쪽
및 오른쪽 노드는 트리를 만드는 데 편리하고 가중치는 비교에 사용되며 도로는 인코딩을 기록할 수 있습니다. 내용은 문자 내용을 기록합니다.

//内部节点类
    public class Node{
    
    
        private Node left;
        private Node right;
        //出现频率(权值)
        private int num;
        //记载路径
        private String road;
        //记载内容
        private char content;
        public Node(Node left, Node right,char content, int num,String road) {
    
    
            this.left = left;
            this.right = right;
            this.content = content;
            this.num = num;
            this.road = null;
        }
    }

아이디어는 원리상 허프만 트리와 동일합니다 어떻게 실현했는지 보시면 알겠지만
최소 가중치를 찾을 때마다 반복 비교를 방지하기 위해 노드를 삭제해야 한다는 점은 주목할 만합니다
. 마지막 노드가 남아 있을 때 트리가 구축되고 루트 노드를 기록합니다.
이 노드의 ArrayList는 항상 사용되므로 마지막 노드를 지우는 것을 잊지 마십시오.

//搭建树
    private void setTree(){
    
    
        while(arrayList.size() != 1){
    
    
            //最小权值结点
            Node bro1 = Min();
            Node bro2 = Min();
            Node father = new Node(bro1,bro2,'0',bro1.num + bro2.num,null);
            arrayList.add(father);
        }
        root = arrayList.get(0);
        arrayList.remove(0);
    }
    //找到最小权值
    private Node Min(){
    
    
        int minIndex = 0;
        for (int i = 0; i < arrayList.size(); i++) {
    
    
            if(arrayList.get(minIndex).num > arrayList.get(i).num){
    
    
                minIndex = i;
            }
        }
        Node node = arrayList.get(minIndex);
        arrayList.remove(minIndex);
        return node;
    }

3. 문자 코드를 가져와서 hashMap<code, character>에 저장합니다.

여기서 언급한 문자 인코딩은 노드의 길인데 어떻게 구현해야 할까요?
사실 허프만 트리를 순회할 수 있는데, 왼쪽으로 가면 '0', 오른쪽으로 가면 '1'로 인코딩이 풀리고 전에 사용했던 가중치가 좀 많이 쓰이는 데이터 레이어를 좀 더 적게 만들어서 압축 효과를 얻기 위해 인코딩이 더 짧습니다.

다음 코드가 인코딩됩니다:
실제 데이터를 저장하는 노드는 실제로 리프 노드입니다. 즉, 노드에 자식 노드가 없습니다.그 이유를 생각해 볼 수 있습니다.
순회 트리에는 실제로 선순 순회, 후순 순회 등 다양한 방법이 있지만 제가 쓴 것은 재귀를 피하고 생각을 단순화할 수 있는 계층 순회를 사용합니다.
그러나 Queue 대기열은 내가 직접 작성하므로 일반 대기열과 다를 수 있습니다.
여기에 이미지 설명 삽입
계층적 순회를 설명할 수 있습니다. 허프만 트리는 완벽한 이진 트리이므로 각 노드에는 하위 노드가 없거나 두 개 있으므로 Queue 데이터 구조를 사용하여 노드를 팝하고 두 개의 노드를 푸시할 수 있습니다. FIFO 메커니즘, 순회 효과가 달성됩니다.
그렇다면 hashMap에 저장되는 이유는 중요하다.복선, 설명이 이어집니다.

//得到路径
    private void setRoad() throws IOException {
    
    
        Queue<Node> queue = new Queue<>();
        queue.enqueue(root);
        root.road = "";
        while (!queue.isEmpty()){
    
    
            if(queue.peak().left != null) {
    
    
                queue.enqueue(queue.peak().left);
                queue.peak().left.road = queue.peak().road + '0';
            }
            if(queue.peak().right != null){
    
    
                queue.enqueue(queue.peak().right);
                queue.peak().right.road = queue.peak().road + '1';
            }else{
    
    
                arrayList.add(queue.peak());
            }
            queue.dequeue();
        }
        //得到readMap
        for (int i = 0; i < arrayList.size(); i++) {
    
    
            readMap.put(arrayList.get(i).road,arrayList.get(i).content);
        }
    }

4. hashMap<Character, String>에 따라 디코딩하여 0/1 문자열을 얻습니다.

여기에서 우리가 읽은 텍스트를 순회하고 hashMap에 따라 문자를 해당 0/1 코드로 바꾸고 새 문자열을 얻는 것은 비교적 간단하지만 1바이트 = 8비트이므로 a에 대한 보완이 필요하다는 것을 이해해야 합니다. 8의 배수, 그렇지 않으면 바이트로 변환하여 파일에 쓸 때 오류가 발생하며 동시에 완료 후 문자열 헤더에 보조 자릿수를 추가해야 합니다.

 		//存入内容
        char[] chars = comContent.toCharArray();
        //先将comContent清空,得到有效位数+二进制字符串
        /**
         * 这是压缩时最耗时间的部分,可以考虑换成string来算,因为这个比char的hashcode算的快,
         * 因为算hashCode从map取值,重复过多了
         */
        //注意清空啊,嘚吧!
        comContent = "";
        StringBuilder builder = new StringBuilder();
        for (char cc:chars) {
    
    
                //hashMap.get可查看源码,返回值为V;
                builder.append(hashMap.get(cc));
        }

        //存入补足编码
        int num = 8 - builder.length() % 8;
        for (int i = 0; i < num; i++) {
    
    
            builder.append('0');
        }
        //存入补的位数,直接存数字,eg:……+"num"。
        byte b = (byte) num;
        //运用 StringBuilder ,出现了heap溢出
        builder.insert(0,String.valueOf(b));
        comContent = builder.toString();

주목할 가치가 있습니다: comContent는 initSet(wait)[구성 노드 초기화]를 통해 얻어지며, char[] 배열로 변환하는 것을 기억하고, 인코딩된 문자열을 얻기 위해 그것을 지우는 것을 기억하십시오.

5. (0/1 string -> byte[]) 문서에 쓰기(hashMap<encoding, character>가 작성됨):

여기서는 주로 0/1 문자열을 바이트로 변환하는 방법에 대해 이야기하는데, '0'을 읽으면 바이트가 왼쪽으로 이동하고, '1'을 읽으면 바이트를 왼쪽으로 이동하여 +1이 된다. , 8회 읽기 btye 값을 형성하고 바이트【】에 쓸 수 있습니다.
그리고 byte[]의 첫 번째 비트는 압축 해제 프로세스에 편리한 마지막 바이트의 유효 0/1 비트를 저장하므로 텍스트를
파일 작성 요구 사항을 충족하는 byte[] 배열로 변환합니다.

	//写入到文件的具体方法
    private void outputStream(String comContent,File after) throws IOException {
    
    
        //得到要写入字符串的字符
        char[] chars = comContent.toCharArray();
        //得到需要写多少byte,初始位置存num
        byte[] bytes = new byte[(chars.length - 1) / 8 + 1];
        //得到最后一个字节中的几位有效
        int num = 8 - ((int) chars[0] - 48);
        //(byte)转型,可以得到int num的最后一个字节
        bytes[0] = (byte) (num & 255);
        //第一位是存储需要丢弃几个数字,所以可以从下标1开始
        for (int i = 1; i < bytes.length; i++) {
    
    
            byte b = 0;
            int bb = i - 1;
            for (int j = 1; j <= 8; j++) {
    
    
                if(chars[bb * 8 + j] == '0'){
    
    
                    b = (byte) (b << 1);
                }else{
    
    
                    b = (byte) (b <<1 + 1);
                }
                bytes[i] = b;
            }
        }
        //开写开写
        FileOutputStream fop = new FileOutputStream(after);
        /**
         * 可以去仔细研究:::
         * 写入readMap,只能用ObjectInputStream,写入任何类型
         * 但要注意,该类型要实现java.io.Serializable,使其“序列化”
         */
        ObjectOutputStream foop = new ObjectOutputStream(fop);
        //写入任何类型
        foop.writeObject(readMap);
        fop.write(bytes);
    }

여기서 우리는 3단계를 강조할 필요가 있습니다.복선예, 인코딩 규칙이 다르기 때문에 압축을 풀고 읽으려면 당시 저장한 hashMap인 참조용 디코딩 테이블이 필요합니다. 이 양식이 부족하다면 제2차 세계대전 당시 전보가 도청된 후 그의 얼굴 표정은 마치 폐지된 것처럼 당황하여 할 말을 잃었을 것이라고 상상할 수 있습니다. 디코딩 테이블은 강력하고 Serializable 인터페이스를 구현하는 모든 유형에 쓸 수 있는 ObjectOutputStream(둘 다 파일 I/O 스트림 기반)의 도움으로 작성됩니다.
궁금하신 분들은 확인하시면 됩니다.

압축 해제 방법:

1. (바이트[]->0/1 문자열) 프로세스:

참고로 쉬프트는 7->0에서 연속 쉬프트 되므로 0/1 문자열을 순서대로 얻을 수 있으니 알아서 생각하시면 됩니다.

for (int i = 1; i < bytes.length; i++) {
    
    
            //将byte转换为字符串
            for (int j = 7; j >= 0 ; j--) {
    
    
                int num = bytes[i];
                if(((num >> j) & 1) == 0){
    
    
                    codeBulider.append('0');
                }
                else{
    
    
                    codeBulider.append('1');
                }
            }
        }

2. read hashMap<code, character>, (0/1 string -> String)에 따라 실제 콘텐츠 문자열을 가져옵니다.

hashMap<encoding, character> 디코딩 테이블 읽기

        FileInputStream fip = new FileInputStream(wait);
        // Object的输入输出流都是基于file输入输出流
        ObjectInputStream fiop = new ObjectInputStream(fip);

디코딩을 시작하고 sinBuilder(단일 문자)를 사용하여 0/1을 연결한 다음 키(코드)가 존재하는지 확인하고 코드가 존재할 때까지 반복한 다음 contentBuilder를 사용하여 키를 통해
hashMap을 연결하여 값을 찾습니다( 문자) , 브라우징이 완료되고 디코딩이 완료되었습니다.在这里插入代码片

for (char cc:chars) {
    
    
            sinBuilder.append(cc);
            if(readMap.containsKey(sinBuilder.toString())){
    
    
                contentBuilder.append(readMap.get(sinBuilder.toString()));
                sinBuilder.delete(0, sinBuilder.length());
            }
        }
        content = contentBuilder.toString();

3. 압축 해제된 파일에 쓰기:

String에 쓸 때 String.getBytes() 메서드를 사용해야 합니다.

		FileOutputStream fop = new FileOutputStream(after);
        fop.write(content.getBytes());

인터페이스 디자인:

외관 디자인:

양식을 만든 다음 구성 요소를 만드는 것 이상이며 코드는 소스 코드에 있으며 매우 간단합니다.

액션리스너 생성

내부 리스너 클래스를 전달할 수 있으므로 필요한 데이터를 전달하지 않고 마음대로 호출할 수 있습니다.
예를 들어 여기서는 작업하기 전에 주소를 먼저 얻지 않고 텍스트 상자의 텍스트를 직접 사용할 수 있습니다.
참고: 내부 리스너 클래스 끝에 세미콜론이 있으며 긴 문장으로 이해할 수 있습니다.

ActionListener ac = new ActionListener() {
    
    
            @Override
            public void actionPerformed(ActionEvent e) {
    
    
                Color tuWhite = new Color(238,238,238);
                g.setColor(tuWhite);
                g.fillRect(300,550,400,400);
                g.drawImage(starBuf,300,550,400,400,null);
                long start = System.currentTimeMillis();
                String btname = e.getActionCommand();
                File wait = new File(jt1.getText());
                File after = new File(jt2.getText());
                if(btname.equals("压缩")){
    
    
                    try {
    
    
                        hf.copCode(wait,after);
                    } catch (IOException ex) {
    
    
                        ex.printStackTrace();
                    }
                }else{
    
    
                    try {
    
    
                        hf.unPack(wait,after);
                    } catch (IOException ex) {
    
    
                        ex.printStackTrace();
                    } catch (ClassNotFoundException ex) {
    
    
                        ex.printStackTrace();
                    }
                }
                long end = System.currentTimeMillis();
                double time = (end - start) / 1000;
                System.out.println(btname + ":  "+ time + "  S");
                g.setColor(tuWhite);
                g.fillRect(300,550,400,400);
                g.drawImage(finBuf,300,550,400,400,null);
            }
        };

실행 중인 그림 로고 추가

버튼을 클릭하면 로딩 바 그림을 그릴 수 있고 종료 후 봉인 성공 그림을 그릴 수 있습니다.이 코드는 모두 위에 있습니다.

최적화

부스트 속도

원래의 스트링 스플라이싱 프로세스를 사용하면 매우 느리고 참을 수 없는 지점에 도달했음을 알 수 있습니다. 여기에서 나만의 솔루션을 제안합니다.

1. 최소 무게 찾기:

솔루션: 버블 정렬과 같은 저수준 정렬을 사용하는 경우 효율성이 낮고 문자가 많을 때 당연히 적용할 수 없습니다. 퀵정렬을 이용하거나 저처럼 최소값의 첨자를 직접 찾아내어 속도를 크게 향상시킬 수 있습니다.

2. 시간 소모적인 문자열 접합:

솔루션: 문자열의 "+" 를 직접 사용하여 연결하는 경우 기본 원칙에 따라 얼마나 많은 시간이 낭비되는지 생각할 수 있습니다.모든 연결을 통과하고 복사한 다음 연결해야 합니다. [시간 복잡도는 (n^2 )]. 따라서 모든 콘텐츠를 연결한 다음 이를 String 유형으로 변환하는 StringBuilder 객체만 생성하면 됩니다.
그 효과는 매우 놀라운데, 당시에는 1M 미만의 파일에 200초 이상이 걸렸지만 지금은 100M 파일의 경우 약 6초 밖에 걸리지 않습니다.
관심이 있는 경우 String의 추가 방법을 살펴볼 수 있습니다.

부스트 용량

StringBuilder에는 제한이 있습니다.

300M 텍스트 압축을 사용하면 힙 오버플로 예외가 보고됩니다.StringBuilder의 소스 코드를 쿼리한 결과 하위 레이어가 char[] 배열이고 길이가 int 유형으로 제한되어 텍스트가 너무 많은 경우 , 메모리 오버플로가 쌓일 것입니다.
해결책: StringBuilder【】의 배열을 생성하고 연결할 문자 수를 지정한 다음 다음 StringBuilder로 이동하고 마지막으로 접합하여 용량을 확장할 수 있습니다.
이것은 개인적인 생각일 뿐 아직 완성되지 않았으니, 높은 이상을 가진 분들이 운영해 주셨으면 합니다.

jar 파일로 패키징: 그림과 같이

항아리 포장 과정

소스 코드(모든 코드 포함):

1. 인터페이스

package fcj1028;

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

public class Ui extends JFrame {
    public void ui() throws IOException {
        Huffman_Compress hf = new Huffman_Compress();
        this.setTitle("压缩软件");
        this.setSize(1000,1000);
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        this.setLocationRelativeTo(null);
        JLabel jl1 = new JLabel("待处理文件:");
        jl1.setFont(new java.awt.Font("Dialog", 1, 30));
        JLabel jl2 = new JLabel("处理后文件:");
        jl2.setFont(new java.awt.Font("Dialog", 1, 30));
        JTextField jt1 = new JTextField();
        Dimension dim1 = new Dimension(800,100);
        Dimension dim2 = new Dimension(400,150);
        jt1.setFont(new java.awt.Font("Dialog", 1, 30));
        JTextField jt2 = new JTextField();
        jt2.setFont(new java.awt.Font("Dialog", 1, 30));
        JButton jb1 = new JButton("压缩");
        jb1.setFont(new java.awt.Font("Dialog", 1, 30));
        JButton jb2 = new JButton("解压");
        jb2.setFont(new java.awt.Font("Dialog", 1, 30));
        jt1.setPreferredSize(dim1);
        jt2.setPreferredSize(dim1);
        jb1.setPreferredSize(dim2);
        jb2.setPreferredSize(dim2);
        this.add(jl1);
        this.add(jt1);
        this.add(jl2);
        this.add(jt2);
        this.add(jb1);
        this.add(jb2);
        this.setLayout(new FlowLayout());
        this.setVisible(true);
        //先可视化,再得到画笔
        Graphics g = this.getGraphics();
        File starPic = new File("C:\\Users\\27259\\Pictures\\java_pic\\9df86687_E848203_9ea3a43f-removebg-preview.png");
        BufferedImage starBuf = ImageIO.read(starPic);
        File finPic = new File("C:\\Users\\27259\\Pictures\\java_pic\\R-C-removebg-preview(1).png");
        BufferedImage finBuf = ImageIO.read(finPic);
        ActionListener ac = new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                Color tuWhite = new Color(238,238,238);
                g.setColor(tuWhite);
                g.fillRect(300,550,400,400);
                g.drawImage(starBuf,300,550,400,400,null);
                long start = System.currentTimeMillis();
                String btname = e.getActionCommand();
                File wait = new File(jt1.getText());
                File after = new File(jt2.getText());
                if(btname.equals("压缩")){
                    try {
                        hf.copCode(wait,after);
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }else{
                    try {
                        hf.unPack(wait,after);
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    } catch (ClassNotFoundException ex) {
                        ex.printStackTrace();
                    }
                }
                long end = System.currentTimeMillis();
                double time = (end - start) / 1000;
                System.out.println(btname + ":  "+ time + "  S");
                g.setColor(tuWhite);
                g.fillRect(300,550,400,400);
                g.drawImage(finBuf,300,550,400,400,null);
            }
        };
        jb1.addActionListener(ac);
        jb2.addActionListener(ac);
    }

    public static void main(String[] args) throws IOException {
        new Ui().ui();
    }
}

```java

```java

```java

2. 압축 소프트웨어

package fcj1028;

import Algorithm.Linear.Queue;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class Huffman_Compress {
    //写到压缩文件里,方便之后,文件读出内容(避免程序结束,所存hashMap消失)
    private HashMap<String,Character> readMap = new HashMap<>();
    //根节点,便于寻找
    private Node root;
    /**哈夫曼树:a.先对节点排序保存到List中
    b.取出List中最小的两个节点,让它们的权值累加,保存新的父节点中
    c.让最小两个节点作为左右子树,把父节点添加到List中重新排序
    d.直到List只有一个节点
   3.设置叶子节点的编码,往左编码为1 往右编码为0
    统计每个叶子节点对应的编码
     */
    //用途:节省空间存储内容
    //原理:使用路径编码即可代表字符,而且越常用字符编码越少
    //用来存节点
    private ArrayList<Node> arrayList = new ArrayList<Node>();

    //内部节点类
    public class Node{
        private Node left;
        private Node right;
        //出现频率(权值)
        private int num;
        //记载路径
        private String road;
        //记载内容
        private char content;
        public Node(Node left, Node right,char content, int num,String road) {
            this.left = left;
            this.right = right;
            this.content = content;
            this.num = num;
            this.road = null;
        }
    }
    //获取权值和内容,构建初始节点
    private String initSet(File wait) throws IOException {
        String str = readWaitCop(wait);
        HashMap<Character,Integer> hashMap = new HashMap<Character,Integer>();
        char[] chars = str.toCharArray();
        for (char cc:chars) {
            String sinStr = String.valueOf(cc);
            if(hashMap.containsKey(cc)){
                hashMap.put(cc, hashMap.get(cc) + 1);
            }else{
                hashMap.put(cc,1);
            }
        }
        Set<Map.Entry<Character, Integer>> entrys = hashMap.entrySet();
        for(Map.Entry<Character, Integer> entry:entrys) {
            Node node = new Node(null,null, entry.getKey(), entry.getValue(), null);
            arrayList.add(node);
        }
        return str;
    }
    //读取待压缩文件
    private String readWaitCop(File wait) throws IOException {
        //读取文件内容
        FileInputStream fip = new FileInputStream(wait);
        /**
         * V1.0:一开始的想法是,得到的是byte【】数组,转换为字符串
         * 其中借用了fip.available(),可以返回文件的字节数
         * 但是问题在于,这样的话byte[]数组并不能转换成字符串内容,反而得到的是其长度
         * 原因:当时也忘记了用fip.read(bytes)存放内容,所以读出长度十分正常
         * 故,我们的想法可以是用将每个字节转换为char。
         * V2.0:由于数字和中文编码并不一样,所以这个方法也要淘汰
         * V3.0:在得到bytes[]后,可以使用String()方法,规定好长度自动会帮我们转型
         * 值得注意的是:String.valueOf()会得到乱码
         */
        byte[] buf = new byte[fip.available()];
        //**重中之重,一定要先读文本内容,填充这个byte数组,不然出大乱子——————大冤种的劝告 ~_~。
        /**
         * 在文件读写里:string 和 byte 可以互换,达到input/output都成为可阅读文本
         * 写文件:fop.write(String.getBytes());
         * 读文件: fip.read(bytes[]);
         *         new String(bytes[])
         */
        fip.read(buf);
        return new String(buf);
    }
    //搭建树
    private void setTree(){
        while(arrayList.size() != 1){
            //最小权值结点
            Node bro1 = Min();
            Node bro2 = Min();
            Node father = new Node(bro1,bro2,'0',bro1.num + bro2.num,null);
            arrayList.add(father);
        }
        root = arrayList.get(0);
        arrayList.remove(0);
    }
    //找到最小权值
    private Node Min(){
        int minIndex = 0;
        for (int i = 0; i < arrayList.size(); i++) {
            if(arrayList.get(minIndex).num > arrayList.get(i).num){
                minIndex = i;
            }
        }
        Node node = arrayList.get(minIndex);
        arrayList.remove(minIndex);
        return node;
    }
    //得到路径
    /**
     * 首先表扬一下自己,通过不断输出语句还是找到了bug
     * 这里的问题并非是树连接失败,而是我(nt)的忘了把队列里的节点叉出去,导致死循环
     */
    private void setRoad() throws IOException {
        Queue<Node> queue = new Queue<>();
        queue.enqueue(root);
        root.road = "";
        while (!queue.isEmpty()){
            if(queue.peak().left != null) {
                queue.enqueue(queue.peak().left);
                queue.peak().left.road = queue.peak().road + '0';
            }
            if(queue.peak().right != null){
                queue.enqueue(queue.peak().right);
                queue.peak().right.road = queue.peak().road + '1';
            }else{
                arrayList.add(queue.peak());
            }
            queue.dequeue();
        }
        //得到readMap
        for (int i = 0; i < arrayList.size(); i++) {
            readMap.put(arrayList.get(i).road,arrayList.get(i).content);
        }
    }
    //写入到文件的具体方法
    private void outputStream(String comContent,File after) throws IOException {
        //得到要写入字符串的字符
        char[] chars = comContent.toCharArray();
        //得到需要写多少byte,初始位置存num
        byte[] bytes = new byte[(chars.length - 1) / 8 + 1];
        //得到最后一个字节中的几位有效
        int num = 8 - ((int) chars[0] - 48);
        //(byte)转型,可以得到int num的最后一个字节
        bytes[0] = (byte) (num & 255);
        //第一位是存储需要丢弃几个数字,所以可以从下标1开始
        for (int i = 1; i < bytes.length; i++) {
            byte b = 0;
            /**
             * V1.0:使用byte存储,思路是想用位运算使得效率更高,但是发现不是想要的效果,存入的都是0
             * 原因:暂时还不清楚,可去看这篇文章
             * https://blog.csdn.net/weixin_39775428/article/details/114176698
             * 所以,现在还是用 * 2.
             */
            int bb = i - 1;
            for (int j = 1; j <= 8; j++) {
                if(chars[bb * 8 + j] == '0'){
                    b = (byte) (b * 2);
                }else{
                    b = (byte) (b * 2 + 1);
                }
                /**
                 * 值得注意:byte表示范围为 -128~127,一旦超出127,会自动转换
                 * 但是读取时就需要主要判断正负,得到第一位了,问题在于补码中的0,存在 10000000,00000000两种情况,需要特殊判断
                 */
                bytes[i] = b;
            }
        }
        //开写开写
        FileOutputStream fop = new FileOutputStream(after);
        /**
         * 可以去仔细研究:::
         * 写入readMap,只能用ObjectInputStream,写入任何类型
         * 但要注意,该类型要实现java.io.Serializable,使其“序列化”
         */
        ObjectOutputStream foop = new ObjectOutputStream(fop);
        //写入任何类型
        foop.writeObject(readMap);
        fop.write(bytes);
    }
    //得到压缩后的编码(除了补足八位,构成byte存入),还需要记录补了几个),并写入到文件
    public void copCode(File wait,File after) throws IOException {
        String comContent = initSet(wait);
        setTree();
        setRoad();
        //利用HashMap存好字符的数据编码,即路径
        HashMap<Character,String> hashMap = new HashMap<>();
        for (Node node:arrayList) {
            hashMap.put(node.content, node.road);
        }
        //存入内容
        char[] chars = comContent.toCharArray();
        //先将comContent清空,得到有效位数+二进制字符串
        /**
         * 这是压缩时最耗时间的部分,可以考虑换成string来算,因为这个比char的hashcode算的快,
         * 因为算hashCode从map取值,重复过多了
         */
        //注意清空啊,嘚吧!
        comContent = "";
        /**
         *     神之一手 ———— 郝为高
         *     由于每次字符串的连接,都会先创建 StringBuilder builder对象,浪费大量时间,不如直接一步到位,先创建出来
         */
        StringBuilder builder = new StringBuilder();
        for (char cc:chars) {
                //hashMap.get可查看源码,返回值为V;
                builder.append(hashMap.get(cc));
        }

        //存入补足编码
        int num = 8 - builder.length() % 8;
        for (int i = 0; i < num; i++) {
            builder.append('0');
        }
        //存入补的位数,直接存数字,eg:……+"num"。
        byte b = (byte) num;
        //运用 StringBuilder ,出现了heap溢出
        builder.insert(0,String.valueOf(b));
        comContent = builder.toString();
        builder.delete(0,builder.length());
        outputStream(comContent,after);
    }

    //解压压缩后的文本
    public void unPack(File wait,File after) throws IOException, ClassNotFoundException {
        //开始读出readMap(sb,你自己要注意你是用的那个文件啊啊啊啊啊!!!!!)
        FileInputStream fip = new FileInputStream(wait);
        // Object的输入输出流都是基于file输入输出流
        ObjectInputStream fiop = new ObjectInputStream(fip);
        HashMap<String,Character> readMap = (HashMap<String, Character>) fiop.readObject();
        //将内容都写到byte[]数组里
        byte[] bytes = new byte[fip.available()];
        fip.read(bytes);
        /**
         *     神之一手 ———— 郝为高
         *     由于每次字符串的连接,都会先创建 StringBuilder builder对象,浪费大量时间,不如直接一步到位,先创建出来
         */
        //存储编码后的0/1串
        String copCode = "";
        StringBuilder codeBulider = new StringBuilder();
        for (int i = 1; i < bytes.length; i++) {
            //将byte转换为字符串
            for (int j = 7; j >= 0 ; j--) {
                int num = bytes[i];
                if(((num >> j) & 1) == 0){
                    codeBulider.append('0');
                }
                else{
                    codeBulider.append('1');
                }
            }
        }
        copCode = codeBulider.toString();
        //删除补足‘0’字符串
        copCode = copCode.substring(0,copCode.length() - bytes[0] - 1);
        //为了提高效率的到str的char[]数组
        char[] chars = copCode.toCharArray();
        //得到全体字符
        String content = "";
        /**
         *     神之一手 ———— 郝为高
         *     由于每次字符串的连接,都会先创建 StringBuilder builder对象,浪费大量时间,不如直接一步到位,先创建出来
         */
        StringBuilder sinBuilder = new StringBuilder();
        StringBuilder contentBuilder = new StringBuilder();
        for (char cc:chars) {
            sinBuilder.append(cc);
            if(readMap.containsKey(sinBuilder.toString())){
                contentBuilder.append(readMap.get(sinBuilder.toString()));
                sinBuilder.delete(0, sinBuilder.length());
            }
        }
        content = contentBuilder.toString();
        FileOutputStream fop = new FileOutputStream(after);
        fop.write(content.getBytes());
    }
}

추천

출처blog.csdn.net/AkinanCZ/article/details/127873236