Эффективный способ хранения дерева Хаффмана - PullRequest
30 голосов
/ 17 апреля 2009

Я пишу инструмент кодирования / декодирования Хаффмана и ищу эффективный способ хранения дерева Хаффмана, созданного для хранения внутри выходного файла.

В настоящее время я реализую две разные версии.

  1. Этот код считывает весь файл в память символ за символом и создает таблицу частот для всего документа. Для этого потребуется только один раз вывести дерево, и, следовательно, эффективность не так уж важна, за исключением случаев, когда входной файл мал.
  2. Другой метод, который я использую, - это чтение фрагмента данных размером около 64 килобайт и выполнение анализа частоты по нему, создание дерева и его кодирование. Однако в этом случае перед каждым фрагментом мне нужно будет вывести свое частотное дерево, чтобы декодер смог перестроить свое дерево и правильно декодировать закодированный файл. Именно здесь эффективность достигается, поскольку я хочу сэкономить как можно больше места.

В моих поисках до сих пор я не нашел хорошего способа сохранить дерево как можно меньше, я надеюсь, что сообщество StackOverflow может помочь мне найти хорошее решение!

Ответы [ 5 ]

72 голосов
/ 17 апреля 2009

Поскольку вам уже нужно реализовать код для обработки побитового слоя поверх вашего организованного байта потока / файла, вот мое предложение.

Не храните фактические частоты, они не нужны для декодирования. Вам, однако, нужно фактическое дерево.

Так для каждого узла, начиная с корня:

  1. Если конечный узел: вывод 1-бит + N-битный символ / байт
  2. Если не конечный узел, вывести 0-битный. Затем закодируйте оба дочерних узла (сначала слева, затем справа) одинаково

Чтобы прочитать, сделайте это:

  1. Читать бит. Если 1, то читать N-битный символ / байт, вернуть вокруг него новый узел без дочерних элементов
  2. Если бит равен 0, декодировать левый и правый дочерние узлы одинаково, и возвращать вокруг них новый узел с этими дочерними элементами, но без значения

Конечный узел - это, по сути, любой узел, у которого нет дочерних элементов.

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

Псевдокод для расчета:

Tree-size = 10 * NUMBER_OF_CHARACTERS - 1
Encoded-size = Sum(for each char,freq in table: freq * len(PATH(char)))

При расчете размера дерева учитываются листовые и неконечные узлы, а встроенного узла на один меньше, чем символов.

SIZE_OF_ONE_CHARACTER будет числом бит, и эти два дадут вам общее количество бит, которое мой подход к дереву + кодированные данные будет занимать.

PATH (c) - это функция / таблица, которая выдала бы битовый путь от корня до этого символа в дереве.

Вот псевдокод для поиска на C #, который предполагает, что один символ является простым байтом.

void EncodeNode(Node node, BitWriter writer)
{
    if (node.IsLeafNode)
    {
        writer.WriteBit(1);
        writer.WriteByte(node.Value);
    }
    else
    {
        writer.WriteBit(0);
        EncodeNode(node.LeftChild, writer);
        EncodeNode(node.Right, writer);
    }
}

Чтобы прочитать это обратно:

Node ReadNode(BitReader reader)
{
    if (reader.ReadBit() == 1)
    {
        return new Node(reader.ReadByte(), null, null);
    }
    else
    {
        Node leftChild = ReadNode(reader);
        Node rightChild = ReadNode(reader);
        return new Node(0, leftChild, rightChild);
    }
}

Пример (упрощенный, использовать свойства и т. Д.) Реализация узла:

public class Node
{
    public Byte Value;
    public Node LeftChild;
    public Node RightChild;

    public Node(Byte value, Node leftChild, Node rightChild)
    {
        Value = value;
        LeftChild = leftChild;
        RightChild = rightChild;
    }

    public Boolean IsLeafNode
    {
        get
        {
            return LeftChild == null;
        }
    }
}

Вот пример вывода из конкретного примера.

Ввод: AAAAAABCCCCCCDDEEEEE

Частота:

  • A: 6
  • B: 1
  • C: 6
  • D: 2
  • E: 5

Каждый символ составляет всего 8 бит, поэтому размер дерева будет 10 * 5 - 1 = 49 бит.

Дерево может выглядеть так:

      20
  ----------
  |        8
  |     -------
 12     |     3
-----   |   -----
A   C   E   B   D
6   6   5   1   2

Таким образом, пути к каждому символу следующие (0 слева, 1 справа):

  • A: 00
  • B: 110
  • C: 01
  • D: 111
  • E: 10

Итак, чтобы рассчитать выходной размер:

  • A: 6 вхождений * 2 бита = 12 бит
  • B: 1 вхождение * 3 бита = 3 бита
  • C: 6 вхождений * 2 бита = 12 бит
  • D: 2 вхождения * 3 бита = 6 бит
  • E: 5 вхождений * 2 бита = 10 бит

Сумма закодированных байтов составляет 12 + 3 + 12 + 6 + 10 = 43 бита

Добавьте это к 49 битам из дерева, и результат будет 92 бита или 12 байтов. Сравните это с 20 * 8 байтами, необходимыми для хранения исходных 20 символов без кодировки, вы сэкономите 8 байтов.

Окончательный вывод, включая начальное дерево, выглядит следующим образом. Каждый символ в потоке (A-E) кодируется как 8 битов, тогда как 0 и 1 - это просто один бит. Пространство в потоке просто отделяет дерево от закодированных данных и не занимает места в конечном выводе.

001A1C01E01B1D 0000000000001100101010101011111111010101010

Для конкретного примера, который у вас есть в комментариях, AABCDEF, вы получите это:

Ввод: AABCDEF

Частота:

  • A: 2
  • B: 1
  • С: 1
  • D: 1
  • E: 1
  • F: 1

Дерево:

        7
  -------------
  |           4
  |       ---------
  3       2       2
-----   -----   -----
A   B   C   D   E   F
2   1   1   1   1   1

Дорожка:

  • A: 00
  • B: 01
  • С: 100
  • D: 101
  • E: 110
  • F: 111

Дерево: 001A1B001C1D01E1F = 59 бит
Данные: 000001100101110111 = 18 бит
Сумма: 59 + 18 = 77 бит = 10 байтов

Так как оригинал состоял из 7 символов по 8 бит = 56, у вас будет слишком много служебных данных для таких маленьких фрагментов данных.

10 голосов
/ 03 июня 2009

Если у вас есть достаточный контроль над генерацией дерева, вы можете заставить его создать каноническое дерево (как, например, DEFLATE ), что в основном означает, что вы создаете правила для разрешения любых неоднозначных ситуаций, когда строим дерево. Затем, как и DEFLATE, все, что вам на самом деле нужно хранить, это длины кодов для каждого символа.

То есть, если у вас есть дерево / коды Лассе, упомянутые выше:

  • A: 00
  • B: 110
  • C: 01
  • D: 111
  • E: 10

Тогда вы можете хранить их как: 2, 3, 2, 3, 2

И этой информации на самом деле достаточно для восстановления таблицы Хаффмана, при условии, что вы всегда используете один и тот же набор символов - скажем, ASCII. (Это означает, что вы не можете пропускать буквы - вам нужно будет указать длину кода для каждого, даже если он равен нулю.)

Если вы также наложите ограничение на длину в битах (скажем, 7 бит), вы можете сохранить каждое из этих чисел, используя короткие двоичные строки. Таким образом, 2,3,2,3,2 становится 010 011 010 011 010 - который помещается в 2 байта.

Если вы хотите сделать действительно сумасшедшим, вы можете сделать то, что делает DEFLATE, и создать еще одну таблицу Хаффмана с длинами этих кодов, и заранее сохранить длины кодов. Тем более, что они добавляют дополнительные коды для «вставки ноля N раз подряд», чтобы еще больше сократить время.

RFC для DEFLATE неплох, если вы уже знакомы с кодированием Хаффмана: http://www.ietf.org/rfc/rfc1951.txt

4 голосов
/ 17 апреля 2009

ветви - 0, листья - 1. Сначала пройдитесь по глубине дерева, чтобы получить его "форму"

e.g. the shape for this tree

0 - 0 - 1 (A)
|    \- 1 (E)
  \
    0 - 1 (C)
     \- 0 - 1 (B)
         \- 1 (D)

would be 001101011

Следуйте этому с битами для символов в той же глубине первого порядка AECBD (при чтении вы будете знать, сколько символов ожидать от формы дерева). Затем выведите коды для сообщения. Затем у вас есть длинный ряд битов, которые вы можете разделить на символы для вывода.

Если вы разбиваете его на части, вы можете проверить, что сохранение дерева для следующего фрагмента так же эффективно, как повторное использование дерева для предыдущего фрагмента и иметь форму дерева «1» в качестве индикатора для повторного использования предыдущий кусок.

2 голосов
/ 17 апреля 2009

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

ОБНОВЛЕНИЕ : Как отметил j_random_hacker в комментарии, вы на самом деле не можете этого сделать: вам нужны сами значения частоты. Они объединяются и «всплывают» вверх, когда вы строите дерево. Эта страница описывает способ построения дерева из таблицы частот. В качестве бонуса он также сохраняет этот ответ от удаления, упомянув способ сохранения дерева:

Самый простой способ вывести само дерево Хаффмана, начиная с корня, сбрасывать сначала левую, а затем правую стороны. Для каждого узла выведите 0, для каждого листа выведите 1, за которым следуют N битов, представляющих значение.

0 голосов
/ 15 августа 2013

лучший подход

Дерево: 7 ------------- | 4 | --------- 3 2 2 ----- ----- ----- A B C D E F 2 1 1 1 1 1: частоты 2 2 3 3 3 3: глубина дерева (биты кодирования)

Теперь просто выведите эту таблицу: глубина количество кодов


 2   2 [A B]
 3   4 [C D E F]

Вам не нужно использовать одно и то же двоичное дерево, просто сохраняйте вычисленную глубину дерева, то есть количество битов кодирования. Так что просто держите вектор несжатых значений [A B C D E F] упорядоченным по глубине дерева, вместо этого используйте относительные индексы для этого отдельного вектора. Теперь воссоздайте выровненные битовые комбинации для каждой глубины:

глубина количество кодов


 2   2 [00x 01x]
 3   4 [100 101 110 111]

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

first pattern depth first index
------------- ----- -----------
000           2     0
100           3     2

Этот LUT имеет очень маленький размер (даже если ваши коды Хаффмана могут иметь длину 32 бита, он будет содержать только 32 строки), и фактически первый шаблон всегда равен нулю, вы можете полностью его игнорировать при выполнении двоичного кода. поиск шаблонов в нем (здесь необходимо сравнить только 1 шаблон, чтобы узнать, равна ли битовая глубина 2 или 3, и получить первый индекс, по которому соответствующие данные сохраняются в векторе). В нашем примере вам потребуется выполнить быстрый двоичный поиск шаблонов ввода в области поиска максимум из 31 значения, то есть максимально 5 целочисленных сравнений. Эти 31 процедура сравнения могут быть оптимизированы в 31 коде, чтобы избежать всех циклов и необходимости управлять состояниями при просмотре целочисленного двоичного дерева поиска. Вся эта таблица умещается в небольшую фиксированную длину (для кодов Хаффмана требуется только 31 строка максимум для кодов Хаффмана, длина которых не превышает 32 бита, а 2 других столбца выше заполнят максимум 32 строки).

Другими словами, приведенная выше таблица LUT требует 31 дюйм 32-битного размера каждый, для хранения значений глубины в битах 32 байта, но этого можно избежать, если использовать столбец глубины (и первую строку для глубины 1):

first pattern (depth) first index
------------- ------- -----------
(000)          (1)    (0)
 000           (2)     0
 100           (3)     2
 000           (4)     6
 000           (5)     6
 ...           ...     ...
 000           (32)    6

Итак, ваш LUT содержит [000, 100, 000 (30 раз)]. Для поиска в нем вы должны найти позицию, в которой шаблон входных битов находится между двумя шаблонами: он должен быть ниже, чем шаблон в следующей позиции в этом LUT, но все же выше или равен шаблону в текущей позиции (если обе позиции содержат тот же шаблон, текущая строка не будет соответствовать, шаблон ввода соответствует ниже). Затем вы разделите и победите и будете использовать не более 5 тестов (для бинарного поиска требуется один код с 5 встроенными уровнями, вложенными в / затем / иначе, он имеет 32 ветви, достигнутая ветвь указывает непосредственно на разрядность, которая не должны быть сохранены; затем вы выполняете один непосредственный индексированный поиск во второй таблице для возврата первого индекса; вы аддитивно выводите конечный индекс в векторе декодированных значений).

Как только вы получите позицию в таблице поиска (поиск в 1-м столбце), у вас сразу же будет количество битов, которые нужно взять со входа, а затем начальный индекс для вектора. Полученную битовую глубину можно использовать для непосредственного получения скорректированной позиции индекса с помощью базовой маскировки после вычитания первого индекса.

В итоге: никогда не храните связанные двоичные деревья, и вам не нужен какой-либо цикл для выполнения lookup, который просто требует 5 вложенных if, сравнивающих шаблоны в фиксированных позициях в таблице из 31 шаблона и в таблице из 31-го числа, содержащей начало смещение в векторе декодированных значений (в первой ветви вложенных тестов if / then / else подразумевается начальное смещение вектора, оно всегда равно нулю; это также самая частая ветвь, которая будет принята при совпадении самый короткий код, который предназначен для наиболее частых декодированных значений).

...