Я не специалист по сжатию, но немного разбираюсь. Поэтому решил добавить ещё немного мусора на сайт. И сделать вид что сайт обновляется.

Хаффман для самых маленьких

Постараюсь коротко и ясно. Как правило в книгах и мелких листках, которые бегло говорят о методах сжатия, пишут, что сжатие Хаффмана это некая таблица, по которой более вероятные символы заменяются более короткими кодами. Я помню когда был совсем дитём и писал на Z80, то очень долго смотрел в эту таблицу и пытался понять каким образом её составляют, но толку было не очень много. Такие обзоры обычно портят восприятие и суть методов не отображают (прямая дорога им в печку).

Статический режим

Для начала нужно вычислить вероятность появления каждого символа. На практике это можно сделать просто посчитав сколько раз и какой символ встречается в потоке (тексте) и этих счётчиков уже достаточно для прямой работы. Допустим у нас есть строка:
AACAADAACDAAABCBDBBCDC

Если вероятность появления любого символа в тексте равна еденице, то тут как вы понимаете вероятности появления символов такие:

Далее мы должны работать со списком, где символы отсортированны по убыванию вероятности. Наши действия просты, мы берем два символа в наименьшей вероятностью (два самых редких), здесь это B,D. Складываем их вероятности и получаем вероятность появления в тексте символа B или D. Заменяем эти символы одним абстрактным символом BD с вероятностью 0.35. Потом делаем тоже самое по кругу, пока все символы не кончатся. И в списке не останется общий символ с вероятностью 1.

Надеюсь вы понимаете, что на практике тоже самое делают со цельночисленными счётчиками числа символов, а не вероятностями.

Теперь код которым кодируется каждый символ можно получить просто пройдя в обратном порядке и запомнив путь. А данном случае символы мы кодировать будем так:

Наша строка будет выражена так:
1 1 01 1 1 001 1 1 01 001 1 1 1 000 01 000 001 000 000 01 001 01
Что равняется 43 битам или 6 байтам. Вместо 21 байта.

Пример конешно игрушечный, но метод работает хорошо. Такие вещи как Хаффман и Арифметическое кодирование в статическом режиме не приводят к увеличению размера кодируемых данных. Т.е. в теории и на практике, если вы сожмёте любые данные, то они будут либо меньше либо (худшем случае) размер будет таким же.

Применение фиксированой таблицы в данном случае будет ошибкой, потому как созданное дерево может иметь разную форму. Если все символы равновероятны, то коды будут иметь вид тупой таблицы из кодов с одинаковой разрядностью. А кодирование будет оставлять данные без изменения. Приблизительно так:

Адаптивный режим

Адаптивный метод не гарантирует уменьшение размеров данных в любом случае. Т.е. данные после упаковки могут вырасти. Но практике применяется именно адаптивный метод (как и в большинстве алгоритмов упаковки). Потому как в нём не нужно прилагать к упакованым байтам таблицу и он очень часто обгоняет статический метод по силе сжатия.

Простой случай

Здесь вероятности заранее не считаются. В простейшем случае, берется таблица от какого-нибудь заранее сосчитаного дерева и считается что все символы равновероятны. Далее при обработке каждого нового символа вероятность его корректируется. Попросту можно поднять символ на одну позицию вверх. Для наших ABCD начальная вероятность каждого символа P = 0.25

Встречая первые два символа (A) мы ничего не делаем. Встречая символ C мы поднимаем его на одну позицию вверх (меняем просто два соседних местами).

В итоге после кодирования вероятности будут расположены так:

А наша строка будет закодирована как

1 1 000 1 1 001 1 1 01 000 000 01 1 001 01 000 001 01 1 01 000 1

Что есть 43 бита, но теже 6 байт.

Полноценная модель

Поддержка дерева в соотвествии с текущими частотами символов. Считав чередной символ, мы записываем его код из дерева в файл. Увеличиваем счётчик данного символа на еденицу и обновляем дерево. А обновление выполняется очень просто. Необходимо подниматься от текущего символа к корню дерева, выполняя два преобразования. Таким образом будет поддерживаться правильное дерево для текуший частот символов.

В самом начале работы, я создаю полностью сбалансированное дерево из всех символов, но каждый счётчик имеет нулевое значение. Таким образом любой реальный символ текста автоматически перевешивает всё это нулевое дерево, обеспечивая для себя минимальный код. Но в тоже время сохраняется возможность добавлять другие реальные символы в дерево, без больших затрат.

В реальных условиях это не самая лучшая модель. Она довольно медленно адаптируется к потоку символов. Необходимо как-то ослаблять влияние старых символов на дерево. Например время от времени можно делить все счётчики дерева пополам. Сжатие таким образом может работать немного лучше.

Дополнения в статическому режиму

Существует возможность не составлять дерево для постройки кодов. А имитировать дерево в некотором плоском виде, просто вычислить длины кодов, которые получит каждый символ. И для них уже составить готовые коды.
ACDCABEBDDCBEBACEBDEB
Итак в начале каждому символу соотвествует некий список, единичной длины. (через двоеточие указана длина кода, через тире - частота использования).

Работает всё очень просто, мы объединяем два списка в наименьшей частотой, и увеличиваем длину каждого кода в списке на еденицу. Повторяем этот процесс, до тех пор пока все символы не сольются в один список:

В итоге получаем один список, в котором символы отсортированы по частоте использования, кроме того для каждого вычислена длина кода, которую следует использовать во время кодирования. Более редкие символы имеют более длинные коды. С одной стороны смущает что то по каждому списку приходится пробегать по многу раз, для увеличения какой-то цифры. Огранизуя работу с числами и списками чуть более эффективно - мы всё равно получим тоже самое дерево Хафмана, с другой стороны ничего страшного тут нет. Если всего 256 элементов, то по современным меркам списки не проблема.

Сами коды для хафмана очень легко получить имея только таблицу длин кодов.

void Gen(int len_array[],int codes[],int count)
{
   int n,code=0,len=1;
   for(n=0; n<count;) {
      if(len_array[n]==len) codes[n++]=code++;
      else code<<=1,len++;
   }
}

параметры:

результат с кодами:

Для того чтобы все коды удобным образом умещались в наши переменные, можно ограничить длину кодов 16-ю или 32-мя битами. Для этого надо просто перераспределить массу длин. Т.е. найти все кода которые больше 16 (или 32) и уменьшить их до 16 бит. Соотвествующим образом увеличив длины более коротких кодов, с тем чтобы сохранить общую сумму длин (увеличивать лучше длины самых редких кодов). Если длина файла не превышает 32 битного числа, то я плохо представляю что надо сделать для того чтобы один из кодов стал длинее 32 бит.

Самый интересный момент заключается в способе сохранения наших кодов в файле. Если мы упаковывали 8-битные данные и ограничивали длину кодов 16-ю битами. То на диск достаточно сохранить 16 байтов, для описания длин. Каждый из которых указывает сколько соотвествующих длин кодов есть в таблицах, в следов сохранить 256 символов отсортированных в по частоте использования (если не все коды используются, то сохранять можно меньше). Для восстановления информации о кодах всего этого будет достаточно. В случае нашей строки это будет выглядеть так. Коды еденичной длины мы не используем, у нас есть три кода длиной в два бита, и два кода длиной в три бита. Значит таблица длин и символов будет выглядеть так:

0 3 2 0 .... 0

D E B A C

Кстати говоря, вовсе не обязательно во время слияния списков помнить длину кода каждого элемента. Можно сразу вычислять общее количество длин в кодов. Для этого достаточно завести битовую маску в заголовке каждого списка, которая указывает какой длины коды есть в самом списке.

Файлы

Исходники:
Hosted by uCoz