Compressione dei dati con l'algoritmo di Huffman

Introduzione

In questo articolo parlerò del noto algoritmo di Huffman e del suo utilizzo nella compressione dei dati.

Di conseguenza scriveremo un semplice archiver. Questo è già stato articolo su Habr, ma senza implementazione pratica. Il materiale teorico di questo post è tratto dalle lezioni di informatica scolastica e dal libro di Robert Lafore "Data Structures and Algorithms in Java". Dunque, tutto sotto il tag!

Alcuni pensieri

In un file di testo normale, un carattere è codificato in 8 bit (codifica ASCII) o 16 (codifica Unicode). In seguito considereremo la codifica ASCII. Prendiamo ad esempio la stringa s1 = "SUSIE SAYS IT IS EASYn". In totale ci sono 22 caratteri nella stringa, inclusi naturalmente gli spazi e il simbolo di nuova riga - 'n'. Un file contenente questa stringa peserà 22*8 = 176 bit. Subito sorge la domanda: è razionale utilizzare tutti e 8 i bit per codificare 1 carattere? Non usiamo tutti i simboli della codifica ASCII. Anche se li usassimo, sarebbe più razionale dare alla lettera più comune - S - il codice più corto possibile, mentre per la lettera più rara - T (o U o 'n') - un codice più lungo. Questo è ciò che fa l'algoritmo di Huffman: trovare l'opzione di codifica ottimale in cui il file abbia il peso minimo. È perfettamente normale che i diversi caratteri abbiano lunghezze di codice diverse: è su questo che si basa l'algoritmo.

Codifica

Perché non dare al carattere 'S' un codice, ad esempio lungo 1 bit: 0 o 1. Facciamo che sia 1. Allora daremo 0 al secondo carattere più comune - ' ' (spazio). Immagina di aver iniziato a decodificare il tuo messaggio - la stringa codificata s1 - e noti che il codice inizia con 1. Quindi, cosa fare: è il simbolo S, o è un altro simbolo, ad esempio A? Pertanto, sorge una regola importante:

Nessun codice deve essere un prefisso di un altro

Questa regola è fondamentale nell'algoritmo. Pertanto, la creazione del codice inizia con una tabella delle frequenze, in cui è indicata la frequenza (il numero di occorrenze) di ciascun simbolo:

Compressione dei dati con l'algoritmo di Huffman I simboli con il numero maggiore di occorrenze devono essere codificati con il minor numero possibile di bit. Ecco un esempio di una delle possibili tabelle dei codici:

Compressione dei dati con l'algoritmo di Huffman Pertanto, il messaggio codificato apparirà così:

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

Ho separato il codice di ciascun simbolo con uno spazio. In un file compresso reale non sarà così!
Sorge la domanda: come ha fatto questo novellino a inventare il codice per creare una tabella di codici? Di questo si parlerà più avanti.

Costruzione dell'albero di Huffman

Qui entrano in gioco gli alberi binari di ricerca. Non preoccupatevi, qui non saranno necessari metodi di ricerca, inserimento e cancellazione. Ecco la struttura dell'albero in java:

public class Node {
    private int frequenza;
    private char lettera;
    private Node figlioSinistro;
    private Node figlioDestro;
    ...
}

class BinaryTree {
    private Node radice;

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

Questo non è il codice completo, il codice completo verrà fornito più in basso.

Ecco l'algoritmo per costruire l'albero:

  1. Creare un oggetto Node per ogni simbolo del messaggio (stringa s1). In questo caso avremo 9 nodi (oggetti Node). Ogni nodo è composto da due campi dati: simbolo e frequenza.
  2. Creare un oggetto Albero (BinaryTree) per ciascuno dei nodi Node. Il nodo diventa la radice dell'albero.
  3. Inserire questi alberi in una coda di priorità. Maggiore è la frequenza, minore è la priorità. In questo modo, l'albero con la frequenza più bassa viene sempre estratto.

Successivamente, è necessario eseguire ciclicamente quanto segue:

  1. Estrarre due alberi dalla coda di priorità e fare di essi i figli di un nuovo nodo (il nodo appena creato senza lettera). La frequenza del nuovo nodo è la somma delle frequenze dei due alberi figli.
  2. Per questo nodo, creare un albero con la radice in questo nodo. Inserire di nuovo questo albero nella coda di priorità. (Poiché l'albero ha una nuova frequenza, probabilmente si sposterà in una nuova posizione nella coda.)
  3. Continuare a seguire i passi 1 e 2 finché nella coda non rimarrà un solo albero: l'albero di Huffman.

Consideriamo questo algoritmo sulla stringa s1:

Compressione dei dati con l'algoritmo di Huffman

Qui il simbolo «lf» (linefeed) indica un cambio di riga, «sp» (space) è uno spazio.

E poi?

Abbiamo ottenuto l'albero di Huffman. Bene. E adesso cosa facciamo? Neppure gratuitamente lo prenderanno! Inoltre, è necessario tracciare tutti i possibili percorsi dalla radice alle foglie dell'albero. Conveniamo di designare un arco 0 se conduce a un figlio sinistro e 1 se a un destro. Strettamente parlando, in queste designazioni, il codice del simbolo è il percorso dalla radice dell'albero alla foglia che contiene quel simbolo.

Compressione dei dati con l'algoritmo di Huffman

In questo modo si è ottenuta la tabella dei codici. Notiamo che se esaminiamo questa tabella, possiamo trarre delle conclusioni sul «peso» di ogni simbolo: è la lunghezza del suo codice. Quindi, in forma compressa, il file originale peserà: 2 * 3 + 2 * 4 + 3 * 3 + 6 * 2 + 1 * 4 + 1 * 5 + 2 * 4 + 4 * 2 + 1 * 5 = 65 bit. Inizialmente pesava 176 bit. Pertanto, lo abbiamo ridotto di ben 176/65 = 2,7 volte! Ma questa è utopia. È difficile ottenere tale coefficiente. Perché? Di questo parleremo più avanti.

Decodifica

Beh, forse è rimasto l'aspetto più semplice: la decodifica. Penso che molti di voi abbiano intuito che non è possibile creare un file compresso senza alcun indizio su come sia stato codificato: non saremo in grado di decodificarlo! Sì, è stato difficile accettarlo, ma sarà necessario creare un file di testo table.txt con la tabella di compressione:

01110
 00
A010
E1111
I110
S10
T0110
U01111
Y1110

Registrazione della tabella nella forma ‘simbolo’«codice simbolo». Perché 01110 senza simbolo? In realtà ha un simbolo, solo che gli strumenti Java che utilizzo per l'output nel file convertono il carattere di nuova riga — ‘n’ - in un vero e proprio ritorno a capo (per quanto stupido possa sembrare). Quindi la riga vuota sopra è il simbolo per il codice 01110. Per il codice 00, il simbolo è uno spazio all'inizio della riga. Dico subito che il nostro coefficiente rischia di tramontare: questo modo di memorizzare la tabella può essere considerato il meno razionale. Ma è semplice da comprendere e da realizzare. Sarò felice di ascoltare i vostri suggerimenti nei commenti riguardo all'ottimizzazione.

Avendo questa tabella, è molto semplice decodificare. Ricordiamo quale regola abbiamo seguito nella creazione della codifica:

Nessun codice dovrebbe essere un prefisso di un altro.

Ed è qui che giocano un ruolo semplificatore. Leggiamo bit per bit, e non appena la stringa d ottenuta, composta dai bit letti, corrisponde alla codifica associata al simbolo character, sappiamo subito che è stato codificato il simbolo character (e solo lui!). Poi registriamo character nella stringa di decodifica (la stringa che contiene il messaggio decodificato), azzeriamo la stringa d e continuiamo a leggere il file codificato.

Implementazione

È tempo di abbattere il mio codice e scrivere un compressore. Chiamerò questo compressore Compressor.

Iniziamo dall'inizio. Prima di tutto, scriviamo la classe Node:

public class Node {
    private int frequenza; // frequenza
    private char lettera; // lettera
    private Node figlioSinistro; // figlio sinistro
    private Node figlioDestro; // figlio destro

    public Node(char lettera, int frequenza) { // costruttore
        this.lettera = lettera;
        this.frequenza = frequenza;
    }

    public Node() {} // sovraccarico del costruttore per nodi senza nome (vedi sopra nella sezione sulla costruzione dell'albero di Huffman)
    public void aggiungiFiglio(Node nuovoNodo) { // aggiungere un figlio
        if (figlioSinistro == null) // se sinistro è vuoto => destro è anche vuoto => aggiungi a sinistro
            figlioSinistro = nuovoNodo;
        else {
            if (figlioSinistro.getFrequenza() <= nuovoNodo.getFrequenza()) // in generale, figlio sinistro
                figlioDestro = nuovoNodo; // diventerà quello con frequenza minore
            else {
                figlioDestro = figlioSinistro;
                figlioSinistro = nuovoNodo;
            }
        }

        frequenza += nuovoNodo.getFrequenza(); // frequenza totale
    }

    public Node getFiglioSinistro() {
        return figlioSinistro;
    }

    public Node getFiglioDestro() {
        return figlioDestro;
    }

    public int getFrequenza() {
        return frequenza;
    }

    public char getLettera() {
        return lettera;
    }

    public boolean isFoglia() { // controllo se è una foglia
        return figlioSinistro == null && figlioDestro == null;
    }
}

Ora l'alberello:

class BinaryTree {
    private Node radice;

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

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

    public int getFrequenza() {
        return radice.getFrequenza();
    }

    public Node getRadice() {
        return radice;
    }
}

Coda di priorità:

import java.util.ArrayList; // sì, sì, la coda sarà basata su una lista

class PriorityQueue {
    private ArrayList dati; // lista della coda
    private int nElem; // numero di elementi nella coda

    public PriorityQueue() {
        dati = new ArrayList();
        nElem = 0;
    }

    public void inserisci(BinaryTree nuovoAlbero) { // inserimento
        if (nElem == 0)
            dati.add(nuovoAlbero);
        else {
            for (int i = 0; i  nuovoAlbero.getFrequenza()) { // se la frequenza dell'albero inserito è minore
                    dati.add(i, nuovoAlbero); // spostiamo tutti gli alberi nelle posizioni a destra di 1 cella
                    break; // poi inseriamo il nuovo albero nella posizione corrente
                }
                if (i == nElem - 1) 
                    dati.add(nuovoAlbero);
            }
        }
        nElem++; // aumentiamo il numero di elementi di 1
    }

    public BinaryTree rimuovi() { // rimozione dalla coda
        BinaryTree tmp = dati.get(0); // copiamo l'elemento da rimuovere
        dati.remove(0); // effettivamente rimuoviamo
        nElem--; // diminuiamo il numero di elementi di 1
        return tmp; // restituiamo l'elemento rimosso (l'elemento con la frequenza minore)
    }
}

Classe che crea un albero di Huffman:

public class HuffmanTree {
    private final byte ENCODING_TABLE_SIZE = 127; // lunghezza della tabella di codifica
    private String myString; // messaggio
    private BinaryTree huffmanTree; // albero di Huffman
    private int[] freqArray; // tabella delle frequenze
    private String[] encodingArray; // tabella di codifica


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

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

        huffmanTree = getHuffmanTree();

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

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

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

    //------------------------creazione dell'albero di Huffman------------------
    private BinaryTree getHuffmanTree() {
        PriorityQueue pq = new PriorityQueue();
        // algoritmo descritto sopra
        for (int i = 0; i < ENCODING_TABLE_SIZE; i++) {
            if (freqArray[i] != 0) { // se il simbolo esiste nella stringa
                Node newNode = new Node((char) i, freqArray[i]); // quindi crea un Node per esso
                BinaryTree newTree = new BinaryTree(newNode); // e crea un BinaryTree per Node
                pq.insert(newTree); // inserire nella coda
            }
        }

        while (true) {
            BinaryTree tree1 = pq.remove(); // estrarre dalla coda il primo albero.

            try {
                BinaryTree tree2 = pq.remove(); // estrarre dalla coda il secondo albero

                Node newNode = new Node(); // creare un nuovo Node
                newNode.addChild(tree1.getRoot()); // fare dei due alberi estratti i suoi figli
                newNode.addChild(tree2.getRoot());

                pq.insert(new BinaryTree(newNode));
            } catch (IndexOutOfBoundsException e) { // rimane un solo albero nella coda
                return tree1;
            }
        }
    }

    public BinaryTree getTree() {
        return huffmanTree;
    }

    //-------------------tabella di codifica------------------
    void fillEncodingArray(Node node, String codeBefore, String direction) { // riempire la tabella di codifica
        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() { // per il debug
        fillEncodingArray(huffmanTree.getRoot(), "", "");

        System.out.println("======================Tabella di codifica====================");
        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;
    }
}

Classe che codifica/decodifica:

public class HuffmanOperator {
    private final byte ENCODING_TABLE_SIZE = 127; //lunghezza della tabella
    private HuffmanTree mainHuffmanTree; //albero di Huffman (utilizzato solo per la compressione)
    private String myString; //messaggio originale
    private int[] freqArray; //tabella delle frequenze
    private String[] encodingArray; //tabella di codifica
    private double ratio; //coefficiente di compressione

    public HuffmanOperator(HuffmanTree MainHuffmanTree) { //per compressione
        this.mainHuffmanTree = MainHuffmanTree;

        myString = mainHuffmanTree.getOriginalString();

        encodingArray = mainHuffmanTree.getEncodingArray();

        freqArray = mainHuffmanTree.getFrequenceArray();
    }

    public HuffmanOperator() {} //per estrarre;

    //---------------------------------------compression-----------------------------------------------------------
    private String getCompressedString() {
        String compressed = "";
        String intermidiate = ""; //stringa intermedia (senza zeri aggiuntivi)
        //System.out.println("=============================Compression=======================");
        //displayEncodingArray();
        for (int i = 0; i 
        //dobbiamo aggiungere zeri alla fine (può essere 1, non fa differenza)
        byte counter = 0; //numero di zeri aggiunti alla fine (un byte è sufficiente: 0<=counter<8<127)
        for (int length = intermidiate.length(), delta = 8 - length % 8; 
                counter < delta; counter++) { //delta - numero di zeri aggiunti
            intermidiate += "0";
        }

        //unire il numero di zeri aggiuntivi nella rappresentazione binaria e la stringa intermedia 
        compressed = String.format("%8s", Integer.toBinaryString(counter & 0xff)).replace(" ", "0") + intermidiate;
        
        //coefficiente idealizzato
        setCompressionRatio();
        //System.out.println("===============================================================");
        return compressed;
    }
    
    private void setCompressionRatio() { //calcolare il coefficiente idealizzato 
        double sumA = 0, sumB = 0; //A - il totale originale
        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() { //compressone finale
        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();
        //ottenere il numero di zeri aggiunti
        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, poiché il primo byte rappresenta il numero di zeri aggiunti
            current += compressed.charAt(i);
            for (int j = 0; j < ENCODING_TABLE_SIZE; j++) {
                if (current.equals(encodingArray[j])) { //se corrisponde
                    decompressed += (char)j; //allora aggiungiamo l'elemento
                    current = ""; //e azzeriamo la stringa corrente
                }
            }
        }

        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() { //per debug
        System.out.println("======================Tabella di codifica====================");
        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("========================================================");
    }
}

Classe che semplifica la scrittura su file:

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("Percorso non valido o file inesistente!");
    	}
    }

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

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

Classe che semplifica la lettura da file:

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) // se il file è finito
    		throw new EOFException();
    	return (byte)cur;
    }
    
    public String readLine() throws IOException {
    	return fileBufferedReader.readLine();
    }
    
    @Override
    public void close() throws IOException{
    	fileInputStream.close();
    }
}

E, infine, la classe principale:

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 { // Specifica l'istruzione tramite argomenti da riga di comando
            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("Formato di input degli argomenti non valido ");
            System.out.println("Leggi Readme.txt");
            e.printStackTrace();
        }
    }

	public static void compress(String stringPath) throws IOException {
        List 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("Percorso non valido o file non esistente!");
            return;
        } catch (MalformedInputException e) {
        	System.out.println("Il formato di codifica del file non è supportato");
        	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());
        }
        // crea file con tabella di codifica:
        
        table = new File(inputFile.getAbsolutePath() + ".table.txt");
        table.createNewFile();
        try (FileOutputHelper fo = new FileOutputHelper(table)) {
        	fo.writeString(operator.getEncodingTable());
        }
        
        System.out.println("Percorso del file compresso: " + compressedFile.getAbsolutePath());
        System.out.println("Percorso della tabella di codifica " + table.getAbsolutePath());
        System.out.println("Senza tabella, il file non può essere estratto!");
        
        double idealRatio = Math.round(operator.getCompressionRatio() * 100) / (double) 100; // rapporto idealizzato
        double realRatio = Math.round((double) inputFile.length() 
        		/ ((double) compressedFile.length() + (double) table.length()) * 100) / (double)100; // rapporto reale
        
        System.out.println("Il rapporto di compressione idealizzato è " + idealRatio);
        System.out.println("Il rapporto di compressione includendo la tabella di codifica è " + 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];
        // leggi il file compresso
        // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! verifica qui:
        try (FileInputHelper fi = new FileInputHelper(compressedFile)) {
        	byte b;
        	while (true) {
        		b = fi.readByte(); // il metodo restituisce EOFException
        		compressed += String.format("%8s", Integer.toBinaryString(b & 0xff)).replace(" ", "0");
        	}
        } catch (EOFException e) {
        	
        }
        
        // --------------------
        
        // leggi la tabella di codifica:
        try (FileInputHelper fi = new FileInputHelper(tableFile)) {
        	fi.readLine(); // salta la prima stringa vuota
        	encodingArray[(byte)'n'] = fi.readLine(); // leggi il codice per '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();
        // estrazione:
		try (FileOutputHelper fo = new FileOutputHelper(extractedFile)) {
			fo.writeString(operator.extract(compressed, encodingArray));
		}
		
		System.out.println("Percorso del file estratto " + extractedFile.getAbsolutePath());
    }
}

Il file con le istruzioni readme.txt dovrete scriverlo voi stessi 🙂

Conclusione

Forse è tutto ciò che volevo dire. Se avete qualcosa da dire riguardo alla mia incompetenza nei miglioramenti del codice, dell'algoritmo o in generale su qualsiasi ottimizzazione, scrivete pure. Se qualcosa non vi è chiaro, scrivete anche quello. Sarò felice di sentirvi nei commenti!

P.S.

Sì-sì, sono ancora qui, infatti non ho dimenticato il coefficiente. Per la stringa s1, la tabella di codifica pesa 48 byte — molto di più del file originale, e non dimentichiamo i zeri aggiuntivi (il numero di zeri aggiunti è 7) => il coefficiente di compressione sarà inferiore a uno: 176/(65 + 48*8 + 7)=0.38. Se anche voi l'avete notato, siete bravi. Sì, questa implementazione sarà estremamente inefficace per file piccoli. Ma cosa succede con file grandi? Le dimensioni del file superano di gran lunga le dimensioni della tabella di codifica. Qui l'algoritmo funziona come dovrebbe! Ad esempio, per il monologo di Faust il comprimente restituisce un coefficiente reale (non idealizzato) pari a 1.46 — quasi un volta e mezza! E sì, si presumeva che il file fosse in inglese.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS - ProHoster