Оптимальное хранение словаря в алгоритме Хаффмана
При сжатии данных алгоритмом Хаффмана появляется проблема передачи оптимального кода зашифрованного сообщения, который поможет получателю расшифровать его. Рассмотрим некоторые варианты решения этой задачи.
Содержание
Постановка задачи
Пусть у нас есть алфавит
, , и код , сопоставляющий каждому символу его код .Нужно придумать эффективное представление кода
в памяти.Простое решение
Заметим, что
. Дополним все коды до длины нулями и запишем их друг за другом. Также необходимо передать . При условии, что не превышает получаем дополнительных бит на передачу префиксного кода.Чтобы декодировать полученную информацию о коде, достаточно сначала узнать
и записать в префиксное дерево коды всех символов. Построив соответствующее дерево, можно заметить, что некоторые вершины имеют одного ребенка — такие вершины получены в результате дополнения нашего оптимального кода до бит нулями. Все такие вершины можно просто удалить из дерева, и мы получим оптимальный префиксный код, который мы передавали.Пример
Для примера возьмем слово "миссисипи". Тогда код
будет выглядеть следующим образом:Символ | и | м | п | с |
---|---|---|---|---|
Код | 0 | 100 | 101 | 11 |
При помощи нашего алгоритма
будет закодирован как:0000 1000 1010 1100
Построим префиксное дерево по полученному коду:
Удалив вершины, являющиеся единственными детьми у других вершин, получим зашифрованное префиксное дерево:
Эффективное решение
Построим префиксное дерево, соответствующее нашему коду
. Наша задача состоит из двух подзадач: передача структуры этого дерева и передача информации для различения листьев.Передача структуры дерева
Обойдем дерево, используя обход в глубину. Каждый раз, проходя по ребру запишем одну из букв , или , в зависимости от того, куда по ребру мы прошли ( - в левого ребенка, - в левого ребенка, - в родителя). Эта информация поможет нам восстановить дерево.
Построим обход дерева Хаффмана для слова "миссисипи":
LURLLURUURUU
Первая модификация
Заметим, что на самом деле мы можем объединить ребра типа
и в ребро типа , которое значит, что мы спускаемся вниз, а в которого ребенка — в левого или в правого, можно определить на месте.Модифицируем наш обход:
DUDDDUDUUDUU
Вторая модификация
Модифицируем значение команды
. Пусть теперь символ значит, что мы поднимаемся к предку текущей вершины, пока мы — правый ребенок или пока не достигли корня, и если мы, остановившись, пришли из левого сына вершины, перейдем по ребру в правого ребенка. После такой модификации записывая каждый символ мы ровно по одному ребру проходим в первый раз.Конечный вариант кодировки дерева:
DUDDUUU
Распишем подробнее:
D | Спускаемся влево |
U | Поднимаемся и спускаемся вправо |
D | Спускаемся влево |
D | Спускаемся влево |
U | Поднимаемся и спускаемся вправо |
U | Поднимаемся, поднимаемся и спускаемся вправо |
U | Поднимаемся до корня |
Оценим используемое количество памяти. Так как в нашем дереве
листьев, то в нем (это легко показать из алгоритма Хаффмана, в нем итерация и на каждой в дерево добавляется по 2 ребра), а символов в нашей записи будет , так как на каждое ребро приходится по символу плюс последний, терминальный, .Передача информации для восстановления листьев
Сопоставим каждому символу алфавита равномерный код
длины - его порядковый номер в алфавите, записанный в двоичной системе счисления. Тогда, если выписать подряд коды для всех символов в том порядке, в котором обход в глубину посещает соответствующие им листы, несложно восстановить, какому символу какой код соответствует.Используемая память
Итого, задача решена с использованием
бит.