Эффективный способ хранения дерева Хаффмана
Я пишу инструмент кодирования/декодирования Хаффмана и ищу эффективный способ хранения дерева Хаффмана, созданного для хранения внутри выходного файла.
В настоящее время есть две разные версии, которые я реализую.
- этот считывает весь файл в память символ за символом и строит таблицу частот для всего документа. Это потребует вывода дерева только один раз, и, следовательно, эффективность не так велика, кроме того, если входной файл небольшой.
- другой метод, который я использую, - это чтение куска данных размером около 64 килобайт и запуск частотного анализа, создание дерева и его кодирование. Однако в этом случае перед каждым куском мне нужно будет вывести мое дерево частот, чтобы декодер смог заново построить свое дерево и правильно декодировать закодированный файл. Это где эффективность приходит на место, так как я хочу сохранить столько места, сколько вероятный.
в моих поисках до сих пор я не нашел хорошего способа хранения дерева в как можно меньшем пространстве, я надеюсь, что сообщество StackOverflow может помочь мне найти хорошее решение!
5 ответов
поскольку вам уже нужно реализовать код для обработки немного мудрого слоя поверх вашего байтового потока / файла, вот мое предложение.
Не храните фактические частоты, они не нужны для декодирования. Однако вам нужно настоящее дерево.
Итак, для каждого узла, начиная с root:
- If leaf-node: вывод 1-бит + N-битный символ / байт
- если не leaf-node, выведите 0-бит. Затем кодируйте оба дочерних узла (сначала слева правильно) таким же образом
читать, сделайте следующее:
- читать немного. Если 1, то прочитайте N-битный символ / байт, верните новый узел вокруг него без детей
- если бит равен 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 символов unencoded, вы сохраните 8 байтов.
конечный результат, включая дерево для начала, выглядит следующим образом. Каждый символ в потоке (A-E) кодируется как 8 бит, тогда как 0 и 1-это всего лишь один бит. Пространство в потоке просто отделяет дерево от закодированных данных и не занимает места в конечном выходе.
001A1C01E01B1D 0000000000001100101010101011111111010101010
для конкретного примера, который у вас есть в комментариях, AABCDEF, вы получите следующее:
вход: AABCDEF
частоты:
- A: 2
- B: 1
- C: 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
- C: 100
- D: 101
- E: 110
- F: 111
дерево: 001A1B001C1D01E1F = 59 бит
Данные: 000001100101110111 = 18 бит
Сумма: 59 + 18 = 77 бит = 10 байт
так как оригинал был 7 символов 8 бит = 56, у вас будет слишком много накладных расходов такого малого часть данных.
Если у вас есть достаточный контроль над поколением дерева, вы можете сделать его каноническим деревом (таким же образом сдуется, например), который в основном означает, что вы создаете правила для разрешения любых неясных ситуациях при построении дерева. Затем, как и 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 для выкачивания не так уж плохо, если вы уже знаком с кодировкой Хаффмана:http://www.ietf.org/rfc/rfc1951.txt
ветви 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" в качестве индикатора, чтобы просто повторно использовать дерево из предыдущего куска.
дерево обычно создается из таблицы частот байтов. Поэтому сохраните эту таблицу или просто сами байты, отсортированные по частоте, и воссоздайте дерево на лету. Это, конечно, предполагает, что вы строите дерево для представления одиночных байтов, а не больших блоков.
обновление: как указал j_random_hacker в комментарии, вы на самом деле не можете этого сделать: вам нужны сами значения частоты. Они объединены и "пузырь" вверх, как вы строите дерево. на этой странице описывает способ построения дерева из таблицы частот. В качестве бонуса он также сохраняет этот ответ от удаления, упомянув способ сохранения дерева:
самый простой способ вывести само дерево Хаффмана-это, начиная с корня, сбросить сначала левую сторону, а затем правую сторону. Для каждого узла выводится 0, для каждого листа выводится 1, за которым следует N битов, представляющих значение.
лучше
дерево: Семь ------------- | 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 имеет очень малый размер (даже если ваши коды Huffman могут быть 32-битным, он будет содержать только 32 строки), и на самом деле первый шаблон всегда равен нулю, вы можете полностью игнорировать его при выполнении двоичного поиска шаблонов в нем (здесь нужно будет сравнить только 1 шаблон, чтобы узнать, является ли битовая глубина 2 или 3 и получить первый индекс, при котором связанные данные хранятся в векторе). В нашем примере вам нужно будет выполнить быстрый двоичный поиск входных шаблонов в пространстве поиска не более 31 значения, т. е. максимум 5 целочисленных сравнений. Эти 31 процедуры сравнения можно оптимизировать в 31 коде, чтобы избежать всех циклов и управлять состояниями при просмотре дерева поиска целых двоичных чисел. Вся эта таблица вписывается в небольшую фиксированную длину (LUT просто нуждается в 31 строке atmost для кодов Хаффмана не длиннее 32 бит, а 2 других столбца выше заполнят не более 32 строк).
другими словами, LUT выше требует 31 ints 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(30times)]. Для поиска в нем вы должны найти позицию, в которой шаблон входных битов находится между двумя шаблонами: он должен быть ниже шаблона в следующей позиции в этом LUT, но все же выше или равен шаблону в текущей позиции (если обе позиции содержат один и тот же шаблон, текущая строка не будет совпадать, входной шаблон подходит ниже). Затем вы разделите и завоюете, и будете использовать 5 тесты не более (для двоичного поиска требуется один код с 5 вложенными уровнями if/then/else, он имеет 32 ветви, достигнутая ветвь указывает непосредственно на битовую глубину, которую не нужно хранить; вы выполняете один непосредственно индексированный поиск во вторую таблицу для возврата первого индекса; вы производите дополнительный конечный индекс в векторе декодированных значений).
как только вы получите позицию в таблице поиска (Поиск в 1-м столбце), у вас сразу же будет номер из битов, которые нужно взять с входа, а затем начать индекс вектора. Разрядность вас могут быть использованы для вывода непосредственно скорректированный индекс, основной bitmasking после вычитания первого индекса.
in summary: никогда не храните связанные двоичные деревья, и вам не нужен никакой цикл для выполнения thelookup, который просто требует 5 вложенных шаблонов сравнения ifs на фиксированных позициях в таблице 31 шаблонов и таблице 31 ints, содержащей начальное смещение в векторе декодированного значения (в первой ветви вложенных тестов if/then/else подразумевается начальное смещение вектора, оно всегда равно нулю; это также Самая частая ветвь, которая будет принята, поскольку она соответствует кратчайшему коду, который для наиболее частых декодированных значений).