Можно изменить алгоритм кодирования так, чтобы он выполнялся за один проход и не требовал информации о структуре дерева:
Псевдокод не вполне точен. В частности, в нем не указано условие прекращения декодирования.
Инициализировать дерево кодирования можно исходя из некоторого предположения о частотах повторений различных символов. Можно добавить к 256 символам еще два - EOF (код 256) и CHR (код 257) и включить в дерево только их. Другие символы будут включены в дерево после их первого появления в кодируемом файле. Соответственно, при первом появлении символа в выходной файл записывается код CHR и сам символ (восемь бит), при последующих появлениях этого символа в выходной файл записывается его код (символ уже включен в дерево и может быть закодирован). Выходной файл завершается кодом EOF. При декодировании появлене кода EOF является условием завершения.
Разумеется, при кодировании и декодировании должны использоваться одни и те же функции инициализации и перестроения дерева. Получаемый код не будет кодом Хаффмана, поскольку коды символов могут меняться по ходу кодирования. Вопрос о том, лучше он или хуже чем код Хаффмана будет рассмотрем ниже. Очевидно, для коротких сообщений он лучше, для длинных - не обязательно.
В принципе можно после считывания каждого символа строить дерево Хаффмана заново, это потребует весьма значительных затрат времени, но работать будет. Для начала так и сделаем, затем заменим процедуры Insert и Update более быстродействующими. Весь прочий код изменяться не будет. Вот полный текст программы, он получен путем незначительной правки статического алгоритма. Собственно алгоритм начинается с первого define - возможности выполнять сборку программы из нескольких файлов в DOS-версии компилятора не будет, в Windows-версии она есть, но нет некоторых других возможностей, так что пока следует использовать DOS-компилятор версии 1.1 или 1.2:
В приведенной программе дерево кодирования строится заново после обработки каждого символа и это занимает очень много времени. Более внимательное рассмотрение свойств опимального дерева кодирования приводит к существенно лучшим алгоритмам. В дереве Хаффмана более часто встречающимся символам соответствуют более короткие коды. Для любых двух узлов A и B верно следующее:
Na и Nb - веса узлов, La и Lb - длины кодов. Из предположения обратного следует уменьшение веса дерева при перестановке A и B, что противоречит оптимальности:
Таким образом, соотношение между весами и длинами кодав является необходимым условием оптимальности. Но также оно является достаточным условием, т.е. из его истинности следует оптимальность дерева. Доказательство аналогично доказательству оптимальности дерева Хаффмана. Пусть A - самый редкий символ сообщения. Из соотношения весов и длин следует, что либо длина La его кода максимальна, либо существует символ C с весом Nc=Na и длиной Lc>La. В последнем случае переставим узлы A и С. Действительно, для любого символа C c весом Nc>Na длина кода Lc<=La (соотношение весов и длин кодов). Символ B - следующий после A по весу, Nb>=Na. Если допустить, что Lb<La, то существует символ D c весом Nd>=Nb и длиной Ld=La>Lb. Если Nd>Nb, приходим к противоречию, остается вариант Nd=Nb и после перестановки B и D придем к Lb=La и кодам A и B отличающимся только последним битом.
Предположим, дерево T, для которого выполнено соотношение весов и длин кодов неоптимально. Т.е. существует дерево T' с весом W(T')<W(T).
В обоих деревьях коды символов A и B имеют максилальную длину и различаются последним битом. Заменим алфавит A, B -> Z, Nz=Na+Nb. Веса обоих деревьев кодирования уменьшатся на одинаковую величину Na+Nb бит, соотношение весов и длин кодов не нарушится. Продолжая процесс слияния самых редких символов придем к тому, что дерево кодирования для алфавита из двух символов неоптимально. Противоречие доказывает оптимальность дерева T.
Пусть имеется уже построенное оптимальное дерево и вес одного из символов (A) нужно увеличить на единицу. Простое увеличение веса соответствующего символу узла и всех лежащих выше узлов может нарушить оптимальность. Пусть Na - вес символа (до увеличения), La - длина кода. Nb и Lb - вес и длина некоторого другого узла (не обязательно символа). Перестановка узлов A и B приведет к следующему измененению веса дерева:
вес дерева уменьшится лишь при равенстве весов A и B и при меньшей длине кода B. Этой перестановки недостаточно для восстановления оптимальноти дерева, но она дает ключ к построению алгоритма.
Также заметим, что перестановка двух равных по весу поддеревьев оптимального дерева не меняет его веса и, следовательно, не нарушает указанного выше соотношения весов и длин кодов (обратное противоречит оптимальности дерева). Сделаем следующее:
Все эти перестановки не нарушают оптимальности дерева. Затем увеличим на единицу веса всех узлов на пути от корня до A. Такое увеличение не нарушит соотношение между весами узлов и длинами кодов, поскольку всем узлам соответствуют коды минимальной возможной для их весов длины. Следовательно, полученное дерево будет оптимальным.
Для начала реализуем все это в виде двух рекурсивных процедур Locate и Update. Первая из них обходит дерево и находит узел с заданным весом и самым коротким кодом, вторая реализует привденный алгоритм. Процедура Insert стала более сложной - если старая процедура только регистрировала новый символ, то новая вставляет в дерево узел с нулевым весом (поскольку длина его кода максимальна, это не нарушает соотношение весов и длин кодов):
Можно сделать еще лучше. Поскольку выполнено соотношение весов и длин кодов, узлы дерева кодирования могут быть одновременно упорядочены по уменьшению веса и увеличению длины кода. Действительно, ничто не мешает упорядочить узлы с одинаковыми весами по увеличению длины кода, коды узлов с меньшими весами как минимум не короче кодов узлов с большими весами. Используя упорядоченный массив поиск узла с заданным весом и самым коротким кодом можно выполнять быстрее:
И наконец, рекурсивную процедуру обновления можено заменить циклической:
Такой способ обновления дерева кодирования был предложен Робертом Галлагером в 1978 году. Стоит отметить, что все три способа перестроения дерева приводят к немного разным результатам. Общее лишь то, что на каждом шаге колирования деревья оптимальны.
Теперь о длине кода. Можно создать такое сообщение, каждый байт которого будет закодирован не менее чем девятью битами. Поскольку всего кодов 258, в дереве всегда есть коды длиной более 8 бит. Следующая процедура создает сообщение, начинющееся с 256 различных символов, за которыми следую следуют символы с максимально длинными кодами:
Таким образом, адаптивное кодирование может быть хуже статического - в худшем случае статический код длиннее исходного сообщения на длину таблицы частот, адаптивный код - как минимум, на 12.5%.