Nén dữ liệu bằng thuật toán Huffman

Nhập

Trong bài viết này tôi sẽ nói về thuật toán Huffman nổi tiếng cũng như ứng dụng của nó trong việc nén dữ liệu.

Kết quả là chúng ta sẽ viết một trình lưu trữ đơn giản. Điều này đã được thảo luận rồi bài viết về Habré, nhưng không có triển khai thực tế. Tài liệu lý thuyết của bài viết hiện tại được lấy từ các bài học khoa học máy tính ở trường và cuốn sách “Cấu trúc dữ liệu và thuật toán trong Java” của Robert Laforet. Vì vậy, mọi thứ đều bị cắt!

Một chút suy tư

Trong tệp văn bản thông thường, một ký tự được mã hóa bằng 8 bit (mã hóa ASCII) hoặc 16 (mã hóa Unicode). Tiếp theo chúng ta sẽ xem xét mã hóa ASCII. Ví dụ: lấy dòng s1 = “SUSIE SAYS IT IS EASYn”. Đương nhiên, có tổng cộng 22 ký tự trong dòng, bao gồm cả dấu cách và ký tự dòng mới - 'n'. File chứa dòng này sẽ nặng 22*8 = 176 bit. Câu hỏi ngay lập tức được đặt ra: việc sử dụng cả 8 bit để mã hóa 1 ký tự có hợp lý không? Chúng tôi không sử dụng tất cả các ký tự ASCII. Ngay cả nếu họ làm vậy, sẽ hợp lý hơn nếu chữ cái phổ biến nhất - S - được cấp mã ngắn nhất có thể và đối với chữ cái hiếm nhất - T (hoặc U, hoặc 'n') - được cấp mã dài hơn. Đây là nội dung của thuật toán Huffman: cần phải tìm tùy chọn mã hóa tối ưu trong đó tệp sẽ có trọng lượng tối thiểu. Điều khá bình thường là độ dài mã sẽ khác nhau đối với các ký hiệu khác nhau - đây là cơ sở của thuật toán.

Mã hóa

Tại sao không đặt mã cho ký tự 'S', ví dụ: dài 1 bit: 0 hoặc 1. Đặt nó là 1. Sau đó, ký tự phổ biến thứ hai - '' (dấu cách) - cho 0. Hãy tưởng tượng bạn bắt đầu giải mã tin nhắn của mình - chuỗi được mã hóa s1 - và bạn thấy mã bắt đầu bằng 1. Vậy, bạn phải làm gì: đây là ký tự S hay một ký tự nào khác, ví dụ A? Do đó, một quy tắc quan trọng được đặt ra:

Không mã nào phải là tiền tố của mã khác

Quy tắc này là chìa khóa trong thuật toán. Do đó, việc tạo mã bắt đầu bằng bảng tần số, cho biết tần suất (số lần xuất hiện) của mỗi ký hiệu:

Nén dữ liệu bằng thuật toán Huffman Các ký tự xuất hiện nhiều nhất sẽ được mã hóa ít nhất có thể số bit. Tôi sẽ đưa ra một ví dụ về một trong các bảng mã có thể có:

Nén dữ liệu bằng thuật toán Huffman Vì vậy, tin nhắn được mã hóa sẽ trông như thế này:

10 01111 10 110 1111 00 10 010 1110 10 00 110 0110 00 110 10 00 1111 010 10 1110 01110

Tôi tách mã của mỗi ký tự bằng một khoảng trắng. Điều này sẽ không xảy ra trong một tệp nén thực sự!
Câu hỏi được đặt ra: làm thế nào mà chàng trai trẻ này lại nghĩ ra được đoạn mã để tạo ra một bảng mã? Điều này sẽ được thảo luận dưới đây.

Xây dựng cây Huffman

Đây là lúc cây tìm kiếm nhị phân ra tay giải cứu. Đừng lo lắng, bạn sẽ không cần các phương pháp tìm kiếm, chèn hoặc xóa ở đây. Đây là cấu trúc cây trong java:

public class Node {
    private int frequence;
    private char letter;
    private Node leftChild;
    private Node rightChild;
    ...
}

class BinaryTree {
    private Node root;

    public BinaryTree() {
        root = new Node();
    }
    public BinaryTree(Node root) {
        this.root = root;
    }
    ...
}

Đây không phải là mã hoàn chỉnh, mã đầy đủ sẽ ở bên dưới.

Đây là thuật toán xây dựng cây:

  1. Tạo một đối tượng Node cho mỗi ký tự trong tin nhắn (dòng s1). Trong trường hợp của chúng tôi sẽ có 9 nút (đối tượng Node). Mỗi nút bao gồm hai trường dữ liệu: ký hiệu và tần số
  2. Tạo một đối tượng Cây (BinaryTree) cho mỗi Nút. Nút trở thành gốc của cây.
  3. Chèn những cây này vào hàng đợi ưu tiên. Tần số càng thấp thì mức độ ưu tiên càng cao. Vì vậy, khi trích xuất, dervo có tần số thấp nhất luôn được chọn.

Tiếp theo, bạn cần thực hiện các thao tác sau theo chu kỳ:

  1. Loại bỏ hai cây khỏi hàng đợi ưu tiên và biến chúng thành con của nút mới (nút mới được tạo không có chữ cái). Tần số của nút mới bằng tổng tần số của hai cây con.
  2. Đối với nút này, hãy tạo một cây có gốc tại nút này. Chèn cây này trở lại hàng ưu tiên. (Vì cây có tần số mới nên rất có thể nó sẽ xuất hiện ở vị trí mới trong hàng đợi)
  3. Tiếp tục bước 1 và 2 cho đến khi chỉ còn một cây trong hàng đợi - cây Huffman

Hãy xem xét thuật toán này trên dòng s1:

Nén dữ liệu bằng thuật toán Huffman

Ở đây ký hiệu “lf” (linefeed) có nghĩa là dòng mới, “sp” (dấu cách) là khoảng trắng.

Tiếp theo là gì?

Chúng tôi có một cây Huffman. ĐƯỢC RỒI. Và phải làm gì với nó? Họ thậm chí sẽ không lấy nó miễn phí, và sau đó, bạn cần phải tìm ra tất cả các đường đi có thể từ gốc đến lá của cây. Hãy đồng ý ký hiệu cạnh 0 nếu nó dẫn đến con bên trái và 1 nếu nó dẫn đến con bên phải. Nói đúng ra, trong ký hiệu này, mã của ký hiệu là đường đi từ gốc cây đến lá chứa chính ký hiệu này.

Nén dữ liệu bằng thuật toán Huffman

Đây là cách bảng mã hóa ra. Lưu ý rằng nếu xem xét bảng này, chúng ta có thể kết luận về “trọng lượng” của mỗi ký hiệu - đây là độ dài mã của nó. Khi đó, ở dạng nén, file gốc sẽ có trọng lượng: 2 * 3 + 2 * 4 + 3 * 3 + 6 * 2 + 1 * 4 + 1 * 5 + 2 * 4 + 4 * 2 + 1 * 5 = 65 bit . Lúc đầu nó nặng 176 bit. Do đó, chúng tôi đã giảm nó tới 176/65 = 2.7 lần! Nhưng đây là một điều không tưởng. Một hệ số như vậy khó có thể đạt được. Tại sao? Điều này sẽ được thảo luận sau một chút.

Giải mã

Chà, có lẽ việc đơn giản nhất còn lại là giải mã. Tôi nghĩ nhiều bạn đã đoán rằng chúng ta không thể đơn giản tạo một tệp nén mà không có bất kỳ gợi ý nào về cách nó được mã hóa - chúng ta sẽ không thể giải mã nó! Vâng, vâng, thật khó để tôi nhận ra điều này, nhưng tôi sẽ phải tạo một tệp văn bản table.txt với bảng nén:

01110
 00
A010
E1111
I110
S10
T0110
U01111
Y1110

Mục nhập bảng ở dạng 'ký hiệu' 'mã ký tự'. Tại sao 01110 không có ký hiệu? Trên thực tế, đó là bằng một ký hiệu, chỉ là các công cụ java tôi sử dụng khi xuất ra tệp, ký tự dòng mới - 'n' - được chuyển đổi thành dòng mới (dù nghe có vẻ ngu ngốc đến mức nào). Do đó, dòng trống ở trên cùng là ký tự của mã 01110. Đối với mã 00, ký tự là khoảng trắng ở đầu dòng. Tôi sẽ nói ngay rằng đối với hệ số Khan của chúng tôi, phương pháp lưu trữ bảng này có thể được coi là phi lý nhất. Nhưng nó rất dễ hiểu và thực hiện. Tôi sẽ rất vui khi nghe đề xuất của bạn trong phần nhận xét về việc tối ưu hóa.

Có bảng này làm cho nó rất dễ dàng để giải mã. Chúng ta hãy nhớ quy tắc chúng ta đã tuân theo khi tạo mã hóa:

Không mã nào phải là tiền tố của mã khác

Đây là nơi nó đóng một vai trò hỗ trợ. Chúng tôi đọc tuần tự từng chút một và ngay khi chuỗi kết quả d, bao gồm các bit đọc, khớp với mã hóa tương ứng với ký tự ký tự, chúng tôi biết ngay rằng ký tự ký tự đó (và chỉ nó!) đã được mã hóa. Tiếp theo, ta ghi ký tự vào dòng giải mã (dòng chứa thông báo đã giải mã), reset dòng d rồi đọc file mã hóa.

Thực hiện

Đã đến lúc hạ nhục mã của tôi và viết một trình lưu trữ. Hãy gọi nó là Máy nén.

Bắt đầu lại. Trước hết, chúng ta viết lớp Node:

public class Node {
    private int frequence;//частота
    private char letter;//буква
    private Node leftChild;//левый потомок
    private Node rightChild;//правый потомок

   

    public Node(char letter, int frequence) { //собственно, конструктор
        this.letter = letter;
        this.frequence = frequence;
    }

    public Node() {}//перегрузка конструтора для безымянных узлов(см. выше в разделе о построении дерева Хаффмана)
    public void addChild(Node newNode) {//добавить потомка
        if (leftChild == null)//если левый пустой=> правый тоже=> добавляем в левый
            leftChild = newNode;
        else {
            if (leftChild.getFrequence() <= newNode.getFrequence()) //в общем, левым потомком
                rightChild = newNode;//станет тот, у кого меньше частота
            else {
                rightChild = leftChild;
                leftChild = newNode;
            }
        }

        frequence += newNode.getFrequence();//итоговая частота
    }

    public Node getLeftChild() {
        return leftChild;
    }

    public Node getRightChild() {
        return rightChild;
    }

    public int getFrequence() {
        return frequence;
    }

    public char getLetter() {
        return letter;
    }

    public boolean isLeaf() {//проверка на лист
        return leftChild == null && rightChild == null;
    }
}

Bây giờ là cây:

class BinaryTree {
    private Node root;

    public BinaryTree() {
        root = new Node();
    }

    public BinaryTree(Node root) {
        this.root = root;
    }

    public int getFrequence() {
        return root.getFrequence();
    }

    public Node getRoot() {
        return root;
    }
}

Hàng đợi ưu tiên:

import java.util.ArrayList;//да-да, очередь будет на базе списка

class PriorityQueue {
    private ArrayList<BinaryTree> data;//список очереди
    private int nElems;//кол-во элементов в очереди

    public PriorityQueue() {
        data = new ArrayList<BinaryTree>();
        nElems = 0;
    }

    public void insert(BinaryTree newTree) {//вставка
        if (nElems == 0)
            data.add(newTree);
        else {
            for (int i = 0; i < nElems; i++) {
                if (data.get(i).getFrequence() > newTree.getFrequence()) {//если частота вставляемого дерева меньше 
                    data.add(i, newTree);//чем част. текущего, то cдвигаем все деревья на позициях справа на 1 ячейку                   
                    break;//затем ставим новое дерево на позицию текущего
                }
                if (i == nElems - 1) 
                    data.add(newTree);
            }
        }
        nElems++;//увеличиваем кол-во элементов на 1
    }

    public BinaryTree remove() {//удаление из очереди
        BinaryTree tmp = data.get(0);//копируем удаляемый элемент
        data.remove(0);//собственно, удаляем
        nElems--;//уменьшаем кол-во элементов на 1
        return tmp;//возвращаем удаленный элемент(элемент с наименьшей частотой)
    }
}

Lớp tạo cây Huffman:

public class HuffmanTree {
    private final byte ENCODING_TABLE_SIZE = 127;//длина кодировочной таблицы
    private String myString;//сообщение
    private BinaryTree huffmanTree;//дерево Хаффмана
    private int[] freqArray;//частотная таблица
    private String[] encodingArray;//кодировочная таблица


    //----------------constructor----------------------
    public HuffmanTree(String newString) {
        myString = newString;

        freqArray = new int[ENCODING_TABLE_SIZE];
        fillFrequenceArray();

        huffmanTree = getHuffmanTree();

        encodingArray = new String[ENCODING_TABLE_SIZE];
        fillEncodingArray(huffmanTree.getRoot(), "", "");
    }

    //--------------------frequence array------------------------
    private void fillFrequenceArray() {
        for (int i = 0; i < myString.length(); i++) {
            freqArray[(int)myString.charAt(i)]++;
        }
    }

    public int[] getFrequenceArray() {
        return freqArray;
    }

    //------------------------huffman tree creation------------------
    private BinaryTree getHuffmanTree() {
        PriorityQueue pq = new PriorityQueue();
        //алгоритм описан выше
        for (int i = 0; i < ENCODING_TABLE_SIZE; i++) {
            if (freqArray[i] != 0) {//если символ существует в строке
                Node newNode = new Node((char) i, freqArray[i]);//то создать для него Node
                BinaryTree newTree = new BinaryTree(newNode);//а для Node создать BinaryTree
                pq.insert(newTree);//вставить в очередь
            }
        }

        while (true) {
            BinaryTree tree1 = pq.remove();//извлечь из очереди первое дерево.

            try {
                BinaryTree tree2 = pq.remove();//извлечь из очереди второе дерево

                Node newNode = new Node();//создать новый Node
                newNode.addChild(tree1.getRoot());//сделать его потомками два извлеченных дерева
                newNode.addChild(tree2.getRoot());

                pq.insert(new BinaryTree(newNode);
            } catch (IndexOutOfBoundsException e) {//осталось одно дерево в очереди
                return tree1;
            }
        }
    }

    public BinaryTree getTree() {
        return huffmanTree;
    }

    //-------------------encoding array------------------
    void fillEncodingArray(Node node, String codeBefore, String direction) {//заполнить кодировочную таблицу
        if (node.isLeaf()) {
            encodingArray[(int)node.getLetter()] = codeBefore + direction;
        } else {
            fillEncodingArray(node.getLeftChild(), codeBefore + direction, "0");
            fillEncodingArray(node.getRightChild(), codeBefore + direction, "1");
        }
    }

    String[] getEncodingArray() {
        return encodingArray;
    }

    public void displayEncodingArray() {//для отладки
        fillEncodingArray(huffmanTree.getRoot(), "", "");

        System.out.println("======================Encoding table====================");
        for (int i = 0; i < ENCODING_TABLE_SIZE; i++) {
            if (freqArray[i] != 0) {
                System.out.print((char)i + " ");
                System.out.println(encodingArray[i]);
            }
        }
        System.out.println("========================================================");
    }
    //-----------------------------------------------------
    String getOriginalString() {
        return myString;
    }
}

Lớp chứa mã hóa/giải mã:

public class HuffmanOperator {
    private final byte ENCODING_TABLE_SIZE = 127;//длина таблицы
    private HuffmanTree mainHuffmanTree;//дерево Хаффмана (используется только для сжатия)
    private String myString;//исходное сообщение
    private int[] freqArray;//частотаная таблица
    private String[] encodingArray;//кодировочная таблица
    private double ratio;//коэффициент сжатия 


    public HuffmanOperator(HuffmanTree MainHuffmanTree) {//for compress
        this.mainHuffmanTree = MainHuffmanTree;

        myString = mainHuffmanTree.getOriginalString();

        encodingArray = mainHuffmanTree.getEncodingArray();

        freqArray = mainHuffmanTree.getFrequenceArray();
    }

    public HuffmanOperator() {}//for extract;

    //---------------------------------------compression-----------------------------------------------------------
    private String getCompressedString() {
        String compressed = "";
        String intermidiate = "";//промежуточная строка(без добавочных нулей)
        //System.out.println("=============================Compression=======================");
        //displayEncodingArray();
        for (int i = 0; i < myString.length(); i++) {
            intermidiate += encodingArray[myString.charAt(i)];
        }
        //Мы не можем писать бит в файл. Поэтому нужно сделать длину сообщения кратной 8=>
        //нужно добавить нули в конец(можно 1, нет разницы)
        byte counter = 0;//количество добавленных в конец нулей (байта в полне хватит: 0<=counter<8<127)
        for (int length = intermidiate.length(), delta = 8 - length % 8; 
        		counter < delta ; counter++) {//delta - количество добавленных нулей
            intermidiate += "0";
        }
        
        //склеить кол-во добавочных нулей в бинарном предаствлении и промежуточную строку 
        compressed = String.format("%8s", Integer.toBinaryString(counter & 0xff)).replace(" ", "0") + intermidiate;
        		
        //идеализированный коэффициент
        setCompressionRatio();
        //System.out.println("===============================================================");
        return compressed;
    }
    
    private void setCompressionRatio() {//посчитать идеализированный коэффициент 
        double sumA = 0, sumB = 0;//A-the original sum
        for (int i = 0; i < ENCODING_TABLE_SIZE; i++) {
            if (freqArray[i] != 0) {
                sumA += 8 * freqArray[i];
                sumB += encodingArray[i].length() * freqArray[i];
            }
        }
        ratio = sumA / sumB;
    }

    public byte[] getBytedMsg() {//final compression
        StringBuilder compressedString = new StringBuilder(getCompressedString());
        byte[] compressedBytes = new byte[compressedString.length() / 8];
        for (int i = 0; i < compressedBytes.length; i++) {
                compressedBytes[i] = (byte) Integer.parseInt(compressedString.substring(i * 8, (i + 1) * 8), 2);
        }
        return compressedBytes;
    }
    //---------------------------------------end of compression----------------------------------------------------------------
    //------------------------------------------------------------extract-----------------------------------------------------
    public String extract(String compressed, String[] newEncodingArray) {
        String decompressed = "";
        String current = "";
        String delta = "";
        encodingArray = newEncodingArray;
        
        //displayEncodingArray();
        //получить кол-во вставленных нулей
        for (int i = 0; i < 8; i++) 
        	delta += compressed.charAt(i);
        int ADDED_ZEROES = Integer.parseInt(delta, 2);
       
        for (int i = 8, l = compressed.length() - ADDED_ZEROES; i < l; i++) {
            //i = 8, т.к. первым байтом у нас идет кол-во вставленных нулей
            current += compressed.charAt(i);
            for (int j = 0; j < ENCODING_TABLE_SIZE; j++) {
                if (current.equals(encodingArray[j])) {//если совпало
                    decompressed += (char)j;//то добавляем элемент
                    current = "";//и обнуляем текущую строку
                }
            }
        }

        return decompressed;
    }

    public String getEncodingTable() {
        String enc = "";
    	for (int i = 0; i < encodingArray.length; i++) {
        	if (freqArray[i] != 0) 
        		enc += (char)i + encodingArray[i] + 'n';
        }
    	return enc;
    }

    public double getCompressionRatio() {
        return ratio;
    }


    public void displayEncodingArray() {//для отладки
        System.out.println("======================Encoding table====================");
        for (int i = 0; i < ENCODING_TABLE_SIZE; i++) {
            //if (freqArray[i] != 0) {
                System.out.print((char)i + " ");
                System.out.println(encodingArray[i]);
            //}
        }
        System.out.println("========================================================");
    }
    }

Một lớp giúp ghi vào tệp dễ dàng hơn:

import java.io.File;
import java.io.PrintWriter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Closeable;

public class FileOutputHelper implements Closeable {
    private File outputFile;
    private FileOutputStream fileOutputStream;

    public FileOutputHelper(File file) throws FileNotFoundException {
        outputFile = file;
        fileOutputStream = new FileOutputStream(outputFile);
    }

    public void writeByte(byte msg) throws IOException {
        fileOutputStream.write(msg);
    }

    public void writeBytes(byte[] msg) throws IOException {
        fileOutputStream.write(msg);
    }

    public void writeString(String msg) {
    	try (PrintWriter pw = new PrintWriter(outputFile)) {
    		pw.write(msg);
    	} catch (FileNotFoundException e) {
    		System.out.println("Неверный путь, или такого файла не существует!");
    	}
    }

    @Override
    public void close() throws IOException {
        fileOutputStream.close();
    }

    public void finalize() throws IOException {
        close();
    }
}

Một lớp giúp đọc từ một tệp dễ dàng hơn:

import java.io.FileInputStream;
import java.io.EOFException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;

public class FileInputHelper implements Closeable {
	private FileInputStream fileInputStream;
	private BufferedReader fileBufferedReader;
	
	public FileInputHelper(File file) throws IOException {
		fileInputStream = new FileInputStream(file);
		fileBufferedReader = new BufferedReader(new InputStreamReader(fileInputStream));
	}
	
	
    public byte readByte() throws IOException {
    	int cur = fileInputStream.read();
    	if (cur == -1)//если закончился файл
    		throw new EOFException();
    	return (byte)cur;
    }
    
    public String readLine() throws IOException {
    	return fileBufferedReader.readLine();
    }
    
    @Override
    public void close() throws IOException{
    	fileInputStream.close();
    }
}

Vâng, và lớp chính:

import java.io.File;
import java.nio.charset.MalformedInputException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Paths;
import java.util.List;
import java.io.EOFException;
public class Main {
	private static final byte ENCODING_TABLE_SIZE = 127;
	
    public static void main(String[] args) throws IOException {
        try {//указываем инструкцию с помощью аргументов командной строки
            if (args[0].equals("--compress") || args[0].equals("-c"))
                compress(args[1]);
            else if ((args[0].equals("--extract") || args[0].equals("-x"))
            		&& (args[2].equals("--table") || args[2].equals("-t"))) {
            	extract(args[1], args[3]);
            }
            else
                throw new IllegalArgumentException();
        } catch (ArrayIndexOutOfBoundsException | IllegalArgumentException e) {
            System.out.println("Неверный формат ввода аргументов ");
            System.out.println("Читайте Readme.txt");
            e.printStackTrace();
        }
    }

	public static void compress(String stringPath) throws IOException {
        List<String> stringList;
        File inputFile = new File(stringPath);
        String s = "";
        File compressedFile, table;
        
        try {
            stringList = Files.readAllLines(Paths.get(inputFile.getAbsolutePath()));
        } catch (NoSuchFileException e) {
            System.out.println("Неверный путь, или такого файла не существует!");
            return;
        } catch (MalformedInputException e) {
        	System.out.println("Текущая кодировка файла не поддерживается");
        	return;
        }

        for (String item : stringList) {
            s += item;
            s += 'n';
        }

        HuffmanOperator operator = new HuffmanOperator(new HuffmanTree(s));

        compressedFile = new File(inputFile.getAbsolutePath() + ".cpr");
        compressedFile.createNewFile();
        try (FileOutputHelper fo = new FileOutputHelper(compressedFile)) {
        	fo.writeBytes(operator.getBytedMsg());
        }
        //create file with encoding table:
        
        table = new File(inputFile.getAbsolutePath() + ".table.txt");
        table.createNewFile();
        try (FileOutputHelper fo = new FileOutputHelper(table)) {
        	fo.writeString(operator.getEncodingTable());
        }
        
        System.out.println("Путь к сжатому файлу: " + compressedFile.getAbsolutePath());
        System.out.println("Путь к кодировочной таблице " + table.getAbsolutePath());
        System.out.println("Без таблицы файл будет невозможно извлечь!");
        
        double idealRatio = Math.round(operator.getCompressionRatio() * 100) / (double) 100;//идеализированный коэффициент
        double realRatio = Math.round((double) inputFile.length() 
        		/ ((double) compressedFile.length() + (double) table.length()) * 100) / (double)100;//настоящий коэффициент
        
        System.out.println("Идеализированный коэффициент сжатия равен " + idealRatio);
        System.out.println("Коэффициент сжатия с учетом кодировочной таблицы " + realRatio);
    }

    public static void extract(String filePath, String tablePath) throws FileNotFoundException, IOException {
        HuffmanOperator operator = new HuffmanOperator();
        File compressedFile = new File(filePath),
        	 tableFile = new File(tablePath),
        	 extractedFile = new File(filePath + ".xtr");
        String compressed = "";
        String[] encodingArray = new String[ENCODING_TABLE_SIZE];
        //read compressed file
        //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!check here:
        try (FileInputHelper fi = new FileInputHelper(compressedFile)) {
        	byte b;
        	while (true) {
        		b = fi.readByte();//method returns EOFException
        		compressed += String.format("%8s", Integer.toBinaryString(b & 0xff)).replace(" ", "0");
        	}
        } catch (EOFException e) {
        	
        }
        
        //--------------------
        
        //read encoding table:
        try (FileInputHelper fi = new FileInputHelper(tableFile)) {
        	fi.readLine();//skip first empty string
        	encodingArray[(byte)'n'] = fi.readLine();//read code for 'n'
        	while (true) {
        		String s = fi.readLine();
        		if (s == null)
        			throw new EOFException();
        		encodingArray[(byte)s.charAt(0)] = s.substring(1, s.length());        		
        	}
        } catch (EOFException ignore) {}
        
        extractedFile.createNewFile();
        //extract:
		try (FileOutputHelper fo = new FileOutputHelper(extractedFile)) {
			fo.writeString(operator.extract(compressed, encodingArray));
		}
		
		System.out.println("Путь к распакованному файлу " + extractedFile.getAbsolutePath());
    }
}

Bạn sẽ phải tự viết tệp readme.txt :)

Kết luận

Tôi đoán đó là tất cả những gì tôi muốn nói. Nếu bạn có bất cứ điều gì muốn nói về sự kém cỏi của tôi trong việc cải thiện mã, thuật toán hoặc bất kỳ tối ưu hóa nào nói chung, thì cứ thoải mái viết. Nếu tôi chưa giải thích bất cứ điều gì, xin vui lòng viết. Tôi muốn nghe ý kiến ​​từ bạn trong phần bình luận!

PS

Vâng, vâng, tôi vẫn ở đây vì tôi không quên hệ số. Đối với chuỗi s1, bảng mã hóa nặng 48 byte - lớn hơn nhiều so với tệp nguồn và chúng tôi không quên các số 7 bổ sung (số số 176 được thêm vào là 65) => tỷ lệ nén sẽ nhỏ hơn một: 48/ (8 + 7*0.38 + XNUMX) = XNUMX. Nếu bạn cũng nhận thấy điều này thì không chỉ khuôn mặt của bạn mới tuyệt vời. Có, việc triển khai này sẽ cực kỳ kém hiệu quả đối với các tệp nhỏ. Nhưng điều gì xảy ra với các tập tin lớn? Kích thước tệp lớn hơn nhiều so với kích thước của bảng mã hóa. Đây là nơi thuật toán hoạt động như bình thường! Ví dụ, đối với Lời độc thoại của Faust Trình lưu trữ tạo ra hệ số thực (không lý tưởng hóa) là 1.46 - gần gấp rưỡi! Và vâng, tập tin lẽ ra phải bằng tiếng Anh.

Nguồn: www.habr.com

Thêm một lời nhận xét