Как я могу оптимизировать декодирование Хаффмана?

Итак, я пытался декодировать с помощью Хаффмана, и у меня есть эта рабочая функция, но она имеет ужасную временную и пространственную сложность. До сих пор я читал каждый байт, получал каждый бит и добавлял его в currentBitString. Затем я перевернул строку и добавил ее в огромную строку, которая в основном содержит все байтовые данные для файла. После этого я проследил бы гигантскую строку и поискал бы код Хаффмана, а затем, если бы он совпал, я бы записал в файл. Этот код занимает около 60 секунд, чтобы декодировать 200 КБ, что очень плохо, но я не совсем уверен, как его улучшить? Я знаю, что мог бы для начала записать в файл более одного байта за раз, но, похоже, это не улучшило время, когда я пытался?

         public static void decode(File f) throws Exception {

    BufferedInputStream fin = new BufferedInputStream(new FileInputStream(f));
    int i = f.getName().lastIndexOf('.');
    String extension="txt";
    String newFileName=f.getName().substring(0, i)+extension;
    File nf = new File(newFileName);
    BufferedOutputStream fw = new BufferedOutputStream(new FileOutputStream(nf));
    int c;
    byte bits;
    byte current;
    String currentBitString="";
    String bitString="";
    //read each byte from file, reverse it, add to giant bitString
    //reads ALL BYTES
    while( (c=fin.read())!=-1 ) {
        current=(byte) c;
        currentBitString="";
        bits=0;
        for(int q=0;q<8;q++) {
            bits=getBit(current,q);
            currentBitString+=bits;
        }
        StringBuilder bitStringReverse=new StringBuilder(currentBitString);
        bitString+=bitStringReverse.reverse().toString();
    }
    currentBitString="";
    boolean foundCode=false;
    for(int j=0;j<bitString.length();j++) {
        currentBitString+=bitString.charAt(j);
        for(int k=0;k<nodes.length;k++) {
            //nodes is an array of huffman nodes which contains the the byte 
            //data and the huffman codes for each byte
            if(nodes[k].code.compareTo(currentBitString.trim())==0) {
                fw.write(nodes[k].data);    
                foundCode=true;
                break;
            }
        }
        if(foundCode) {
            currentBitString="";
            foundCode=false;
        }

    }
    fw.flush();
    fw.close();
    fin.close();

}

вот функция gitBit

        public static byte getBit(byte ID, int position) {
        // return cretin bit in selected byte
        return (byte) ((ID >> position) & 1);
        }

вот элементы данных класса HuffmanNode (массив узлов представляет собой массив узлов HuffmanNodes)

       public class HuffmanNode{
       byte data;
       int repetitions;
       String code;
       HuffmanNode right;
       HuffmanNode left;
       }

person TheHaruWhoCodes    schedule 11.11.2018    source источник
comment
Возможно, это будет полезно geeksforgeeks.org/   -  person Maor Refaeli    schedule 11.11.2018
comment
У вас может быть очень неэффективно закодированная реализация, но сложность ничего не говорит об этом. Это по-прежнему O (n) времени, а при быстром сканировании - O (n) пространства. Вы задаете неправильный вопрос, вы не можете понять его сложность, но хотите оптимизировать код. И вопрос, вероятно, слишком широк для SO.   -  person Erwin Bolwidt    schedule 11.11.2018
comment
У меня действительно нет плана, что делает ваш код. Но я заметил, что выделение памяти очень тяжелое: для каждого символа, который вы читаете, вы выделяете экземпляр StringBuilder, toString() выделяет String instance, а += выделяет еще один экземпляр 9 String. Существует огромный потенциал для оптимизации.   -  person Codo    schedule 11.11.2018
comment
См. rules.sonarsource.com/java/RSPEC-1643. Кроме того, зачем строить битовую строку слева направо, а затем переворачивать ее (таким образом используя больше памяти и занимая больше времени), когда вы можете просто построить ее правильно для начала, используя for (int q=7;q>=0;q--).   -  person Klitos Kyriacou    schedule 11.11.2018


Ответы (2)


Вы можете заменить конкатенацию строк += на StringBuilder. Это выделяет меньше объектов и снижает нагрузку на сборщик мусора.

int c;
StringBuilder bitString = new StringBuilder();
//read each byte from file, reverse it, add to giant bitString
//reads ALL BYTES
while ((c = fin.read()) != -1) {
    byte current = (byte) c;
    StringBuilder currentBitString = new StringBuilder();
    for (int q = 0; q < 8; q++) {
        byte bits = getBit(current, q);
        currentBitString.append(bits);
    }
    bitString.append(currentBitString.reverse());
}

Вместо того, чтобы помещать коды и данные в массив nodes, вы должны использовать здесь HashMap. Вы сравниваете код, перебирая весь массив, пока не найдете правильное совпадение. В среднем это n/2 обращений к String#equals() на элемент. С HashMap вы уменьшаете это до ~ 1.

Заполните карту данными для кодов в качестве ключей.

Map<String, Integer> nodes = new HashMap<>();
nodes.put(code, data);

Доступ к данным с карты

boolean foundCode = false;
for (int j = 0; j < bitString.length(); j++) {
    currentBitString.append(bitString.charAt(j));
    Integer data = nodes.get(currentBitString.toString().trim());
    if (data != null) {
        fw.write(data);
        foundCode = true;
    }
    if (foundCode) {
        currentBitString = new StringBuilder();
        foundCode = false;
    }
}
person Simulant    schedule 11.11.2018
comment
Разве оператор break не выходит из всего цикла и не проверяет, найден ли код? - person TheHaruWhoCodes; 11.11.2018
comment
Я попробовал это и поместил оператор break во второй цикл, но это не сработало, кажется, где-то есть бесконечный цикл - person TheHaruWhoCodes; 11.11.2018
comment
Оператор break больше не нужен, так как HashMap удалил второй цикл для перебора массива. Обновил код. Если код не на 100% правильный, я думаю, вы все равно поняли идею. - person Simulant; 11.11.2018
comment
Я понял идею и реализовал ее, но время почему-то особо не уменьшилось, по-прежнему требуется минута на декодирование 200-килобайтного файла. - person TheHaruWhoCodes; 11.11.2018
comment
@TheHaruWhoCodes: В этом случае вы должны профилировать свою программу, например. с визуалвм. Это может сказать вам, в каком методе вы провели больше всего времени. - person Simulant; 14.12.2018

  1. Не считывайте все это в память. Обрабатывайте свои коды по мере их появления. Считайте достаточно битов, чтобы декодировать следующий код, декодируйте его, сохраните неиспользуемые биты для последующего кода, повторите.

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

  3. Не выполняйте поиск по всем длинам кода, а внутри этого линейный поиск по всем кодам, чтобы найти свой код! Мне было бы трудно придумать более медленный подход. Вы должны использовать спуск по дереву или поиск по таблице для декодирования. Если вы сгенерируете канонический код Хаффмана, существует простой способ поиска, который может быть реализованы. Для примера см. puff.c. Подход из учебника (который медленнее, чем то, что делает puff.c) состоит в том, чтобы построить то же самое дерево Хаффмана на принимающей стороне и идти по этому дереву бит за битом, пока не дойдете до символа. Выпустите символ и повторите.

Вы должны быть в состоянии обработать 200 КБ сжатого ввода за несколько миллисекунд на одном ядре современного процессора.

person Mark Adler    schedule 11.11.2018