Συμπίεση δεδομένων με τον αλγόριθμο Huffman

Είσοδος

Σε αυτό το άρθρο θα μιλήσω για τον γνωστό αλγόριθμο Huffman, καθώς και για την εφαρμογή του στη συμπίεση δεδομένων.

Ως αποτέλεσμα, θα γράψουμε ένα απλό αρχείο αρχειοθέτησης. Αυτό έχει ήδη γίνει άρθρο στο Habré, αλλά χωρίς πρακτική εφαρμογή. Το θεωρητικό υλικό της παρούσας ανάρτησης προέρχεται από μαθήματα πληροφορικής στο σχολείο και το βιβλίο του Robert Laforet «Δομές δεδομένων και αλγόριθμοι στην Java». Οπότε, όλα είναι υπό περικοπή!

Λίγος προβληματισμός

Σε ένα κανονικό αρχείο κειμένου, ένας χαρακτήρας κωδικοποιείται με 8 bit (κωδικοποίηση ASCII) ή 16 (κωδικοποίηση Unicode). Στη συνέχεια, θα εξετάσουμε την κωδικοποίηση ASCII. Για παράδειγμα, πάρτε τη συμβολοσειρά s1 = "SUSIE SAYS IT IS EASYn". Συνολικά, υπάρχουν 22 χαρακτήρες στη γραμμή, φυσικά, συμπεριλαμβανομένων των διαστημάτων και του χαρακτήρα νέας γραμμής - 'n'. Το αρχείο που περιέχει αυτή τη γραμμή θα ζυγίζει 22*8 = 176 bit. Αμέσως προκύπτει το ερώτημα: είναι λογικό να χρησιμοποιηθούν και τα 8 bit για την κωδικοποίηση 1 χαρακτήρα; Δεν χρησιμοποιούμε όλους τους χαρακτήρες ASCII. Ακόμα κι αν ήταν, θα ήταν πιο λογικό να δίνεται στο πιο συχνό γράμμα - S - ο συντομότερος δυνατός κωδικός, και για το πιο σπάνιο γράμμα - T (ή U, ή 'n') - να δίνεται ο κωδικός πιο αυθεντικός. Αυτός είναι ο αλγόριθμος Huffman: πρέπει να βρείτε την καλύτερη επιλογή κωδικοποίησης, στην οποία το αρχείο θα έχει ελάχιστο βάρος. Είναι πολύ φυσιολογικό οι διαφορετικοί χαρακτήρες να έχουν διαφορετικά μήκη κώδικα - αυτή είναι η βάση του αλγορίθμου.

Κωδικοποίηση

Γιατί να μην δώσετε στον χαρακτήρα 'S' έναν κωδικό, για παράδειγμα, μήκους 1 bit: 0 ή 1. Αφήστε τον να είναι 1. Τότε ο δεύτερος πιο εμφανιζόμενος χαρακτήρας - ' ' (κενό) - θα δώσουμε 0. Φανταστείτε, αρχίσατε να αποκωδικοποιήστε το μήνυμά σας - κωδικοποιημένη συμβολοσειρά s1 - και βλέπετε ότι ο κωδικός ξεκινά με 1. Λοιπόν, τι πρέπει να κάνετε: είναι ο χαρακτήρας S ή κάποιος άλλος χαρακτήρας, όπως ο A; Επομένως, προκύπτει ένας σημαντικός κανόνας:

Κανένας κωδικός δεν πρέπει να είναι πρόθεμα άλλου

Αυτός ο κανόνας είναι το κλειδί για τον αλγόριθμο. Επομένως, η δημιουργία του κώδικα ξεκινά με έναν πίνακα συχνοτήτων, ο οποίος υποδεικνύει τη συχνότητα (αριθμός εμφανίσεων) κάθε συμβόλου:

Συμπίεση δεδομένων με τον αλγόριθμο Huffman Οι χαρακτήρες με τις περισσότερες εμφανίσεις θα πρέπει να κωδικοποιούνται με τις λιγότερες δυνατόν τον αριθμό των bit. Θα δώσω ένα παράδειγμα ενός από τους πιθανούς πίνακες κωδικών:

Συμπίεση δεδομένων με τον αλγόριθμο Huffman Έτσι το κωδικοποιημένο μήνυμα θα μοιάζει με αυτό:

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

Ξεχώρισα τον κωδικό κάθε χαρακτήρα με ένα κενό. Αυτό δεν θα συμβεί πραγματικά σε ένα συμπιεσμένο αρχείο!
Τίθεται το ερώτημα: πώς αυτός ο πρωτάρης βρήκε έναν κώδικα πώς να δημιουργήσει έναν πίνακα κωδικών; Αυτό θα συζητηθεί παρακάτω.

Χτίζοντας ένα δέντρο Huffman

Εδώ έρχονται στη διάσωση τα δυαδικά δέντρα αναζήτησης. Μην ανησυχείτε, δεν θα χρειαστείτε τις μεθόδους αναζήτησης, εισαγωγής και διαγραφής εδώ. Εδώ είναι η δομή δέντρου στη 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;
    }
    ...
}

Αυτός δεν είναι ο πλήρης κωδικός, ο πλήρης κωδικός θα είναι παρακάτω.

Εδώ είναι ο αλγόριθμος για την κατασκευή ενός δέντρου:

  1. Δημιουργήστε ένα αντικείμενο Node για κάθε χαρακτήρα από το μήνυμα (γραμμή s1). Στην περίπτωσή μας, θα υπάρχουν 9 κόμβοι (αντικείμενα κόμβου). Κάθε κόμβος αποτελείται από δύο πεδία δεδομένων: σύμβολο και συχνότητα
  2. Δημιουργήστε ένα αντικείμενο Tree (BinaryTree) για κάθε έναν από τους κόμβους Node. Ο κόμβος γίνεται η ρίζα του δέντρου.
  3. Εισαγάγετε αυτά τα δέντρα στην ουρά προτεραιότητας. Όσο χαμηλότερη είναι η συχνότητα, τόσο μεγαλύτερη είναι η προτεραιότητα. Έτσι, κατά την εξαγωγή, επιλέγεται πάντα το δέντρο με τη χαμηλότερη συχνότητα.

Στη συνέχεια, πρέπει να κάνετε κυκλικά τα εξής:

  1. Ανακτήστε δύο δέντρα από την ουρά προτεραιότητας και κάντε τα παιδιά ενός νέου κόμβου (έναν κόμβο που δημιουργήθηκε πρόσφατα χωρίς γράμμα). Η συχνότητα του νέου κόμβου είναι ίση με το άθροισμα των συχνοτήτων των δύο απογόνων δέντρων.
  2. Για αυτόν τον κόμβο, δημιουργήστε ένα δέντρο με ρίζες σε αυτόν τον κόμβο. Εισαγάγετε αυτό το δέντρο ξανά στην ουρά προτεραιότητας. (Δεδομένου ότι το δέντρο έχει νέα συχνότητα, πιθανότατα θα μπει σε μια νέα θέση στην ουρά)
  3. Συνεχίστε τα βήματα 1 και 2 μέχρι να μείνει ένα δέντρο στην ουρά - το δέντρο Huffman

Εξετάστε αυτόν τον αλγόριθμο στη γραμμή s1:

Συμπίεση δεδομένων με τον αλγόριθμο Huffman

Εδώ το σύμβολο "lf" (linefeed) υποδηλώνει μια νέα γραμμή, το "sp" (κενό) είναι ένα κενό.

Και τι είναι το επόμενο βήμα;

Πήραμε το δέντρο Huffman. ΕΝΤΑΞΕΙ. Και τι να το κάνουμε; Δεν θα το πάρουν δωρεάν. Και μετά, πρέπει να εντοπίσετε όλα τα πιθανά μονοπάτια από τη ρίζα μέχρι τα φύλλα του δέντρου. Συμφωνούμε να βάλουμε ετικέτα σε μια άκρη με 0 αν οδηγεί στο αριστερό παιδί και με 1 αν οδηγεί στο δεξί. Αυστηρά μιλώντας, σε αυτές τις σημειώσεις, ο κωδικός χαρακτήρα είναι η διαδρομή από τη ρίζα του δέντρου στο φύλλο που περιέχει αυτόν ακριβώς τον χαρακτήρα.

Συμπίεση δεδομένων με τον αλγόριθμο Huffman

Έτσι, προέκυψε ο πίνακας των κωδικών. Σημειώστε ότι αν εξετάσουμε αυτόν τον πίνακα, μπορούμε να συμπεράνουμε σχετικά με το "βάρος" κάθε χαρακτήρα - αυτό είναι το μήκος του κώδικά του. Στη συνέχεια, σε συμπιεσμένη μορφή, το αρχείο προέλευσης θα ζυγίζει: 2 * 3 + 2 * 4 + 3 * 3 + 6 * 2 + 1 * 4 + 1 * 5 + 2 * 4 + 4 * 2 + 1 * 5 = 65 bit . Αρχικά ζύγιζε 176 bit. Επομένως, το μειώσαμε έως και 176/65 = 2.7 φορές! Αλλά αυτό είναι μια ουτοπία. Μια τέτοια αναλογία είναι απίθανο να επιτευχθεί. Γιατί; Αυτό θα συζητηθεί λίγο αργότερα.

Αποκρυπτογράφηση

Λοιπόν, ίσως το πιο απλό πράγμα που απομένει είναι η αποκωδικοποίηση. Νομίζω ότι πολλοί από εσάς έχετε μαντέψει ότι είναι αδύνατο να δημιουργήσετε απλά ένα συμπιεσμένο αρχείο χωρίς υποδείξεις σχετικά με τον τρόπο κωδικοποίησής του - δεν θα μπορέσουμε να το αποκωδικοποιήσουμε! Ναι, ναι, ήταν δύσκολο για μένα να το συνειδητοποιήσω, αλλά πρέπει να δημιουργήσω ένα αρχείο κειμένου table.txt με έναν πίνακα συμπίεσης:

01110
 00
A010
E1111
I110
S10
T0110
U01111
Y1110

Καταχώριση πίνακα με τη μορφή «χαρακτήρας» «κωδικός χαρακτήρα». Γιατί το 01110 είναι χωρίς σύμβολο; Στην πραγματικότητα, με ένα σύμβολο, μόνο τα εργαλεία java που χρησιμοποιώ όταν βγάζω σε ένα αρχείο, ο χαρακτήρας νέας γραμμής - 'n' - μετατρέπεται σε νέα γραμμή (όσο ανόητο κι αν ακούγεται). Επομένως, η κενή γραμμή παραπάνω είναι ο χαρακτήρας για τον κωδικό 01110. Για τον κωδικό 00, ο χαρακτήρας είναι ένα κενό στην αρχή της γραμμής. Πρέπει να πω αμέσως ότι αυτή η μέθοδος αποθήκευσης του πίνακα μπορεί να ισχυριστεί ότι είναι η πιο παράλογη για τον συντελεστή khan μας. Αλλά είναι εύκολο να κατανοηθεί και να εφαρμοστεί. Θα χαρώ να ακούσω τις συστάσεις σας στα σχόλια σχετικά με τη βελτιστοποίηση.

Με αυτόν τον πίνακα, είναι πολύ εύκολο να αποκωδικοποιηθεί. Ας θυμηθούμε ποιος κανόνας καθοδηγηθήκαμε κατά τη δημιουργία της κωδικοποίησης:

Κανένας κωδικός δεν πρέπει να είναι πρόθεμα άλλου

Εδώ παίζει ρόλο διευκόλυνσης. Διαβάζουμε σπιθαμή προς σπιθαμή διαδοχικά και μόλις η προκύπτουσα συμβολοσειρά d, που αποτελείται από τα μπιτ ανάγνωσης, ταιριάζει με την κωδικοποίηση που αντιστοιχεί στον χαρακτήρα του χαρακτήρα, γνωρίζουμε αμέσως ότι ο χαρακτήρας του χαρακτήρα (και μόνο αυτός!) κωδικοποιήθηκε. Στη συνέχεια, γράφουμε χαρακτήρα στη συμβολοσειρά αποκωδικοποίησης (η συμβολοσειρά που περιέχει το αποκωδικοποιημένο μήνυμα), επαναφέρουμε τη συμβολοσειρά d και διαβάζουμε περαιτέρω το κωδικοποιημένο αρχείο.

Реализация

Ήρθε η ώρα να ταπεινώσω τον κώδικα μου γράφοντας έναν αρχειοθέτη. Ας το πούμε Συμπιεστής.

Ξανά από την αρχή. Πρώτα απ 'όλα, γράφουμε την κλάση 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;
    }
}

Τώρα το δέντρο:

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

Ουρά προτεραιότητας:

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

Η τάξη που δημιουργεί το δέντρο 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;
    }
}

Η κλάση που περιέχει την οποία κωδικοποιεί/αποκωδικοποιεί:

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

Μια τάξη που διευκολύνει την εγγραφή σε ένα αρχείο:

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

Μια τάξη που διευκολύνει την ανάγνωση από ένα αρχείο:

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

Λοιπόν, και η κύρια τάξη:

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

Θα πρέπει να γράψετε μόνοι σας το αρχείο με οδηγίες readme.txt 🙂

Συμπέρασμα

Μάλλον αυτό ήθελα να πω. Εάν έχετε κάτι να πείτε για την ανικανότητά μου για βελτιώσεις στον κώδικα, τον αλγόριθμο, γενικά, οποιαδήποτε βελτιστοποίηση, τότε μη διστάσετε να γράψετε. Αν δεν έχω εξηγήσει κάτι, γράψε και εσύ. Θα ήθελα πολύ να ακούσω νέα σας στα σχόλια!

PS

Ναι, ναι, είμαι ακόμα εδώ, γιατί δεν ξέχασα τον συντελεστή. Για τη συμβολοσειρά s1, ο πίνακας κωδικοποίησης ζυγίζει 48 byte - πολύ περισσότερο από το αρχικό αρχείο και δεν ξέχασαν τα επιπλέον μηδενικά (ο αριθμός των μηδενικών που προστέθηκαν είναι 7) => ο λόγος συμπίεσης θα είναι μικρότερος από ένα: 176 /(65 + 48*8 + 7) = 0.38. Αν και εσείς το προσέξατε αυτό, τότε απλά δεν είστε στο πρόσωπο που έχετε τελειώσει. Ναι, αυτή η υλοποίηση θα είναι εξαιρετικά αναποτελεσματική για μικρά αρχεία. Τι συμβαίνει όμως με τα μεγάλα αρχεία; Τα μεγέθη των αρχείων είναι πολύ μεγαλύτερα από το μέγεθος του πίνακα κωδικοποίησης. Εδώ είναι που ο αλγόριθμος λειτουργεί όπως θα έπρεπε! Για παράδειγμα, για Ο μονόλογος του Φάουστ ο αρχειοθέτης δίνει πραγματικό (όχι εξιδανικευμένο) συντελεστή ίσο με 1.46 - σχεδόν μιάμιση φορά! Και ναι, το αρχείο έπρεπε να είναι στα αγγλικά.

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο