Пусть сообщение M составлено из символов ASCII. Эти символы могут быть представлены кодами фиксированной длины (8 бит). Допустим, числа повторений символов A, B, C и D - Na, Nb, Nc и Nd соответственно - удовлетворяют неравенству
Число повторений символа B произвольно. Назначим этим символам следующие коды:
A: | 00000000 |
B: | 00000001 |
C: | 00000010 |
D: | 00000011 |
Оставшиеся 252 кода произвольно распределим между оставшимися символами алфавита. Далее сделаем следующую замену кодов:
A: | 0000000 |
B: | 00000010 |
C: | 000000110 |
D: | 000000111 |
В результате получим экономию в Na-(Nc+Nd) бит (на самом деле меньше, т.к. вместе с сообщением надо передавать таблицу кодов). Проблем с декодированием не возникает - ни один из кодов не является префиксом другого. Декодер довольно прост, о нем чуть ниже.
Алгоритм Хаффмана основан на этой идее с той лишь разницей, что она применяется ко всем символам алфавита. Идея алгоритма - свести задачу к аналогичной с меньшим числом символов. Число символов уменьшается за счет объединения их в пары (C и D в приведенном примере). Легко установить, что начинать можно не с любых символов - для этого рассмотрим еще одно сообщение из трех символов A, B и C, повторяющихся 1, 2 и 3 раза соответственно. Вариантов объединения здесь всего три - (A и B), (A и C) и (B и C), их перебор показывает, что к наилучшему результату приводит объединение в пару символов A и B. Далее нужно попытаться доказать, что если объединять в пары самые редкие символы, то результат будет наилучшим. Но сначала детально рассмотрим алгоритм.
На первом шаге определяются числа повторений (веса) всех символов. На втором шаге строится дерево кодирования. Используются две структуры данных - массив и дерево, в массив заносятся все символы и их веса, дерево пусто. Затем ищутся два самых редких символа, исключаются из массива и добавляются в дерево (листья). Также создается новый узел дерева с весом равным сумме весов найденных символов и содержащий ссылки на эти листья. Этот узел добавляется в массив. Процедура повторяется пока в массиве не останется один элемент. Третий шаг - собственно кодирование.
Рассмотрим еще один пример - сообщение составленное из шести символов A, B, C, D, E и F со следующими числами повторений:
A | B | C | D | E | F |
60 | 25 | 30 | 5 | 10 | 20 |
Возьмем два узла с самыми маленькими весами (D и E), образуем из них новый узел с весом 15 и добавим его в список. Узлы D и E исключим:
A | B | C | F | |||
60 | 25 | 30 | 20 | 15 | ||
/ | \ | |||||
E | D | |||||
10 | 5 |
Возьмем два узла с наименьшими весами (только что созданный и F), образуем из них новый узел с весом 35 и добавим его в список. Взятые узлы исключим:
A | B | C | ||||
60 | 25 | 30 | 35 | |||
/ | \ | |||||
| | 15 | |||||
| | / | \ | ||||
F | E | D | ||||
20 | 10 | 5 |
Возьмем два узла с наименьшими весами (B и C), образуем из них новый узел с весом 55 и добавим его в список. Взятые узлы исключим:
A | |||||||
60 | 35 | 55 | |||||
/ | \ | / | \ | ||||
| | 15 | | | | | ||||
| | / | \ | | | | | |||
F | E | D | C | B | |||
20 | 10 | 5 | 30 | 25 |
Возьмем два узла с наименьшими весами (35 и 55), образуем из них новый узел с весом 90 и добавим его в список. Взятые узлы исключим:
A | ||||||||
60 | – | 90 | – | |||||
/ | \ | |||||||
55 | 35 | |||||||
/ | \ | / | \ | |||||
| | | | | | 15 | |||||
| | | | | | / | \ | ||||
C | B | F | E | D | ||||
30 | 25 | 20 | 10 | 5 |
Возьмем два узла с наименьшими весами (A и 90) и образуем из них новый узел с весом 150 и добавим его в список. Взятые узлы исключим. В списке остался один узел - дерево построено:
– | – | 150 | – | – | |||||
/ | \ | ||||||||
– | 90 | – | | | ||||||
/ | \ | | | |||||||
55 | 35 | | | |||||||
/ | \ | / | \ | | | |||||
| | | | | | 15 | | | |||||
| | | | | | / | \ | | | ||||
C | B | F | E | D | A | ||||
30 | 25 | 20 | 10 | 5 | 60 |
Чтобы получить код Хаффмана для любого из символов нужно пройти от корня дерева к соответствующему этому символу листу, каждый переход влево - 0, вправо - 1. Так получим все биты кода. Реально приходится идти в обратном направлении - от листа к корню. Процедура получения кода похожа на преобразование числа в строку - последний символ строки получается первым. В этом примере коды следующие:
A: | 1 |
B: | 001 |
C: | 000 |
D: | 0111 |
E: | 0110 |
F: | 010 |
Нужно отметить, что длина кода символа может превзойти длину простой переменной (16 или 32 бит), но мы и не будем сохранять их в простых переменных.
Декодирование символа начинается с корня дерева. Сообщение читается по одному биту, если прочитан 0 - переход по дереву влево, если 1 - вправо. По достижению листа определяется конец кода символа и сам этот символ.
Алгоритм Хаффмана дает самый короткий код среди всех кодов, представляющих каждый символ сообщения фиксированной последовательностью бит. Доказательства этого факта строится на следующем свойстве оптимального кода - этот код может быть преобразован так, что два наиболее редких символа A и B будут представлены самыми длинными кодами, причем эти коды будут отличаться лишь младшим битом. Без ограничения общности Na - число повторений символа A не больше Nb - числа поторений символа B. Допустим, что код символа С, встречающегося Nc>Na раз имеет длину Lc>La. Тогда после обмена кодов A и C получим более короткий код:
Это противоречит предположению об оптимальности кода и, следовательно, Lc не может превосходить La. Если же Nc=Na и Lc>La, просто обменяем коды этих символов. Из предположения о том, что Lb меньше La следует, что есть символ D встречающийся Nd>=Nb раз, длина кода которого равна La. Предположение Nd>Nb противоречит оптимальности, поэтому Nd=Nb. Сделав одну замену можно получить оптимальный код код, в котором A и B различаются лишь последним битом.
Обозначим C(N) алфавит из N символов, M(C) - сообщение, образованное из символов этого алфавита, T(M) -дерево кодирования и W(T) - вес дерева T, т.е. сумму по всем символам произведений чисел повторений символа на длину кода этого символа.
Допустим, что дерево Хаффмана T(M) неоптимально, т.е. существует некоторое дерево T'(M), вес которого меньше веса дерева Хаффмана:
Пусть A и В - два наиболее редких символа сообщения M, которые были объединены в пару на первом шаге построения кода Хаффмана. Их числа повторений Na и Nb соответственно. Дерево T' может быть преобразовано в равное по весу дерево, в котором коды этих символов различаются лишь младшим битом. Заменим все символы A и B новым (отсутстующим в алфавите C) символом Z. В результате получим новый алфавит C'(N-1) и новое сообщение M'(C'). Число повторений символа Z в новом сообщении Nz=Na+Nb. Из деревьев T и T' исключим листья, соответствующие символам A и В, а их общий узел поставим в соответствие символу Z. Получим деревья T''(M') и T'''(M'), T'' - дерево Хаффмана для сообщения M'. Поскольку такие преобразования уменьшают веса обоих деревьев на одинаковую величину Nz (код символа Z на один бит короче кода символа A), дерево T'' также неоптимально:
Продолжая процесс слияния самых редких символов придем к тому, что дерево Хаффмана для сообщения из алфавита C(2) неоптимально. Но это не так - каждый символ этого сообщения представляется одним битом. Противоречие доказывает оптимальность кода Хаффмана. Но это вовсе не означает, что алгоритм Хаффмана - лучший алгоритм сжатия. Это только лучший алгоритм кодирования, а сжатие - это не только кодирование.
Это не единственный способ посторения кода Хаффмана. Ниже рассмотрен совершенно иной алгоритм построения дерева Хаффмана, также неочевидный, но возможно более понятный.
Вот текст программы. Для доступа к файлам написаны функции побитовой записи/чтения (Save/Load). Информация о кодах символов в архиве представлена в виде таблицы чисел повторений - это не самый экономный способ, но зато самый простой. Содержательная часть начинается с первого define - на весь предшествующий текст можно не обращать внимания, в DOS-версии Context'а нет возможности сборки программы из модулей:
Рассмотренный алгоритм имеет два недостатка. Во-первых, он требует двух проходов по кодируемому тексту, во-вторых, в выходной файл должна быть записна информация о структуре дерева кодирования (что увеличивает размер файла).
Нужно отметить, что для хранения дерева кодирования используется массив Tree, состоящий из 515 элементов, но достаточно 511 элементов - на каждом шаге в дерево добавляется два, один или ни одного листа и один дополнительный узел, список узлов сокращается на единицу. Всего шагов 255, столько дополнительных узлов нужно, листов 256 - по одному на каждый символ. Дополнительные четыре элемента будут использованы в при рассмотрении адаптивной версии алгоритма.