การบีบอัดข้อมูลด้วยอัลกอริธึม Huffman

การเข้า

ในบทความนี้ ฉันจะพูดถึงอัลกอริธึม Huffman ที่รู้จักกันดีตลอดจนการประยุกต์ใช้ในการบีบอัดข้อมูล

ด้วยเหตุนี้เราจะเขียนโปรแกรมเก็บถาวรแบบง่าย เรื่องนี้เกิดขึ้นแล้ว บทความเกี่ยวกับฮาเบรแต่ไม่มีการปฏิบัติจริง เนื้อหาทางทฤษฎีของโพสต์ปัจจุบันนำมาจากบทเรียนวิทยาการคอมพิวเตอร์ของโรงเรียนและหนังสือ "โครงสร้างข้อมูลและอัลกอริทึมใน Java" ของ Robert Laforet ดังนั้น ทุกอย่างก็อยู่ในขั้นตอน!

ภาพสะท้อนเล็กน้อย

ในไฟล์ข้อความปกติ อักขระหนึ่งตัวจะถูกเข้ารหัสด้วย 8 บิต (การเข้ารหัส ASCII) หรือ 16 (การเข้ารหัส Unicode) ต่อไปเราจะพิจารณาการเข้ารหัส ASCII ตัวอย่างเช่น ใช้สตริง s1 = "SUSIE SAYS IT IS EASYn" โดยรวมแล้วมีอักขระทั้งหมด 22 ตัวในบรรทัด ซึ่งรวมถึงการเว้นวรรคและอักขระขึ้นบรรทัดใหม่ - 'n' ไฟล์ที่มีบรรทัดนี้จะมีน้ำหนัก 22*8 = 176 บิต คำถามเกิดขึ้นทันที: มีเหตุผลไหมที่จะใช้ทั้ง 8 บิตในการเข้ารหัส 1 อักขระ? เราไม่ได้ใช้อักขระ ASCII ทั้งหมด แม้ว่าจะเป็นเช่นนั้นก็ตาม ก็คงจะมีเหตุผลมากกว่าถ้าจะให้ตัวอักษรที่ใช้บ่อยที่สุด - S - รหัสที่สั้นที่สุดเท่าที่จะเป็นไปได้ และสำหรับตัวอักษรที่หายากที่สุด - T (หรือ U หรือ 'n') - ให้รหัสที่น่าเชื่อถือมากขึ้น นี่คืออัลกอริทึมของ Huffman: คุณต้องค้นหาตัวเลือกการเข้ารหัสที่ดีที่สุด ซึ่งไฟล์จะมีน้ำหนักขั้นต่ำ เป็นเรื่องปกติที่อักขระที่แตกต่างกันจะมีความยาวโค้ดต่างกัน ซึ่งเป็นพื้นฐานของอัลกอริทึม

การเข้ารหัส

ทำไมไม่ให้รหัสอักขระ 'S' เช่น ยาว 1 บิต: 0 หรือ 1 ปล่อยให้เป็น 1 จากนั้นอักขระที่เกิดขึ้นมากที่สุดเป็นอันดับสอง - ' ' (ช่องว่าง) - เราจะให้ 0 ลองนึกภาพคุณเริ่ม ถอดรหัสข้อความของคุณ - สตริงที่เข้ารหัส s1 - และคุณเห็นว่าโค้ดขึ้นต้นด้วย 1 แล้วต้องทำอย่างไร: มันเป็นอักขระ S หรือเป็นอักขระอื่น เช่น A? ดังนั้นจึงมีกฎสำคัญเกิดขึ้น:

ไม่มีรหัสใดจะต้องเป็นคำนำหน้าของรหัสอื่น

กฎข้อนี้เป็นกุญแจสำคัญในอัลกอริทึม ดังนั้นการสร้างโค้ดจึงเริ่มต้นด้วยตารางความถี่ซึ่งระบุความถี่ (จำนวนครั้งที่เกิดขึ้น) ของแต่ละสัญลักษณ์:

การบีบอัดข้อมูลด้วยอัลกอริธึม Huffman อักขระที่มีเหตุการณ์มากที่สุดควรเข้ารหัสให้น้อยที่สุด เป็นไปได้ จำนวนบิต ฉันจะยกตัวอย่างตารางโค้ดที่เป็นไปได้รายการหนึ่ง:

การบีบอัดข้อมูลด้วยอัลกอริธึม Huffman ดังนั้นข้อความที่เข้ารหัสจะมีลักษณะดังนี้:

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

ฉันแยกรหัสของอักขระแต่ละตัวด้วยการเว้นวรรค สิ่งนี้จะไม่เกิดขึ้นในไฟล์บีบอัดจริงๆ!
คำถามเกิดขึ้น: มือใหม่นี้เกิดโค้ดได้อย่างไร จะสร้างตารางโค้ดได้อย่างไร? เรื่องนี้จะมีการหารือด้านล่าง

การสร้างต้นไม้ฮัฟฟ์แมน

นี่คือจุดที่ต้นไม้ค้นหาแบบไบนารีเข้ามาช่วยเหลือ ไม่ต้องกังวล คุณไม่จำเป็นต้องค้นหา แทรก และลบวิธีการที่นี่ นี่คือโครงสร้างต้นไม้ใน 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. สร้างวัตถุโหนดสำหรับอักขระแต่ละตัวจากข้อความ (บรรทัด s1) ในกรณีของเรา จะมี 9 โหนด (วัตถุโหนด) แต่ละโหนดประกอบด้วยช่องข้อมูลสองช่อง: สัญลักษณ์และความถี่
  2. สร้างวัตถุ Tree (BinaryTree) สำหรับแต่ละโหนดโหนด โหนดจะกลายเป็นรากของทรี
  3. แทรกแผนผังเหล่านี้ลงในคิวลำดับความสำคัญ ยิ่งความถี่ต่ำ ลำดับความสำคัญก็จะยิ่งสูงขึ้น ดังนั้นเมื่อทำการสกัด ต้นไม้ที่มีความถี่ต่ำสุดจะถูกเลือกเสมอ

ถัดไป คุณต้องทำสิ่งต่อไปนี้เป็นวงจร:

  1. ดึงต้นไม้สองต้นจากคิวลำดับความสำคัญและทำให้เป็นลูกของโหนดใหม่ (โหนดที่สร้างขึ้นใหม่โดยไม่มีตัวอักษร) ความถี่ของโหนดใหม่จะเท่ากับผลรวมของความถี่ของแผนผังลูกหลานทั้งสอง
  2. สำหรับโหนดนี้ ให้สร้างทรีที่รูทที่โหนดนี้ แทรกแผนผังนี้กลับเข้าไปในคิวลำดับความสำคัญ (เนื่องจากต้นไม้มีความถี่ใหม่ จึงมีแนวโน้มจะเข้าที่ใหม่ในคิว)
  3. ทำขั้นตอนที่ 1 และ 2 ต่อไปจนกว่าจะเหลือต้นไม้ต้นหนึ่งอยู่ในคิว - ต้น Huffman

พิจารณาอัลกอริทึมนี้ในบรรทัด s1:

การบีบอัดข้อมูลด้วยอัลกอริธึม Huffman

ที่นี่สัญลักษณ์ "lf" (linefeed) หมายถึงขึ้นบรรทัดใหม่ "sp" (ช่องว่าง) คือช่องว่าง

ถัดไปคืออะไร

เราได้ต้นฮัฟฟ์แมนมา ตกลง. และจะทำอย่างไรกับมัน? พวกเขาจะไม่รับมันไปฟรี ๆ จากนั้น คุณต้องติดตามเส้นทางที่เป็นไปได้ทั้งหมดตั้งแต่รากไปจนถึงใบของต้นไม้ เราตกลงที่จะติดป้ายกำกับ edge 0 หากนำไปสู่ลูกด้านซ้าย และ 1 หากนำไปสู่ลูกด้านขวา พูดอย่างเคร่งครัดในสัญลักษณ์เหล่านี้ รหัสอักขระคือเส้นทางจากรากของต้นไม้ไปยังใบไม้ที่มีอักขระนี้

การบีบอัดข้อมูลด้วยอัลกอริธึม Huffman

ดังนั้นตารางรหัสจึงปรากฏออกมา โปรดทราบว่าหากเราพิจารณาตารางนี้ เราก็สามารถสรุปเกี่ยวกับ "น้ำหนัก" ของอักขระแต่ละตัวได้ - นี่คือความยาวของโค้ด จากนั้นในรูปแบบบีบอัด ไฟล์ต้นฉบับจะมีน้ำหนัก: 2 * 3 + 2 * 4 + 3 * 3 + 6 * 2 + 1 * 4 + 1 * 5 + 2 * 4 + 4 * 2 + 1 * 5 = 65 บิต . ในตอนแรกมันมีน้ำหนัก 176 บิต เราก็เลยลดให้มากถึง 176/65 = 2.7 เท่า! แต่นี่คือยูโทเปีย ไม่น่าจะได้อัตราส่วนดังกล่าว ทำไม เรื่องนี้จะมีการหารือในภายหลัง

ถอดรหัส

บางทีสิ่งที่ง่ายที่สุดที่เหลือคือการถอดรหัส ฉันคิดว่าหลายท่านเดาว่ามันเป็นไปไม่ได้ที่จะสร้างไฟล์บีบอัดโดยไม่มีคำแนะนำเกี่ยวกับวิธีการเข้ารหัส - เราจะไม่สามารถถอดรหัสมันได้! ใช่ ใช่ มันยากสำหรับฉันที่จะตระหนักถึงสิ่งนี้ แต่ฉันต้องสร้างไฟล์ข้อความ table.txt ด้วยตารางการบีบอัด:

01110
 00
A010
E1111
I110
S10
T0110
U01111
Y1110

รายการตารางในรูปแบบ 'อักขระ' "รหัสอักขระ" ทำไม 01110 ถึงไม่มีสัญลักษณ์? ในความเป็นจริงมันมีสัญลักษณ์เพียงแค่เครื่องมือจาวาที่ฉันใช้เมื่อส่งออกไปยังไฟล์อักขระขึ้นบรรทัดใหม่ - 'n' - จะถูกแปลงเป็นการขึ้นบรรทัดใหม่ (ไม่ว่ามันจะฟังดูงี่เง่าแค่ไหนก็ตาม) ดังนั้นบรรทัดว่างด้านบนจึงเป็นอักขระสำหรับรหัส 01110 สำหรับรหัส 00 อักขระคือการเว้นวรรคที่จุดเริ่มต้นของบรรทัด ฉันต้องบอกทันทีว่าวิธีการจัดเก็บตารางนี้สามารถอ้างได้ว่าเป็นวิธีที่ไม่มีเหตุผลที่สุดสำหรับค่าสัมประสิทธิ์ข่านของเรา แต่ง่ายต่อการเข้าใจและนำไปใช้ ฉันยินดีที่จะรับฟังคำแนะนำของคุณในความคิดเห็นเกี่ยวกับการเพิ่มประสิทธิภาพ

ด้วยตารางนี้ มันง่ายมากที่จะถอดรหัส โปรดจำไว้ว่ากฎใดที่เราได้รับคำแนะนำเมื่อสร้างการเข้ารหัส:

ไม่มีรหัสใดจะต้องเป็นคำนำหน้าของรหัสอื่น

นี่คือจุดที่มันมีบทบาทในการอำนวยความสะดวก เราอ่านทีละบิตตามลำดับ และทันทีที่สตริงผลลัพธ์ d ซึ่งประกอบด้วยบิตการอ่าน ตรงกับการเข้ารหัสที่สอดคล้องกับอักขระอักขระ เราจะรู้ทันทีว่าอักขระอักขระ (และมีเพียงมันเท่านั้น!) ถูกเข้ารหัส ต่อไป เราจะเขียนอักขระลงในสตริงการถอดรหัส (สตริงที่มีข้อความที่ถอดรหัสแล้ว) รีเซ็ตสตริง d และอ่านไฟล์ที่เข้ารหัสเพิ่มเติม

การดำเนินงาน

ถึงเวลาที่จะทำให้โค้ดของฉันอับอายด้วยการเขียน Archiver เรียกมันว่าคอมเพรสเซอร์

เริ่มต้นใหม่. ก่อนอื่นเราเขียนคลาส 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 ไบต์ ซึ่งมากกว่าไฟล์ต้นฉบับมาก และพวกเขาก็ไม่ลืมเกี่ยวกับศูนย์พิเศษ (จำนวนศูนย์ที่เพิ่มคือ 7) => อัตราการบีบอัดจะน้อยกว่าหนึ่ง: 176 /(65 + 48*8 + 7) = 0.38 หากคุณสังเกตเห็นสิ่งนี้ก็แสดงว่าคุณทำเสร็จแล้ว ใช่ การใช้งานนี้จะไม่มีประสิทธิภาพอย่างมากสำหรับไฟล์ขนาดเล็ก แต่จะเกิดอะไรขึ้นกับไฟล์ขนาดใหญ่? ขนาดไฟล์มีขนาดใหญ่กว่าขนาดตารางการเข้ารหัสมาก นี่คือจุดที่อัลกอริทึมทำงานอย่างที่ควรจะเป็น! ตัวอย่างเช่นสำหรับ บทพูดคนเดียวของเฟาสต์ ผู้จัดเก็บให้ค่าสัมประสิทธิ์จริง (ไม่อุดมคติ) เท่ากับ 1.46 - เกือบหนึ่งเท่าครึ่ง! ใช่แล้ว ไฟล์นี้ควรจะเป็นภาษาอังกฤษ

ที่มา: will.com

เพิ่มความคิดเห็น