Compressão de dados com o algoritmo de Huffman

Entrada

Neste artigo, falarei sobre o conhecido algoritmo de Huffman, bem como sua aplicação na compressão de dados.

Como resultado, escreveremos um arquivador simples. Isso já foi artigo sobre Habré, mas sem implementação prática. O material teórico do post atual é retirado das aulas de ciência da computação da escola e do livro "Data Structures and Algorithms in Java" de Robert Laforet. Então, tudo está sob o corte!

Um pouco de reflexão

Em um arquivo de texto normal, um caractere é codificado com 8 bits (codificação ASCII) ou 16 (codificação Unicode). Em seguida, consideraremos a codificação ASCII. Por exemplo, pegue a string s1 = "SUSIE DIZ QUE É FÁCIL". No total, existem 22 caracteres na linha, é claro, incluindo espaços e o caractere de nova linha - 'n'. O arquivo que contém esta linha pesará 22*8 = 176 bits. A questão surge imediatamente: é racional usar todos os 8 bits para codificar 1 caractere? Não usamos todos os caracteres ASCII. Mesmo que fossem, seria mais racional dar a letra mais frequente - S - o código mais curto possível, e para a letra mais rara - T (ou U, ou 'n') - dar o código mais autêntico. Este é o algoritmo de Huffman: você precisa encontrar a melhor opção de codificação, na qual o arquivo terá peso mínimo. É bastante normal que caracteres diferentes tenham comprimentos de código diferentes - essa é a base do algoritmo.

Codificação

Por que não dar ao caractere 'S' um código, por exemplo, 1 bit de comprimento: 0 ou 1. Seja 1. Então o segundo caractere que mais ocorre - ' ' (espaço) - daremos 0. Imagine, você começou a decodifique sua mensagem - string codificada s1 - e você verá que o código começa com 1. Então, o que fazer: é o caractere S ou algum outro caractere, como A? Portanto, surge uma regra importante:

Nenhum código deve ser um prefixo de outro

Esta regra é a chave para o algoritmo. Portanto, a criação do código começa com uma tabela de frequência, que indica a frequência (número de ocorrências) de cada símbolo:

Compressão de dados com o algoritmo de Huffman Os caracteres com mais ocorrências devem ser codificados com o menor número possivel o número de bits. Vou dar um exemplo de uma das tabelas de códigos possíveis:

Compressão de dados com o algoritmo de Huffman Portanto, a mensagem codificada ficará assim:

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

Separei o código de cada caractere com um espaço. Isso realmente não acontecerá em um arquivo compactado!
Surge a pergunta: como esse novato criou um código como criar uma tabela de códigos? Isso será discutido abaixo.

Construindo uma Árvore Huffman

É aqui que as árvores de busca binária vêm em socorro. Não se preocupe, você não precisará dos métodos de pesquisa, inserção e exclusão aqui. Aqui está a estrutura da árvore em 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;
    }
    ...
}

Este não é o código completo, o código completo estará abaixo.

Aqui está o algoritmo para construir uma árvore:

  1. Crie um objeto Node para cada caractere da mensagem (linha s1). No nosso caso, serão 9 nós (objetos Node). Cada nó consiste em dois campos de dados: símbolo e frequência
  2. Crie um objeto Árvore (BinaryTree) para cada um dos nós do Nó. O nó se torna a raiz da árvore.
  3. Insira essas árvores na fila de prioridade. Quanto menor a frequência, maior a prioridade. Assim, ao extrair, sempre é selecionada a árvore com menor frequência.

Em seguida, você precisa fazer ciclicamente o seguinte:

  1. Recupere duas árvores da fila de prioridade e torne-as filhas de um novo nó (um nó recém-criado sem uma letra). A frequência do novo nó é igual à soma das frequências das duas árvores descendentes.
  2. Para este nó, crie uma árvore com raiz neste nó. Insira esta árvore de volta na fila de prioridade. (Como a árvore tem uma nova frequência, provavelmente ela entrará em um novo lugar na fila)
  3. Continue as etapas 1 e 2 até que uma árvore seja deixada na fila - a árvore Huffman

Considere este algoritmo na linha s1:

Compressão de dados com o algoritmo de Huffman

Aqui o símbolo "lf" (alimentação de linha) denota uma nova linha, "sp" (espaço) é um espaço.

E depois?

Temos a árvore Huffman. OK. E o que fazer com isso? Eles não vão pegar de graça.E então, você precisa traçar todos os caminhos possíveis desde a raiz até as folhas da árvore. Concordamos em rotular uma aresta 0 se levar ao filho da esquerda e 1 se levar ao filho da direita. A rigor, nessas notações, o código do caractere é o caminho da raiz da árvore até a folha que contém esse mesmo caractere.

Compressão de dados com o algoritmo de Huffman

Assim, a tabela de códigos acabou. Observe que, se considerarmos esta tabela, podemos concluir sobre o "peso" de cada caractere - esse é o comprimento de seu código. Então, em formato compactado, o arquivo de origem pesará: 2 * 3 + 2 * 4 + 3 * 3 + 6 * 2 + 1 * 4 + 1 * 5 + 2 * 4 + 4 * 2 + 1 * 5 = 65 bits . No início, pesava 176 bits. Portanto, reduzimos em até 176/65 = 2.7 vezes! Mas isso é uma utopia. É improvável que tal proporção seja obtida. Por que? Isso será discutido um pouco mais tarde.

Decodificação

Bem, talvez a coisa mais simples que resta seja a decodificação. Acho que muitos de vocês adivinharam que é impossível simplesmente criar um arquivo compactado sem nenhuma dica sobre como ele foi codificado - não seremos capazes de decodificá-lo! Sim, sim, foi difícil para mim perceber isso, mas tenho que criar um arquivo de texto table.txt com uma tabela de compactação:

01110
 00
A010
E1111
I110
S10
T0110
U01111
Y1110

Entrada de tabela no formato 'caractere'"código de caractere". Por que 01110 não tem símbolo? Na verdade, é com um símbolo, apenas as ferramentas java que uso ao enviar para um arquivo, o caractere de nova linha - 'n' - é convertido em uma nova linha (não importa o quão estúpido pareça). Portanto, a linha vazia acima é o caractere do código 01110. Para o código 00, o caractere é um espaço no início da linha. Devo dizer desde já que este método de armazenamento da tabela pode ser considerado o mais irracional para o nosso coeficiente Khan. Mas é fácil de entender e implementar. Ficarei feliz em ouvir suas recomendações nos comentários sobre otimização.

Com esta tabela, é muito fácil decodificar. Vamos lembrar por qual regra fomos guiados ao criar a codificação:

Nenhum código deve ser um prefixo de outro

É aqui que ela desempenha um papel facilitador. Lemos bit a bit sequencialmente e, assim que a string d resultante, que consiste nos bits lidos, corresponde à codificação correspondente ao caractere do caractere, sabemos imediatamente que o caractere do caractere (e somente ele!) foi codificado. Em seguida, escrevemos o caractere na string de decodificação (a string que contém a mensagem decodificada), redefinimos a string d e lemos o arquivo codificado ainda mais.

Implementação

É hora de humilhar meu código escrevendo um arquivador. Vamos chamá-lo de Compressor.

Recomeçar. Primeiro de tudo, escrevemos a classe 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;
    }
}

Agora a árvore:

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

Fila de prioridade:

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;//возвращаем удаленный элемент(элемент с наименьшей частотой)
    }
}

A classe que cria a árvore 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;
    }
}

A classe que contém qual codifica/decodifica:

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("========================================================");
    }
    }

Uma classe que facilita a gravação em um arquivo:

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

Uma classe que facilita a leitura de um arquivo:

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

Bem, e a classe principal:

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

Você mesmo terá que escrever o arquivo com as instruções readme.txt 🙂

Conclusão

Acho que isso é tudo que eu queria dizer. Se você tem algo a dizer sobre minha incompetência em melhorias no código, algoritmo, em geral, qualquer otimização, fique à vontade para escrever. Se eu não expliquei algo, por favor, escreva também. Eu adoraria ouvir de você nos comentários!

PS

Sim, sim, ainda estou aqui, porque não esqueci do coeficiente. Para a string s1, a tabela de codificação pesa 48 bytes - muito mais que o arquivo original, e eles não se esqueceram dos zeros extras (o número de zeros adicionados é 7) => a taxa de compactação será menor que um: 176 /(65 + 48*8 + 7) = 0.38. Se você também notou isso, então não apenas na cara que você terminou. Sim, esta implementação será extremamente ineficiente para arquivos pequenos. Mas o que acontece com arquivos grandes? Os tamanhos dos arquivos são muito maiores do que o tamanho da tabela de codificação. É aqui que o algoritmo funciona como deveria! Por exemplo, para monólogo de Fausto o arquivador dá um coeficiente real (não idealizado) igual a 1.46 - quase uma vez e meia! E sim, o arquivo deveria estar em inglês.

Fonte: habr.com

Adicionar um comentário