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, realizzeremo un semplice arquiviatore. Ne ho già parlato articolo su Habré, ma senza una realizzazione pratica. Il materiale teorico di questo post proviene dalle lezioni scolastiche di informatica e dal libro di Robert Lafore "Data Structures and Algorithms in Java". Quindi, tutto sotto!

Alcune riflessioni

In un normale file di testo, 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». Ci sono in totale 22 caratteri nella stringa, ovviamente inclusi spazi e il carattere di ritorno a capo — ‘n’. Il file contenente questa stringa peserà 22*8 = 176 bit. Subito si pone la domanda: è razionale utilizzare tutti e 8 i bit per codificare 1 carattere? Non utilizziamo tutti i caratteri della codifica ASCII. Anche se lo facessimo, sarebbe più razionale dare alla lettera più frequente — S — il codice più corto possibile, mentre alla lettera meno frequente — T (o U, o ‘n’) — un codice più lungo. Questo è il principio dell'algoritmo di Huffman: trovare l'opzione ottimale di codifica in cui il file avrà il peso minimo. È del tutto normale che la lunghezza del codice per diversi caratteri vari — è proprio su questo che si basa l'algoritmo.

Codifica

Perché non dare al carattere ‘S’ un codice, ad esempio, di 1 bit: 0 o 1. Supponiamo che sia 1. Allora daremo a ‘ ‘(spazio), il secondo carattere più frequente, 0. Immagina di aver iniziato a decodificare il tuo messaggio — la stringa codificata s1 — e vedi che il codice inizia con 1. Allora, cosa devi fare: è il carattere S, o è un altro carattere, come A? Pertanto, sorge una regola importante:

Nessun codice deve essere un prefisso di un altro

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

Compressione dei dati con l'algoritmo di Huffman I caratteri con il maggior numero di occorrenze devono essere codificati con il minor numero possibile di bit. Ecco un esempio di una possibile tabella 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 carattere con uno spazio. In un file compresso non ci sarà davvero nulla di tutto ciò!
Sorge la domanda: come ha fatto questo principiante a inventare un codice e a creare una tabella dei codici? Di questo parleremo più avanti.

Costruzione dell'albero di Huffman

Qui entrano in gioco gli alberi binari di ricerca. Non preoccuparti, non saranno necessari metodi di ricerca, inserimento o cancellazione. Ecco la struttura dell'albero in 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;
    }
    ...
}

Questo non è il codice completo, il codice completo sarà fornito più avanti.

Ecco l'algoritmo stesso per costruire l'albero:

  1. Creare un oggetto Node per ogni carattere del messaggio (stringa s1). Nel nostro caso ci saranno 9 nodi (oggetti Node). Ogni nodo consiste in due campi dati: carattere 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à. Più bassa è la frequenza, maggiore è la priorità. Così facendo, viene sempre scelto l'albero con la frequenza più bassa.

Dopodiché, è necessario eseguire ciclicamente quanto segue:

  1. Estrarre due alberi dalla coda di priorità e farli diventare figli di un nuovo nodo (il nodo appena creato senza lettera). La frequenza del nuovo nodo è uguale alla 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, è probabile che assuma una nuova posizione nella coda.)
  3. Continuare a eseguire i passi 1 e 2 finché nella coda non rimane 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 il passaggio a una nuova riga, «sp» (space) — è uno spazio.

E poi?

Abbiamo ottenuto l'albero di Huffman. Bene, e cosa farne? Non lo prendono nemmeno gratuitamente. E poi, dobbiamo tracciare tutti i possibili percorsi dalla radice alle foglie dell'albero. Concordiamo di indicare il bordo 0 se va al figlio sinistro e 1 — se va al destro. In termini rigorosi, in queste designazioni, il codice del simbolo è il percorso dalla radice dell'albero alla foglia che contiene proprio quel simbolo.

Compressione dei dati con l'algoritmo di Huffman

In questo modo abbiamo ottenuto la tabella dei codici. Notiamo che, esaminando questa tabella, possiamo trarre la conclusione sul "peso" di ogni simbolo: si tratta della 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. Di conseguenza, lo abbiamo ridotto a ben 176/65 = 2.7 volte! Ma questa è utopia. È difficile che tale coefficiente venga raggiunto. Perché? Ne parleremo più avanti.

Decodifica

Bene, rimane l'operazione più semplice: la decodifica. Penso che molti di voi avranno capito che non si può semplicemente creare un file compresso senza alcun riferimento su come è stato codificato: non saremo in grado di decodificarlo! Sì, è stato difficile da accettare, ma dovremo 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 ritorno a capo - 'n' - in un salto di riga (per quanto possa sembrare sciocco). Pertanto, la riga vuota in alto è il simbolo per il codice 01110. Per il codice 00, il simbolo è uno spazio all'inizio della riga. Dico subito che il nostro coefficiente ha dei problemi: questo modo di memorizzare la tabella può essere considerato il meno razionale. Ma è semplice da capire e implementare. Sarò felice di ascoltare i vostri suggerimenti nei commenti riguardo all'ottimizzazione.

Con questa tabella, è molto semplice decodificare. Ricordiamo quale regola abbiamo seguito durante la creazione della codifica:

Nessun codice deve essere un prefisso di un altro.

Ed è proprio qui che questa regola semplifica le cose. Leggiamo bit per bit, e non appena la stringa d, composta dai bit letti, corrisponde alla codifica associata al simbolo character, sappiamo immediatamente che è stato codificato il simbolo character (e solo lui!). Successivamente, registriamo character nella stringa decodificata (la stringa contenente il messaggio decodificato), azzeriamo la stringa d e continuiamo a leggere il file codificato.

Implementazione

È giunto il momento di mettere alla prova il mio codice e scrivere un compressore. Lo chiameremo Compressor.

Iniziamo dall'inizio. La prima cosa è scrivere la classe Node:

public class Node {
    private int frequence; // frequenza
    private char letter; // lettera
    private Node leftChild; // figlio sinistro
    private Node rightChild; // figlio destro

    public Node(char letter, int frequence) { // costruttore
        this.letter = letter;
        this.frequence = frequence;
    }

    public Node() {} // sovraccarico del costruttore per nodi anonimi (vedi sopra nella sezione sulla costruzione dell'albero di Huffman)
    public void addChild(Node newNode) { // aggiungere un nodo figlio
        if (leftChild == null) // se il sinistro è vuoto => anche il destro è vuoto => aggiungiamo a sinistra
            leftChild = newNode;
        else {
            if (leftChild.getFrequence() <= newNode.getFrequence()) // in generale, diventa il nodo figlio destro
                rightChild = newNode; // diventerà quel che ha frequenza inferiore
            else {
                rightChild = leftChild;
                leftChild = newNode;
            }
        }

        frequence += newNode.getFrequence(); // frequenza totale
    }

    public Node getLeftChild() {
        return leftChild;
    }

    public Node getRightChild() {
        return rightChild;
    }

    public int getFrequence() {
        return frequence;
    }

    public char getLetter() {
        return letter;
    }

    public boolean isLeaf() { // verifica se è un foglia
        return leftChild == null && rightChild == null;
    }
}

Ora per l'alberello:

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;
    }
}

Coda prioritaria:

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

class PriorityQueue {
    private ArrayList data; // lista della coda
    private int nElems; // numero di elementi nella coda

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

    public void insert(BinaryTree newTree) { // inserimento
        if (nElems == 0)
            data.add(newTree);
        else {
            for (int i = 0; i  newTree.getFrequence()) { // se la frequenza dell'albero inserito è inferiore 
                    data.add(i, newTree); // spostiamo tutti gli alberi a destra di una cella
                    break; // poi mettiamo il nuovo albero nella posizione dell'attuale
                }
                if (i == nElems - 1) 
                    data.add(newTree);
            }
        }
        nElems++; // aumentiamo il numero di elementi di 1
    }

    public BinaryTree remove() { // rimozione dalla coda
        BinaryTree tmp = data.get(0); // copiamo l'elemento da rimuovere
        data.remove(0); // in effetti, rimuoviamo
        nElems--; // diminuiamo il numero di elementi di 1
        return tmp; // restituiamo l'elemento rimosso (l'elemento con la frequenza più bassa)
    }
}

Classe che crea l'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


    //----------------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();
        // 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]); // allora creare un Node per esso
                BinaryTree newTree = new BinaryTree(newNode); // e creare un BinaryTree per il Node
                pq.insert(newTree); // inserire nella coda
            }
        }

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

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

                Node newNode = new Node(); // creare un nuovo Node
                newNode.addChild(tree1.getRoot()); // rendere figli i due alberi estratti
                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;
    }

    //-------------------encoding array------------------
    void fillEncodingArray(Node node, String codeBefore, String direction) { // riempi 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 contiene la codifica/decodifica:

public class HuffmanOperator {
    private final byte ENCODING_TABLE_SIZE = 127; // dimensione 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 comprimere
        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 (uno può bastare, 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 la quantità di zeri aggiunti 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 - somma 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() { // compressione 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 contiene 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 resettiamo 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 il 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 facilita 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 facilita 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 {//specific instruction involves command-line arguments
            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 argomenti non valido ");
            System.out.println("Controlla 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("Percorso non valido o il file non esiste!");
            return;
        } catch (MalformedInputException e) {
        	System.out.println("La codifica attuale del file non è supportata");
        	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("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;//coefficiente ideale
        double realRatio = Math.round((double) inputFile.length() 
        		/ ((double) compressedFile.length() + (double) table.length()) * 100) / (double)100;//coefficiente reale
        
        System.out.println("Il coefficiente ideale di compressione è " + idealRatio);
        System.out.println("Il coefficiente di compressione tenendo conto della 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];
        //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("Percorso del file estratto " + extractedFile.getAbsolutePath());
    }
}

Dovrete scrivere voi stessi il file di istruzioni readme.txt 🙂

Conclusione

Probabilmente è tutto ciò che volevo dire. Se avete qualcosa da dire riguardo alla mia incompetenza in miglioramenti del codice, algoritmi o in qualsiasi ottimizzazione, non esitate a scrivere. Se qualcosa non è chiaro, scrivetelo. Sarò felice di sentirvi nei commenti!

P.S.

Sì, sì, sono ancora qui, perché non ho dimenticato il coefficiente. Per la stringa s1, la tabella di codifica pesa 48 byte, molto più del file originale, e non dimentichiamo gli zeri aggiuntivi (il numero di zeri aggiunti è pari a 7) => il coefficiente di compressione sarà inferiore a uno: 176/(65 + 48*8 + 7)=0.38. Se anche voi lo avete notato, complimenti, non vi è sfuggito. Sì, questa implementazione sarà estremamente inefficace per file piccoli. Ma cosa succede con file grandi? Le dimensioni del file superano di gran lunga la dimensione della tabella di codifica. Qui l'algoritmo funziona come dovrebbe! Per esempio, per il monologo di Faust il compressore fornisce un rapporto reale (non ideale) pari a 1.46, quasi un uno e mezzo! Ed era previsto che il file fosse in inglese.

Fonte: habr.com

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