Моя программа сжатия LZW едва сжимает

Я беру класс алгоритмов, где мы должны реализовать сжатие LZW в Java. Я решил использовать для этого структуру данных Trie, и я уже реализовал Trie и заставил ее работать, но она очень медленная и почти не сжимается.

Мы должны использовать 8-битные символы и иметь возможность сжимать любой двоичный файл.

Учитывая файл ~ 4 МБ (bible.txt), я получаю около 549 012 элементов в моем массиве кодов. Когда я записываю эти элементы в файл (один целочисленный код в строке), я получаю сжатый файл размером 3,5 МБ, поэтому я получаю 0,5 МБ сжатия.

Как я могу сделать эту программу более эффективной? Я чувствую, что неправильно понял здесь что-то фундаментальное или упустил что-то очевидное, но у меня нет идей, почему это не сжимается.

(Я получил тестовый файл bible.txt с этого веб-сайта: https://corpus.canterbury.ac.nz/descriptions/)

Я читаю байты из двоичного файла, подобного этому (чтение как int и преобразование в char необходимо, чтобы значения выше 0x80 не были отрицательными):

public String readFile(String path) throws IOException, FileNotFoundException {
    File file = new File(path);

    StringBuilder string = new StringBuilder();

    try (FileInputStream fileInputStream = new FileInputStream(file)) {
        int singleCharInt;
        char singleChar;
        while((singleCharInt = fileInputStream.read()) != -1) {
            singleChar = (char) singleCharInt;
            string.append(singleChar);
        }
    } 

    return string.toString();
}

Мой основной метод выглядит следующим образом:

    public static void main(String args[]) throws FileNotFoundException, IOException {
        String bytes = new FileReader().readFile("/home/user/Code/Trie/bible.txt");

        ArrayList<Integer> codes = new Compress().compress(bytes);
    }

Мой класс Compress выглядит следующим образом:

public class Compress {

    private int code = 0;

    public ArrayList<Integer> compress(String data) {
        Trie trie = new Trie();

        // Initialize Trie Data Structure with alphabet (256 possible values with 8-bit
        // symbols)
        for (code = 0; code <= 255; code++) {
            trie.insert(Character.toString((char) code), code);
        }

        code++;

        String s = Character.toString(data.charAt(0));

        ArrayList<Integer> codes = new ArrayList<Integer>();

        for (int i = 1; i < data.length(); i++) {
            String c = Character.toString(data.charAt(i));

            if (trie.find(s + c) > 0) {
                s += c;
            } else {
                codes.add(trie.find(s));
                trie.insert(s + c, code);
                code++;
                s = c;
            }
        }

        codes.add(trie.find(s));

        return codes;
    }

}

Мой класс Trie выглядит так:

public class Trie {
    private TrieNode root;

    public Trie() {
        this.root = new TrieNode(false);
    }

    public void insert (String word, int code) {
        TrieNode current = root;

        for (char l: word.toCharArray()) {
            current = current.getChildren().computeIfAbsent(Character.toString(l), c -> new TrieNode(false));
        }
        current.setCode(code);
        current.setWordEnd(true);
    }

    public int find(String word) {
        TrieNode current = root;

        for (int i = 0 ; i < word.length(); i++) {
            char ch = word.charAt(i);

            TrieNode node = current.getChildren().get(Character.toString(ch));

            if (node == null) {
                return -1;
            }

            current = node;
        }

        return current.getCode();
    }
}

Мой класс TrieNode выглядит так:

public class TrieNode {
    private HashMap<String, TrieNode> children;
    private int code;
    private boolean wordEnd;

    public TrieNode(boolean wordEnd) {
        this.children = new HashMap<String, TrieNode>();
        this.wordEnd = wordEnd;
    }

    public HashMap<String, TrieNode> getChildren() {
        return this.children;
    }

    public void setWordEnd(boolean wordEnd) {
        this.wordEnd = wordEnd;
    }
    
    public boolean isWordEnd() {
        return this.wordEnd;
    }

    public int getCode() {
        return this.code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}

Спасибо за уделенное время!


person bestinthewest    schedule 31.01.2021    source источник
comment
Вам нужно начать с понимания различий между int, char и byte, того, как они представляют данные и как Java выполняет преобразование между ними. Вы не получаете сжатия, потому что используете 4 байта (int) для хранения каждого байта, т.е. занимая в 4 раза больше места. Кроме того, хотя я не исследовал ваш код подробно, вы также можете внести искажения при преобразовании int-to-char. Этих основ достаточно для целой главы учебника, и они слишком широки для Stack Overflow.   -  person Jim Garrison    schedule 31.01.2021
comment
@JimGarrison Я не понимаю, что я должен использовать, если int не подходит для хранения кодов. Я получаю около 250 000 кодов для большого файла, который больше байта и короткий. Следующий вариант - int, но вы говорите, что они слишком большие, поэтому я не уверен, что это за альтернатива. Вы определенно правы в том, что мне не хватает основ.   -  person bestinthewest    schedule 31.01.2021
comment
Внутренне вы в конечном итоге будете использовать int, но все входные значения, с которыми вы работаете, имеют длину 1 байт (0x00-0xFF), и, поскольку вы ожидаете 250 000 кодов, вы будете использовать кодирование переменной длины. Чтобы фактически записать сжатые данные в файл, вы собираетесь преобразовать эти коды в битовые строки переменной длины, упаковать их в буфер и записать полученные байты. Здесь будут полезны буферы NIO. В вашем коде не должно быть char или String, потому что входные данные должны обрабатываться как двоичные, без преобразования набора символов.   -  person Jim Garrison    schedule 31.01.2021


Ответы (1)


Что это значит: когда я записываю эти элементы в файл (один целочисленный код в строке)? Вы пишете четыре байта в файл для каждого кода? Вы пишете четыре байта и новую строку? Вы пишете число в десятичном виде и с новой строки?

В любом случае, все они неверны. Вам нужно хранить коды как биты. В обычной реализации LZW количество битов в коде начинается с 9, а затем увеличивается по мере создания новых кодов. Далее в файл код может быть, например, 12- или 13-битным. Декодер знает из данных, когда кодировщик увеличился, поэтому он всегда знает, сколько битов нужно получить для следующего кода. Время от времени имеет смысл вернуться к 9 битам, о чем энкодер сигнализирует декодеру.

Итак, как вы читаете и записываете биты в файл? Вы быстро обнаружите, что для этого нет никаких функций. Вам нужно написать их самостоятельно.

Короче говоря, вы сохраняете буфер битов в целом числе, используя битовый сдвиг и/или операции для добавления битов в буфер, отслеживая, сколько битов находится в буфере в другом целом числе. Для кодирования, после того как вы добавите биты в буфер, вы увидите, есть ли там хотя бы 8 бит. Если это так, запишите байт в файл и удалите 8 бит из буфера. Повторяйте до тех пор, пока в буфере не останется меньше 8 бит.

В конце нужно позаботиться о том, чтобы записать последние несколько битов в еще один байт, убедившись, что вы продумали, как декодер узнает, когда прекратить декодирование битов.

То же самое на стороне декодера: чтение байтов из входного файла и добавление 8 бит за раз в ваш буфер, пока у вас не будет достаточно битов для извлечения следующего кода.

person Mark Adler    schedule 31.01.2021