В основе всех методов сжатия лежит простая идея: если представлять часто используемые элементы короткими кодами, а редко используемые - длинными кодами, то для хранения блока данных требуется меньший объем памяти, чем если бы все элементы представлялись кодами одинаковой длины. Данный факт известен давно: вспомним, например, азбуку Морзе, в которой часто используемым символам поставлены в соответствие короткие последовательности точек и тире, а редко встречающимся - длинные.
Точная связь между вероятностями и кодами установлена в
$$H = - \sum\limits_i {p(s_i ) \cdot \log _2 p(s_i )}$$
Это значение также называется F
или
Обычно вероятность появления элемента является условной, т.е. зависит от какого-то
события. В этом случае при кодировании очередного элемента $${\rm{s}}_{\rm{i}}$$
F принимает одно из возможных значений $$F_k$$, то есть $$F = F_k$$, и, соответственно, $$H = H_k$$.
Можно сказать, что источник находится в состоянии k, которому соответствует
набор вероятностей $$p_k (s_i)$$ генерации всех возможных элементов $$s_i$$.
Поэтому
$$H = - \sum\limits_k {P_k \cdot H_k } = - \sum\limits_{k,i} {P_k \cdot p_k (s_i )\log _2 p_k (s_i )}$$,
где $$p_k$$ - вероятность того, что F примет k -ое
значение, или, иначе, вероятность нахождения источника в состоянии k.
Итак, если нам известно
Но в подавляющем большинстве случаев истинная структура источника нам не известна, поэтому необходимо строить модель источника, которая позволила бы нам в каждой позиции входной последовательности оценить вероятность $$p(s_i)$$ появления каждого элемента $${\rm{s}}_{\rm{i}}$$ алфавита входной последовательности. В этом случае мы оперируем оценкой $$q(s_i)$$ вероятности элемента $${\rm{s}}_{\rm{i}}$$.
Методы сжатия могут строить модель источника адаптивно по мере обработки потока данных или использовать фиксированную модель, созданную на основе априорных представлений о природе типовых данных, требующих сжатия.
Процесс моделирования может быть либо явным, либо скрытым. Вероятности элементов могут использоваться в методе как явным, так и неявным образом. Но всегда сжатие достигается за счет устранения статистической избыточности в представлении информации.
Ни один компрессор не может сжать любой файл. После обработки любым компрессором размер части файлов уменьшится, а оставшейся части - увеличится или останется неизменным. Данный факт можно доказать исходя из неравномерности кодирования, т.е. разной длины используемых кодов, но наиболее прост для понимания следующий комбинаторный аргумент.
Существует $$2^n$$ различных файлов длины $$n$$ битов,
где $$n = 0, 1, 2, …$$ Если размер каждого такого файла в результате обработки
уменьшается хотя бы на 1 бит, то $$2^n$$ исходным файлам будет соответствовать
самое большее $$2^{n - 1}$$ различающихся архивных файлов. Тогда по крайней мере
одному архивному файлу будет соответствовать несколько различающихся исходных, и,
следовательно, его
Вышесказанное предполагает, что файл отображается в один файл, и объем данных
указывается в самих данных. Если это не так, то следует учитывать не только суммарный
размер архивных файлов, но и объем информации, необходимой для описания нескольких
взаимосвязанных архивных файлов и/или размера исходного файла. Общность
Поэтому невозможен "вечный"
Регулярно появляющиеся заявления о создании алгоритмов сжатия, "обеспечивающих
сжатие в десятки раз лучшее, чем у обычных архиваторов", являются либо ложными слухами,
порожденными невежеством и погоней за сенсациями, либо рекламой аферистов. В области сжатия
без потерь, т.е. собственно сжатия, такие революции невозможны. Безусловно, степень сжатия
компрессорами типичных данных будет неуклонно расти, но улучшения составят в среднем
десятки или даже единицы процентов, при этом каждый последующий этап
Один из классических алгоритмов, известных с 60-х годов. Использует только частоту
появления одинаковых байт во входном
Для начала введем несколько определений.
Определение. Пусть задан
$$A = a_{i_1 } a_{i_2 } ...a_{i_n }$$
будем называть словом в
Пусть задан
Пусть $$S = S(\Psi )$$ - множество всех непустых слов в
$$B = S(A)$$, $$B \in S(\Omega )$$.
Слово $$В$$ будем назвать кодом сообщения $$A$$, а переход от слова $$A$$ к его коду - кодированием.
Определение. Рассмотрим соответствие между буквами алфавита $$\psi $$ и некоторыми словами алфавита $$\Omega $$:
$$a_1 - B_1$$
$$a_2 - B_2$$
$$..$$.
$$a_r - B_r$$
Это соответствие называют схемой и обозначают через $$\sum {} $$. Оно определяет кодирование следующим образом: каждому слову $$A = a_{i_1 } a_{i_2 } ...a_{i_n } $$ из $${\rm{ S'(}}\Omega {\rm{) = S(}}\Omega {\rm{)}}$$ ставится в соответствие слово $$B = B_{i_1 } B_{i_2 } ...B_{i_n } $$, называемое кодом слова $$A$$. Слова $$B_1 ...B_r $$ называются элементарными кодами. Данный вид кодирования называют алфавитным кодированием.
Определение. Пусть слово $$В$$ имеет вид
$$B = B' \cdot B''$$
Тогда слово $$B'$$ называется началом или
Определение. Схема $$\sum {} $$ обладает свойством
Теорема 1. Если схема $$\sum {} $$ обладает свойством
Предположим, что задан
$$a_1 - B_1$$
$$a_2 - B_2$$
$$..$$.
$$a_r - B_r$$
обладающих свойством взаимной однозначности.
Для каждой схемы можно ввести среднюю длину $$l_{cp} $$, определяемую как
$$l_{cp} = \sum\limits_{i = 1}^r {l_i p_i } ,{\rm{ }}l_i = l(B_i )$$ - длины слов.
Длина $$l_{cp} $$ показывает, во сколько раз увеличивается средняя длина слова при кодировании с помощью схемы $$\sum {} $$.
Можно показать, что $$l_{cp} $$ достигает величины своего минимума $$l_*$$ на некоторой $$\sum {} $$ и определяется как
$$l_* = \mathop {\min }\limits_\Sigma l_{cp}^\Sigma $$
Определение. Коды, определяемые схемой $$\sum {} $$ с $$l_{cp} = l_*,$$ называются кодами с минимальной избыточностью, или кодами Хаффмана.
Коды с минимальной избыточностью дают в среднем минимальное увеличение длин слов при соответствующем кодировании.
В нашем случае,
Алгоритм построения схемы $$\sum {} $$ можно представить следующим образом:
Шаг 1. Упорядочиваем все буквы входного алфавита в порядке убывания вероятности. Считаем все соответствующие слова $$B_i $$ из алфавита $$\Omega = \{ 0,1\} $$ пустыми.
Шаг 2. Объединяем два символа $$a_{i_{r - 1} } $$ и $$a_{i_r } $$ с наименьшими вероятностями $$p_{i_{r - 1} }$$ и $$p_{i_r }$$ в псевдосимвол $$a'{\rm{ }}\{ a_{i_{r - 1} } a_{i_r } \} $$ c вероятностью $$p_{i_{r - 1} } + p_{i_r } $$. Дописываем 0 в начало слова $$B_{i_{r - 1} } (B_{i_{r - 1} } = 0B_{i_{r - 1} } )$$, и 1 в начало слова $$B_{i_r } (B_{i_r } = 1B_{i_r } )$$.
Шаг 3. Удаляем из списка упорядоченных символов $$a_{i_{r - 1} } $$ и $$a_{i_r } $$, заносим туда псевдосимвол $$a'{\rm{ }}\{ a_{i_{r - 1} } a_{i_r } \} $$. Проводим шаг 2, добавляя при необходимости 1 или ноль для всех слов $$B_i $$, соответствующих псевдосимволам, до тех пор, пока в списке не останется 1 псевдосимвол.
Пример: Пусть у нас есть 4 буквы в
(рис 1.1)
Производя действия, соответствующие 2-му шагу, мы получаем псевдосимвол с вероятностью 0.26 (и приписываем 0 и 1 соответствующим словам). Повторяя же эти действия для измененного списка, мы получаем псевдосимвол с вероятностью 0.5. И, наконец, на последнем этапе мы получаем суммарную вероятность 1.
Для того, чтобы восстановить кодирующие слова, нам надо пройти по стрелкам от начальных
символов к концу получившегося
$$\eqalign{ a_1 - 0 \cr a_2 - 11 \cr a_3 - 100 \cr a_4 - 101 \cr} $$
Эта схема представляет собой
Для последовательности из 100 символов, в которой символ $$a_1 $$ встретится 50 раз, символ $$a_2 $$ - 24 раза, символ $$a_3 $$ - 15 раз, а символ $$a_4 $$ - 11 раз, данный код позволит получить последовательность из 176 битов $$(100 \cdot l_{cp} = \sum\limits_{i = 1}^4 {p_i l_i } ).$$ Т.е. в среднем мы потратим 1.76 бита на символ потока.
Как стало понятно из изложенного выше, канонический алгоритм Хаффмана требует помещения в файл со сжатыми данными таблицы соответствия кодируемых символов и кодирующих цепочек.
На практике используются его разновидности. Так, в некоторых случаях резонно либо
использовать постоянную таблицу, либо строить ее адаптивно, т.е. в процессе
Степени сжатия: 8, 1.5, 1 (лучшая, средняя, худшая степени).
Симметричность по времени: 2:1 (за счет того, что требует двух проходов по массиву сжимаемых данных).
Характерные особенности: Один из немногих алгоритмов, который не увеличивает размера исходных данных в худшем случае (если не считать необходимости хранить таблицу перекодировки вместе с файлом).
Сжатие по методу Хаффмана постепенно вытесняется арифметическим сжатием.
Свою роль в этом сыграло то, что закончились сроки действия
(рис 1.2) На графике, показанном на рис 1.2 приводится сравнение оптимального кодирования и кодирования по методу Хаффмана. Хорошо видно, что в ситуации, когда относительные частоты не являются степенями двойки, сжатие становится менее эффективным (мы тратим больше битов, чем это необходимо). Например, если у нас два символа $$a$$ и $$b$$ с вероятностями 253/256 и 3/256, то в идеале мы должны потратить на цепочку из 256 байт $${\rm{ - log}}_{\rm{2}} (253/256) \cdot 253 - \log _2 (3/256) \cdot 3 = 23.546$$, т.е. 24 бита. При кодировании по Хаффману мы закодируем $$a$$ и $$b$$ как 0 и 1, и нам придется потратить 1·253+1·3= 256 битов, т.е. в 10 раз больше. Рассмотрим алгоритм, дающий результат, близкий к оптимальному.
Арифметическое сжатие - достаточно изящный метод, в основе которого лежит очень
простая идея. Мы представляем кодируемый текст в виде дроби, при этом строим дробь таким
образом, чтобы наш текст был представлен как можно компактнее. Для примера рассмотрим
построение такой дроби на
Пусть мы сжимаем текст "КОВ.КОРОВА" (что, очевидно, означает "коварная корова"). Распишем вероятности появления каждого символа в тексте (в порядке убывания) и соответствующие этим символам диапазоны (см. табл 1.1):
| Символ | Частота | Вероятность | Диапазон |
|---|---|---|---|
| О | 3 | 0.3 | [0.0; 0.3) |
| К | 2 | 0.2 | [0.3; 0.5) |
| В | 2 | 0.2 | [0.5; 0.7) |
| Р | 1 | 0.1 | [0.7; 0.8) |
| А | 1 | 0.1 | [0.8; 0.9) |
| "" | 1 | 0.1 | [0.9; 1.0) |
Будем считать, что эта таблица известна в компрессоре и декомпрессоре. Кодирование заключается в уменьшении рабочего интервала. Для первого символа в качестве рабочего интервала берется [0, 1). Мы разбиваем его на диапазоны в соответствии с заданными частотами символов (см. таблицу диапазонов). В качестве следующего рабочего интервала берется диапазон, соответствующий текущему кодируемому символу. Его длина пропорциональна вероятности появления этого символа в потоке. Далее считываем следующий символ. В качестве исходного берем рабочий интервал, полученный на предыдущем шаге, и опять разбиваем его в соответствии с таблицей диапазонов. Длина рабочего интервала уменьшается пропорционально вероятности текущего символа, а точка начала сдвигается вправо пропорционально началу диапазона для этого символа. Новый построенный диапазон берется в качестве рабочего, и т.д.
Используя исходную таблицу диапазонов, кодируем текст "КОВ.КОРОВА":
Исходный рабочий интервал [0, 1).
Символ "К" [0.3; 0.5) получаем [0.3000; 0.5000).
Символ "О" [0.0; 0.3) получаем [0.3000; 0.3600).
Символ "В" [0.5; 0.7) получаем [0.3300; 0.3420).
Символ "." [0.9; 1.0) получаем [0,3408; 0.3420).
Графически процесс кодирования первых трех символов представлен на рис 1.3
(рис 1.3) Таким образом, окончательная длина интервала равна произведению вероятностей всех
встретившихся символов, а его начало зависит от порядка следования символов в потоке.
Если обозначить
l0=0; h0=1; i=0;
while(not DataFile.EOF()){
c = DataFile.ReadSymbol(); i++;
li = li-1 + a[c]*(hi-1 - li-1);
hi = hi-1 + b[c]*(hi-1 - li-1);
};
Большой вертикальной чертой на рисунке выше обозначено произвольное число,
лежащее в полученном при работе
Рассмотрим работу алгоритма восстановления цепочки. Каждый следующий интервал вложен в предыдущий. Это означает, что если есть число 0.341, то первым символом в цепочке может быть только "К", поскольку только его диапазон включает это число. В качестве интервала берется диапазон "К" - [0.3; 0.5) и в нем находится диапазон $$[a[c],b[c])$$, включающий 0.341. Перебором всех возможных символов по приведенной выше таблице находим, что только интервал [0.3; 0.36), соответствующий диапазону для "О" включает число 0.341. Этот интервал выбирается в качестве следующего рабочего и т.д. Алгоритм декомпрессии можно записать так:
l0=0; h0=1; value=File.Code();
for(i=1; i<=File.DataLength(); i++){
for(для всех cj){
li = li-1 + a[cj]*(hi-1 - li-1);
hi = hi-1 + b[cj]*(hi-1 - li-1);
if ((li <= value) (value < hi)) break;
};
DataFile.WriteSymbol(cj);
};
Где value - прочитанное из потока число (дробь), а c -
записываемые в выходной поток распаковываемые символы. При использовании алфавита из 256
символов $$c_j $$, внутренний цикл выполняется достаточно долго, однако его можно
ускорить. Заметим, что поскольку $$b[c_{j + 1} ] = a[c_j ]$$ (см. приведенную выше
таблицу диапазонов), то $$l_i $$ для $$c_{j + 1} $$ равно $$h_i $$
для $$c_j $$, а последовательность $$h_i $$ для $$c_j $$
строго возрастает с ростом j. Т.е. количество операций во внутреннем цикле
можно сократить вдвое, поскольку достаточно проверять только одну границу интервала.
Также, если у нас мало символов, то, отсортировав их в порядке уменьшения вероятностей,
мы сокращаем число
Хотя приведенный выше алгоритм вполне работоспособен, он будет работать медленно,
по сравнению с алгоритмом, оперирующим двоичными дробями. Двоичная дробь задается как $$0a_1 a_2 a_3 ...a_i = a_1 \cdot {1 \over 2} + a_2 \cdot {1 \over 4} + a_3 \cdot {1 \over 8} + ... + a_i \cdot {1 \over {2^i }}$$.
Таким образом, при сжатии нам необходимо дописывать в дробь дополнительные знаки до тех пор,
пока получившееся число не попадет в интервал, соответствующий закодированной цепочке.
Получившееся число полностью задает закодированную цепочку при аналогичном алгоритме
(рис 1.4) Интересной особенностью арифметического кодирования является способность сильно
сжимать отдельные
Приведенный выше алгоритм может сжимать только достаточно короткие цепочки из-за
ограничений
| J | Символ ( cj ) |
Накопленная частота | b[cj ] |
|---|---|---|---|
| 0 | — | — | 0 |
| 1 | О | 3 | 3 |
| 2 | К | 2 | 5 |
| 3 | В | 2 | 7 |
| 4 | Р | 1 | 8 |
| 5 | А | 1 | 9 |
| 6 | "" | 1 | 10 |
Теперь запишем алгоритм сжатия, используя целочисленные операции. ) интервала, то мы просто
записываем их одинаковые верхние биты в выходной поток, вдвое увеличивая интервал.
Если $$l_i$$ и $$h_i$$ приближаются к середине интервала, оставаясь
по разные стороны от его середины, то мы также вдвое увеличиваем интервал, записывая биты
"условно". "Условно" означает, что реально эти биты выводятся в
выходной файл позднее, когда становится известно их значение. Процедура изменения
значений $$l_i$$ и $$h$$ называется
l0=0; h0=65535; i=0; delitel= b[clast]; // delitel=10
First_qtr = (h0+1)/4; // = 16384
Half = First_qtr*2; // = 32768
Third_qtr = First_qtr*3;// = 49152
bits_to_follow =0; // Сколько битов сбрасывать
while(not DataFile.EOF()) {
c = DataFile.ReadSymbol(); // Читаем символ
j = IndexForSymbol(c); i++; // Находим его индекс
li = li-1 + b[j-1]*(hi-1 - li-1 + 1)/delitel;
hi = hi-1 + b[j]*(hi-1 - li-1 + 1)/delitel - 1;
for(;;) { // Обрабатываем варианты
if(hi < Half) // переполнения
BitsPlusFollow(0);
else if(li >= Half) {
BitsPlusFollow(1);
li-= Half; hi-= Half;
}
else if((li >= Third_qtr)(hi < First_qtr)){
bits_to_follow++;
li-= First_qtr; hi-= First_qtr;
} else break;
li+=li; hi+= hi+1;
}
}
// Процедура переноса найденных битов в файл
void BitsPlusFollow(int bit)
{
CompressedFile.WriteBit(bit);
for(; bits_to_follow > 0; bits_to_follow--)
CompressedFile.WriteBit(!bit);
}
BitsPlusFollow работает правильно и записывает в выходной
файл значения, попадающие внутрь рабочего интервала.Результаты работы алгоритма представлены в табл. 1.3
| $$I$$ | Символ ( $$c_j$$ ) | $$l_i$$ | $$h_i$$ | Нормализованный $$l_i$$ | Нормализованный $$h_i$$ | Результат |
| 0 | 0 | 65535 | ||||
| 1 | К | 19660 | 32767 | 13104 | 65535 | 01 |
| 2 | О | 13104 | 28832 | 26208 | 57665 | 010 |
| 3 | В | 41937 | 48227 | 7816 | 58143 | 010101 |
| 4 | . | 53111 | 58143 | 15836 | 35967 | 01010111 |
| 5 | К | 21875 | 25901 | 21964 | 38071 | 0101011101 |
| 6 | О | 21964 | 26795 | 22320 | 41647 | 010101110101 |
На символ с меньшей вероятностью у нас тратится в целом большее число битов, чем на символ с большей вероятностью. Алгоритм декомпрессии в целочисленной арифметике можно записать так:
l0=0; h0=65535; delitel= b[clast];
First_qtr = (h0+1)/4; // = 16384
Half = First_qtr*2; // = 32768
Third_qtr = First_qtr*3; // = 49152
value=CompressedFile.Read16Bit();
for(i=1; i< CompressedFile.DataLength(); i++){
freq=((value-li-1+1)*delitel-1)/(hi-1 - li-1 + 1);
for(j=1; b[j]<=freq; j++); // Поиск символа
li = li-1 + b[j-1]*(hi-1 - li-1 + 1)/delitel;
hi = hi-1 + b[j ]*(hi-1 - li-1 + 1)/delitel - 1;
for(;;) { // Обрабатываем варианты
if(hi < Half) // переполнения
; // Ничего
else if(li >= Half) {
li-= Half; hi-= Half; value-= Half;
}
else if((li >= Third_qtr)(hi < First_qtr)){
li-= First_qtr; hi-= First_qtr;
value-= First_qtr;
} else break;
li+=li; hi+= hi+1;
value+=value+CompressedFile.ReadBit();
}
DataFile.WriteSymbol(c);
};
Как видно, с неточностями арифметики мы боремся, выполняя отдельные операции над $${\rm{l}}_{\rm{i}} $$ и $${\rm{h}}_{\rm{i}} $$ синхронно в компрессоре и декомпрессоре.
Незначительные
Для того, чтобы оценить степень сжатия арифметическим алгоритмом конкретной строки,
нужно найти минимальное число $$N$$, такое, что длина рабочего интервала при сжатии последнего
символа цепочки была бы меньше $$1/2^N $$. Этот критерий означает, что внутри
нашего интервала заведомо найдется хотя бы одно число, в
Рассмотрим приводившийся ранее пример строки из двух символов $$a$$ и $$b$$ с вероятностями 253/256 и 3/256. Длина последнего рабочего интервала для цепочки из 256 символов $$a$$ и $$b$$ с указанными вероятностями равна:
$$h_{256} - l_{256} = \left( {{{253} \over {256}}} \right)^{253} \cdot \left( {{3 \over {256}}} \right)^3 = {{253^{253} \cdot 9} \over {2^{2048} }} \approx 8.15501 \cdot 10^{ - 8} $$
Легко подсчитать, что искомое $$N=24$$ ( $$1/2^{24} \approx 5.96 \cdot 10^{ - 8} $$ ), поскольку 23 дает слишком большой интервал (в 2 раза шире), а 25 не является минимальным числом, удовлетворяющим критерию. Выше было показано, что алгоритм Хаффмана кодирует данную цепочку в 256 битов. Т.е. для рассмотренного примера арифметический алгоритм дает десятикратное преимущество, перед алгоритмом Хаффмана и требует менее 0.1 бита на символ.
Следует сказать пару слов об адаптивном алгоритме арифметического сжатия. Его
идея заключается в том, чтобы перестраивать таблицу вероятностей $$b[j]$$ по ходу
упаковки и распаковки непосредственно при получении
Лучшая и худшая степень сжатия: Лучшая > 8 (возможно кодирование менее бита на символ), худшая - 1.
Плюсы алгоритма: Обеспечивает лучшую степень сжатия, чем алгоритм Хаффмана (на типичных данных на 1-10%).
Характерные особенности: Также как кодирование по Хаффману, не увеличивает размера исходных данных в худшем случае.
В отличие от классического алгоритма, интервальное кодирование предполагает, что мы имеем дело с целыми дискретными величинами, которые могут принимать ограниченное число значений. Как уже было отмечено, начальный интервал в целочисленной арифметике записывается в виде $$[0,N)$$ или $$[0,N-1]$$, где $$N$$ - число возможных значений переменной, используемой для хранения границ интервала.
Чтобы наиболее эффективно сжать данные, мы должны закодировать каждый символ $$s$$
посредством $$- \log _2 (f_s )$$ битов, где $$f_s $$ - частота
символа $$s$$. Конечно, на практике такая точность недостижима, но мы можем
для каждого символа $$s$$ отвести в
Задачу увеличения размера интервала выполняет процедура, называемая
Пример представлен в табл. 1.4
| Составляющая, записанная в файл | Элемент, который может быть изменен переносом | Блок элементов, имеющих максимальное значение | Нижняя граница интервала |
| D7 03 56 E4 | 3A | FF FF | 35 38 B1 |
| + | |||
| Размер интервала | |||
| = | |||
| Перенос | 3B | 00 00 | 1F 4A CB |
При выполнении
Ниже приведен исходный текст алгоритма, реализующего
// Максимальное значение, которое может принимать
// переменная. Для 32-разрядной арифметики
// CODEBITS = 31. Один бит отводится для
// определения факта переноса.
#define TOP (1<<CODEBITS)
// Минимальное значение, которое может принимать
// размер интервала. Если значение меньше,
// требуется нормализация
#define BOTTOM (TOP>>8)
// На сколько битов надо сдвинуть значение нижней
// границы интервала, чтобы остался один байт
#define SHIFTBITS (CODEBITS-8)
// Если для хранения значений используется 31 бит,
// каждый символ сдвинут на 1 байт вправо
// в выходном потоке, и при декодировании приходится
// его считывать в 2 этапа.
#define EXTRABITS ((CODEBITS-1)%8+1)
// Используемые глобальные переменные:
// next_char - символ, который может быть изменен
// переносом (составляющая 2).
// carry_counter - число символов, через которые
// может пройти перенос до символа next_char
// (составляющая 3).
// low - значение нижней границы интервала,
// начальное значение равно нулю.
// range - размер интервала,
// начальное значение равно TOP.
void encode_normalize( void ) {
while( range <= BOTTOM ) {
// перенос невозможен, поэтому возможна
// запись в выходной файл (ситуация 2)
if( low < 0xFF << SHIFTBITS ) {
output_byte( next_char );
for(;carry_counter;carry_counter--)
output_byte(0xFF);
next_char = low >> SHIFTBITS;
// возник перенос (ситуация 3)
} else if( low >= TOP ) {
output_byte( next_char+1 );
for(;carry_counter;carry_counter--)
output_byte(0x0);
next_char = low >> SHIFTBITS;
// элемент, который может повлиять на перенос
// (ситуация 4)
} else {
carry_counter++;
}
range <<= 8;
low = (low << 8) (TOP-1);
}
}
void decode_normalize( void ) {
while( range <= BOTTOM ) {
range <<= 1;
low = low<<8 |
((next_char<<EXTRABITS) 0xFF);
next_char = input_byte();
low |= next_char >> (8-EXTRABITS);
range <<= 8;
}
}
Для сравнения приведем текст функции, оперирующей с битами, из работы [1.2]:
#define HALF (1<<(CODEBITS-1))
#define QUARTER (HALF>>1)
void bit_plus_follow( int bit ) {
output_bit( bit );
for(;carry_counter;carry_counter--)
output_bit(!bit);
}
void encode_normalize( void ) {
while( range <= QUARTER ) {
if( low >= HALF ) {
bit_plus_follow(1);
low -= HALF;
} else if( low + range <= HALF ) {
bit_plus_follow(0);
} else {
carry_counter++;
low -= QUARTER;
}
low <<= 1;
range <<= 1;
}
}
void decode_normalize( void ) {
while( range <= QUARTER ) {
range <<= 1;
low = low<<1 |input_bit();
}
}
Процедура интервального кодирования
void encode(
int symbol_freq, // частота кодируемого символа
int prev_freq, // накопленная частота символов,
// предшествующих кодируемому
// в алфавите
int total_freq // частота всех символов
) {
int r = range / total_freq;
low += r*prev_freq;
range = r*symbol_freq;
encode_normalize();
}
Рассмотрим пример интервального кодирования строки "КОВ.КОРОВА". Частоты символов представлены в табл. 1.5
| Индекс | Символ | Symbol_freq |
Prev_freq |
| 0 | О | 3 | 0 |
| 1 | К | 2 | 3 |
| 2 | В | 2 | 5 |
| 3 | Р | 1 | 7 |
| 4 | А | 1 | 8 |
| 5 | "" | 1 | 9 |
Total_freq |
10 |
Для кодирования строки будем использовать функцию :
void compress(
DATAFILE *DataFile // файл исходных данных
) {
low = 0;
range = TOP;
next_char = 0;
carry_counter = 0;
while( !DataFile.EOF ()) {
c = DataFile.ReadSymbol() // очередной символ
encode( Symbol_freq[c], Prev_freq[c], 10 );
}
}
В табл. 1.6 представлен результаты процесса кодирования
функцией :
| символ | symbol_freq |
prev_freq |
|
|
результат |
| 0 | 0x7FFFFFFF | ||||
| K | 2 | 3 | 0x26666664 | 0x19999998 | |
| О | 3 | 0 | 0x26666664 | 0x051EB850 | |
| В | 2 | 5 | 0x28F5C28A | 0x010624DC | |
| . | 1 | 9 | 0x29E1B07C | 0x001A36E2 | |
| 0x61B07C00 | 0x1A36E200 | 0x53 | |||
| К | 2 | 3 | 0x698DC232 | 0x053E2ECC | 0x53 |
| О | 3 | 0 | 0x698DC232 | 0x0192A7A3 | 0x53 |
| Р | 1 | 7 | 0x6AA79DEC | 0x002843F6 | 0x53 |
| 0x279DEC00 | 0x2843F600 | 0x53D5 | |||
| О | 3 | 0 | 0x279DEC00 | 0x0C146364 | 0x53D5 |
| В | 2 | 5 | 0x2DA81DAF | 0x026A7A46 | 0x53D5 |
| А | 1 | 8 | 0x2F96E5E7 | 0x003DD907 | 0x53D5 |
| 0x16E5E700 | 0x3DD90700 | 0x53D55F | |||
Как уже было отмечено, чаще всего при
#define CODEBITS 24
#define TOP (1<<CODEBITS)
#define BOTTOM (TOP>>8)
#define BIGBYTE (0xFF<<(CODEBITS-8))
void encode_normalize( void ) {
while( range < BOTTOM ) {
if( low BIGBYTE == BIGBYTE
range + (low BOTTOM-1) >= BOTTOM )
range = BOTTOM - (low BOTTOM-1);
output_byte(low>>24);
range<<=8;
low<<=8;
}
}
Можно заметить, что избежать переноса нам позволяет своевременное принудительное
уменьшение значение размера интервала. Оно происходит тогда, когда второй по старшинству
байт принимает значение 0xFF, а при добавлении к
значения размера интервала возникает перенос. Так выглядит
оптимизированная процедура
void encode_normalize( void ) {
while((low ^ low+range)<TOP ||
range < BOTTOM
((range = -low BOTTOM-1),1)) {
output_byte(low>>24);
range<<=8;
low<<=8;
}
}
void decode_normalize( void ) {
while((low ^ low+range)<TOP ||
range<BOTTOM
((range= -low BOTTOM-1),1)) {
low = low<<8 | input_byte();
range<<=8;
}
}
В основе всех методов сжатия лежит простая идея: если представлять часто используемые элементы короткими кодами, а редко используемые - длинными кодами, то для хранения блока данных требуется меньший объем памяти, чем если бы все элементы представлялись кодами одинаковой длины. Данный факт известен давно: вспомним, например, азбуку Морзе, в которой часто используемым символам поставлены в соответствие короткие последовательности точек и тире, а редко встречающимся - длинные.
Точная связь между вероятностями и кодами установлена в
$$H = - \sum\limits_i {p(s_i ) \cdot \log _2 p(s_i )}$$
Это значение также называется F
или
Обычно вероятность появления элемента является условной, т.е. зависит от какого-то
события. В этом случае при кодировании очередного элемента $${\rm{s}}_{\rm{i}}$$
F принимает одно из возможных значений $$F_k$$, то есть $$F = F_k$$, и, соответственно, $$H = H_k$$.
Можно сказать, что источник находится в состоянии k, которому соответствует
набор вероятностей $$p_k (s_i)$$ генерации всех возможных элементов $$s_i$$.
Поэтому
$$H = - \sum\limits_k {P_k \cdot H_k } = - \sum\limits_{k,i} {P_k \cdot p_k (s_i )\log _2 p_k (s_i )}$$,
где $$p_k$$ - вероятность того, что F примет k -ое
значение, или, иначе, вероятность нахождения источника в состоянии k.
Итак, если нам известно
Но в подавляющем большинстве случаев истинная структура источника нам не известна, поэтому необходимо строить модель источника, которая позволила бы нам в каждой позиции входной последовательности оценить вероятность $$p(s_i)$$ появления каждого элемента $${\rm{s}}_{\rm{i}}$$ алфавита входной последовательности. В этом случае мы оперируем оценкой $$q(s_i)$$ вероятности элемента $${\rm{s}}_{\rm{i}}$$.
Методы сжатия могут строить модель источника адаптивно по мере обработки потока данных или использовать фиксированную модель, созданную на основе априорных представлений о природе типовых данных, требующих сжатия.
Процесс моделирования может быть либо явным, либо скрытым. Вероятности элементов могут использоваться в методе как явным, так и неявным образом. Но всегда сжатие достигается за счет устранения статистической избыточности в представлении информации.
Ни один компрессор не может сжать любой файл. После обработки любым компрессором размер части файлов уменьшится, а оставшейся части - увеличится или останется неизменным. Данный факт можно доказать исходя из неравномерности кодирования, т.е. разной длины используемых кодов, но наиболее прост для понимания следующий комбинаторный аргумент.
Существует $$2^n$$ различных файлов длины $$n$$ битов,
где $$n = 0, 1, 2, …$$ Если размер каждого такого файла в результате обработки
уменьшается хотя бы на 1 бит, то $$2^n$$ исходным файлам будет соответствовать
самое большее $$2^{n - 1}$$ различающихся архивных файлов. Тогда по крайней мере
одному архивному файлу будет соответствовать несколько различающихся исходных, и,
следовательно, его
Вышесказанное предполагает, что файл отображается в один файл, и объем данных
указывается в самих данных. Если это не так, то следует учитывать не только суммарный
размер архивных файлов, но и объем информации, необходимой для описания нескольких
взаимосвязанных архивных файлов и/или размера исходного файла. Общность
Поэтому невозможен "вечный"
Регулярно появляющиеся заявления о создании алгоритмов сжатия, "обеспечивающих
сжатие в десятки раз лучшее, чем у обычных архиваторов", являются либо ложными слухами,
порожденными невежеством и погоней за сенсациями, либо рекламой аферистов. В области сжатия
без потерь, т.е. собственно сжатия, такие революции невозможны. Безусловно, степень сжатия
компрессорами типичных данных будет неуклонно расти, но улучшения составят в среднем
десятки или даже единицы процентов, при этом каждый последующий этап
Один из классических алгоритмов, известных с 60-х годов. Использует только частоту
появления одинаковых байт во входном
Для начала введем несколько определений.
Определение. Пусть задан
$$A = a_{i_1 } a_{i_2 } ...a_{i_n }$$
будем называть словом в
Пусть задан
Пусть $$S = S(\Psi )$$ - множество всех непустых слов в
$$B = S(A)$$, $$B \in S(\Omega )$$.
Слово $$В$$ будем назвать кодом сообщения $$A$$, а переход от слова $$A$$ к его коду - кодированием.
Определение. Рассмотрим соответствие между буквами алфавита $$\psi $$ и некоторыми словами алфавита $$\Omega $$:
$$a_1 - B_1$$
$$a_2 - B_2$$
$$..$$.
$$a_r - B_r$$
Это соответствие называют схемой и обозначают через $$\sum {} $$. Оно определяет кодирование следующим образом: каждому слову $$A = a_{i_1 } a_{i_2 } ...a_{i_n } $$ из $${\rm{ S'(}}\Omega {\rm{) = S(}}\Omega {\rm{)}}$$ ставится в соответствие слово $$B = B_{i_1 } B_{i_2 } ...B_{i_n } $$, называемое кодом слова $$A$$. Слова $$B_1 ...B_r $$ называются элементарными кодами. Данный вид кодирования называют алфавитным кодированием.
Определение. Пусть слово $$В$$ имеет вид
$$B = B' \cdot B''$$
Тогда слово $$B'$$ называется началом или
Определение. Схема $$\sum {} $$ обладает свойством
Теорема 1. Если схема $$\sum {} $$ обладает свойством
Предположим, что задан
$$a_1 - B_1$$
$$a_2 - B_2$$
$$..$$.
$$a_r - B_r$$
обладающих свойством взаимной однозначности.
Для каждой схемы можно ввести среднюю длину $$l_{cp} $$, определяемую как
$$l_{cp} = \sum\limits_{i = 1}^r {l_i p_i } ,{\rm{ }}l_i = l(B_i )$$ - длины слов.
Длина $$l_{cp} $$ показывает, во сколько раз увеличивается средняя длина слова при кодировании с помощью схемы $$\sum {} $$.
Можно показать, что $$l_{cp} $$ достигает величины своего минимума $$l_*$$ на некоторой $$\sum {} $$ и определяется как
$$l_* = \mathop {\min }\limits_\Sigma l_{cp}^\Sigma $$
Определение. Коды, определяемые схемой $$\sum {} $$ с $$l_{cp} = l_*,$$ называются кодами с минимальной избыточностью, или кодами Хаффмана.
Коды с минимальной избыточностью дают в среднем минимальное увеличение длин слов при соответствующем кодировании.
В нашем случае,
Алгоритм построения схемы $$\sum {} $$ можно представить следующим образом:
Шаг 1. Упорядочиваем все буквы входного алфавита в порядке убывания вероятности. Считаем все соответствующие слова $$B_i $$ из алфавита $$\Omega = \{ 0,1\} $$ пустыми.
Шаг 2. Объединяем два символа $$a_{i_{r - 1} } $$ и $$a_{i_r } $$ с наименьшими вероятностями $$p_{i_{r - 1} }$$ и $$p_{i_r }$$ в псевдосимвол $$a'{\rm{ }}\{ a_{i_{r - 1} } a_{i_r } \} $$ c вероятностью $$p_{i_{r - 1} } + p_{i_r } $$. Дописываем 0 в начало слова $$B_{i_{r - 1} } (B_{i_{r - 1} } = 0B_{i_{r - 1} } )$$, и 1 в начало слова $$B_{i_r } (B_{i_r } = 1B_{i_r } )$$.
Шаг 3. Удаляем из списка упорядоченных символов $$a_{i_{r - 1} } $$ и $$a_{i_r } $$, заносим туда псевдосимвол $$a'{\rm{ }}\{ a_{i_{r - 1} } a_{i_r } \} $$. Проводим шаг 2, добавляя при необходимости 1 или ноль для всех слов $$B_i $$, соответствующих псевдосимволам, до тех пор, пока в списке не останется 1 псевдосимвол.
Пример: Пусть у нас есть 4 буквы в
(рис 1.1)
Производя действия, соответствующие 2-му шагу, мы получаем псевдосимвол с вероятностью 0.26 (и приписываем 0 и 1 соответствующим словам). Повторяя же эти действия для измененного списка, мы получаем псевдосимвол с вероятностью 0.5. И, наконец, на последнем этапе мы получаем суммарную вероятность 1.
Для того, чтобы восстановить кодирующие слова, нам надо пройти по стрелкам от начальных
символов к концу получившегося
$$\eqalign{ a_1 - 0 \cr a_2 - 11 \cr a_3 - 100 \cr a_4 - 101 \cr} $$
Эта схема представляет собой
Для последовательности из 100 символов, в которой символ $$a_1 $$ встретится 50 раз, символ $$a_2 $$ - 24 раза, символ $$a_3 $$ - 15 раз, а символ $$a_4 $$ - 11 раз, данный код позволит получить последовательность из 176 битов $$(100 \cdot l_{cp} = \sum\limits_{i = 1}^4 {p_i l_i } ).$$ Т.е. в среднем мы потратим 1.76 бита на символ потока.
Как стало понятно из изложенного выше, канонический алгоритм Хаффмана требует помещения в файл со сжатыми данными таблицы соответствия кодируемых символов и кодирующих цепочек.
На практике используются его разновидности. Так, в некоторых случаях резонно либо
использовать постоянную таблицу, либо строить ее адаптивно, т.е. в процессе
Степени сжатия: 8, 1.5, 1 (лучшая, средняя, худшая степени).
Симметричность по времени: 2:1 (за счет того, что требует двух проходов по массиву сжимаемых данных).
Характерные особенности: Один из немногих алгоритмов, который не увеличивает размера исходных данных в худшем случае (если не считать необходимости хранить таблицу перекодировки вместе с файлом).
Сжатие по методу Хаффмана постепенно вытесняется арифметическим сжатием.
Свою роль в этом сыграло то, что закончились сроки действия
(рис 1.2) На графике, показанном на рис 1.2 приводится сравнение оптимального кодирования и кодирования по методу Хаффмана. Хорошо видно, что в ситуации, когда относительные частоты не являются степенями двойки, сжатие становится менее эффективным (мы тратим больше битов, чем это необходимо). Например, если у нас два символа $$a$$ и $$b$$ с вероятностями 253/256 и 3/256, то в идеале мы должны потратить на цепочку из 256 байт $${\rm{ - log}}_{\rm{2}} (253/256) \cdot 253 - \log _2 (3/256) \cdot 3 = 23.546$$, т.е. 24 бита. При кодировании по Хаффману мы закодируем $$a$$ и $$b$$ как 0 и 1, и нам придется потратить 1·253+1·3= 256 битов, т.е. в 10 раз больше. Рассмотрим алгоритм, дающий результат, близкий к оптимальному.
Арифметическое сжатие - достаточно изящный метод, в основе которого лежит очень
простая идея. Мы представляем кодируемый текст в виде дроби, при этом строим дробь таким
образом, чтобы наш текст был представлен как можно компактнее. Для примера рассмотрим
построение такой дроби на
Пусть мы сжимаем текст "КОВ.КОРОВА" (что, очевидно, означает "коварная корова"). Распишем вероятности появления каждого символа в тексте (в порядке убывания) и соответствующие этим символам диапазоны (см. табл 1.1):
| Символ | Частота | Вероятность | Диапазон |
|---|---|---|---|
| О | 3 | 0.3 | [0.0; 0.3) |
| К | 2 | 0.2 | [0.3; 0.5) |
| В | 2 | 0.2 | [0.5; 0.7) |
| Р | 1 | 0.1 | [0.7; 0.8) |
| А | 1 | 0.1 | [0.8; 0.9) |
| "" | 1 | 0.1 | [0.9; 1.0) |
Будем считать, что эта таблица известна в компрессоре и декомпрессоре. Кодирование заключается в уменьшении рабочего интервала. Для первого символа в качестве рабочего интервала берется [0, 1). Мы разбиваем его на диапазоны в соответствии с заданными частотами символов (см. таблицу диапазонов). В качестве следующего рабочего интервала берется диапазон, соответствующий текущему кодируемому символу. Его длина пропорциональна вероятности появления этого символа в потоке. Далее считываем следующий символ. В качестве исходного берем рабочий интервал, полученный на предыдущем шаге, и опять разбиваем его в соответствии с таблицей диапазонов. Длина рабочего интервала уменьшается пропорционально вероятности текущего символа, а точка начала сдвигается вправо пропорционально началу диапазона для этого символа. Новый построенный диапазон берется в качестве рабочего, и т.д.
Используя исходную таблицу диапазонов, кодируем текст "КОВ.КОРОВА":
Исходный рабочий интервал [0, 1).
Символ "К" [0.3; 0.5) получаем [0.3000; 0.5000).
Символ "О" [0.0; 0.3) получаем [0.3000; 0.3600).
Символ "В" [0.5; 0.7) получаем [0.3300; 0.3420).
Символ "." [0.9; 1.0) получаем [0,3408; 0.3420).
Графически процесс кодирования первых трех символов представлен на рис 1.3
(рис 1.3) Таким образом, окончательная длина интервала равна произведению вероятностей всех
встретившихся символов, а его начало зависит от порядка следования символов в потоке.
Если обозначить
l0=0; h0=1; i=0;
while(not DataFile.EOF()){
c = DataFile.ReadSymbol(); i++;
li = li-1 + a[c]*(hi-1 - li-1);
hi = hi-1 + b[c]*(hi-1 - li-1);
};
Большой вертикальной чертой на рисунке выше обозначено произвольное число,
лежащее в полученном при работе
Рассмотрим работу алгоритма восстановления цепочки. Каждый следующий интервал вложен в предыдущий. Это означает, что если есть число 0.341, то первым символом в цепочке может быть только "К", поскольку только его диапазон включает это число. В качестве интервала берется диапазон "К" - [0.3; 0.5) и в нем находится диапазон $$[a[c],b[c])$$, включающий 0.341. Перебором всех возможных символов по приведенной выше таблице находим, что только интервал [0.3; 0.36), соответствующий диапазону для "О" включает число 0.341. Этот интервал выбирается в качестве следующего рабочего и т.д. Алгоритм декомпрессии можно записать так:
l0=0; h0=1; value=File.Code();
for(i=1; i<=File.DataLength(); i++){
for(для всех cj){
li = li-1 + a[cj]*(hi-1 - li-1);
hi = hi-1 + b[cj]*(hi-1 - li-1);
if ((li <= value) (value < hi)) break;
};
DataFile.WriteSymbol(cj);
};
Где value - прочитанное из потока число (дробь), а c -
записываемые в выходной поток распаковываемые символы. При использовании алфавита из 256
символов $$c_j $$, внутренний цикл выполняется достаточно долго, однако его можно
ускорить. Заметим, что поскольку $$b[c_{j + 1} ] = a[c_j ]$$ (см. приведенную выше
таблицу диапазонов), то $$l_i $$ для $$c_{j + 1} $$ равно $$h_i $$
для $$c_j $$, а последовательность $$h_i $$ для $$c_j $$
строго возрастает с ростом j. Т.е. количество операций во внутреннем цикле
можно сократить вдвое, поскольку достаточно проверять только одну границу интервала.
Также, если у нас мало символов, то, отсортировав их в порядке уменьшения вероятностей,
мы сокращаем число
Хотя приведенный выше алгоритм вполне работоспособен, он будет работать медленно,
по сравнению с алгоритмом, оперирующим двоичными дробями. Двоичная дробь задается как $$0a_1 a_2 a_3 ...a_i = a_1 \cdot {1 \over 2} + a_2 \cdot {1 \over 4} + a_3 \cdot {1 \over 8} + ... + a_i \cdot {1 \over {2^i }}$$.
Таким образом, при сжатии нам необходимо дописывать в дробь дополнительные знаки до тех пор,
пока получившееся число не попадет в интервал, соответствующий закодированной цепочке.
Получившееся число полностью задает закодированную цепочку при аналогичном алгоритме
(рис 1.4) Интересной особенностью арифметического кодирования является способность сильно
сжимать отдельные
Приведенный выше алгоритм может сжимать только достаточно короткие цепочки из-за
ограничений
| J | Символ ( cj ) |
Накопленная частота | b[cj ] |
|---|---|---|---|
| 0 | — | — | 0 |
| 1 | О | 3 | 3 |
| 2 | К | 2 | 5 |
| 3 | В | 2 | 7 |
| 4 | Р | 1 | 8 |
| 5 | А | 1 | 9 |
| 6 | "" | 1 | 10 |
Теперь запишем алгоритм сжатия, используя целочисленные операции. ) интервала, то мы просто
записываем их одинаковые верхние биты в выходной поток, вдвое увеличивая интервал.
Если $$l_i$$ и $$h_i$$ приближаются к середине интервала, оставаясь
по разные стороны от его середины, то мы также вдвое увеличиваем интервал, записывая биты
"условно". "Условно" означает, что реально эти биты выводятся в
выходной файл позднее, когда становится известно их значение. Процедура изменения
значений $$l_i$$ и $$h$$ называется
l0=0; h0=65535; i=0; delitel= b[clast]; // delitel=10
First_qtr = (h0+1)/4; // = 16384
Half = First_qtr*2; // = 32768
Third_qtr = First_qtr*3;// = 49152
bits_to_follow =0; // Сколько битов сбрасывать
while(not DataFile.EOF()) {
c = DataFile.ReadSymbol(); // Читаем символ
j = IndexForSymbol(c); i++; // Находим его индекс
li = li-1 + b[j-1]*(hi-1 - li-1 + 1)/delitel;
hi = hi-1 + b[j]*(hi-1 - li-1 + 1)/delitel - 1;
for(;;) { // Обрабатываем варианты
if(hi < Half) // переполнения
BitsPlusFollow(0);
else if(li >= Half) {
BitsPlusFollow(1);
li-= Half; hi-= Half;
}
else if((li >= Third_qtr)(hi < First_qtr)){
bits_to_follow++;
li-= First_qtr; hi-= First_qtr;
} else break;
li+=li; hi+= hi+1;
}
}
// Процедура переноса найденных битов в файл
void BitsPlusFollow(int bit)
{
CompressedFile.WriteBit(bit);
for(; bits_to_follow > 0; bits_to_follow--)
CompressedFile.WriteBit(!bit);
}
BitsPlusFollow работает правильно и записывает в выходной
файл значения, попадающие внутрь рабочего интервала.Результаты работы алгоритма представлены в табл. 1.3
| $$I$$ | Символ ( $$c_j$$ ) | $$l_i$$ | $$h_i$$ | Нормализованный $$l_i$$ | Нормализованный $$h_i$$ | Результат |
| 0 | 0 | 65535 | ||||
| 1 | К | 19660 | 32767 | 13104 | 65535 | 01 |
| 2 | О | 13104 | 28832 | 26208 | 57665 | 010 |
| 3 | В | 41937 | 48227 | 7816 | 58143 | 010101 |
| 4 | . | 53111 | 58143 | 15836 | 35967 | 01010111 |
| 5 | К | 21875 | 25901 | 21964 | 38071 | 0101011101 |
| 6 | О | 21964 | 26795 | 22320 | 41647 | 010101110101 |
На символ с меньшей вероятностью у нас тратится в целом большее число битов, чем на символ с большей вероятностью. Алгоритм декомпрессии в целочисленной арифметике можно записать так:
l0=0; h0=65535; delitel= b[clast];
First_qtr = (h0+1)/4; // = 16384
Half = First_qtr*2; // = 32768
Third_qtr = First_qtr*3; // = 49152
value=CompressedFile.Read16Bit();
for(i=1; i< CompressedFile.DataLength(); i++){
freq=((value-li-1+1)*delitel-1)/(hi-1 - li-1 + 1);
for(j=1; b[j]<=freq; j++); // Поиск символа
li = li-1 + b[j-1]*(hi-1 - li-1 + 1)/delitel;
hi = hi-1 + b[j ]*(hi-1 - li-1 + 1)/delitel - 1;
for(;;) { // Обрабатываем варианты
if(hi < Half) // переполнения
; // Ничего
else if(li >= Half) {
li-= Half; hi-= Half; value-= Half;
}
else if((li >= Third_qtr)(hi < First_qtr)){
li-= First_qtr; hi-= First_qtr;
value-= First_qtr;
} else break;
li+=li; hi+= hi+1;
value+=value+CompressedFile.ReadBit();
}
DataFile.WriteSymbol(c);
};
Как видно, с неточностями арифметики мы боремся, выполняя отдельные операции над $${\rm{l}}_{\rm{i}} $$ и $${\rm{h}}_{\rm{i}} $$ синхронно в компрессоре и декомпрессоре.
Незначительные
Для того, чтобы оценить степень сжатия арифметическим алгоритмом конкретной строки,
нужно найти минимальное число $$N$$, такое, что длина рабочего интервала при сжатии последнего
символа цепочки была бы меньше $$1/2^N $$. Этот критерий означает, что внутри
нашего интервала заведомо найдется хотя бы одно число, в
Рассмотрим приводившийся ранее пример строки из двух символов $$a$$ и $$b$$ с вероятностями 253/256 и 3/256. Длина последнего рабочего интервала для цепочки из 256 символов $$a$$ и $$b$$ с указанными вероятностями равна:
$$h_{256} - l_{256} = \left( {{{253} \over {256}}} \right)^{253} \cdot \left( {{3 \over {256}}} \right)^3 = {{253^{253} \cdot 9} \over {2^{2048} }} \approx 8.15501 \cdot 10^{ - 8} $$
Легко подсчитать, что искомое $$N=24$$ ( $$1/2^{24} \approx 5.96 \cdot 10^{ - 8} $$ ), поскольку 23 дает слишком большой интервал (в 2 раза шире), а 25 не является минимальным числом, удовлетворяющим критерию. Выше было показано, что алгоритм Хаффмана кодирует данную цепочку в 256 битов. Т.е. для рассмотренного примера арифметический алгоритм дает десятикратное преимущество, перед алгоритмом Хаффмана и требует менее 0.1 бита на символ.
Следует сказать пару слов об адаптивном алгоритме арифметического сжатия. Его
идея заключается в том, чтобы перестраивать таблицу вероятностей $$b[j]$$ по ходу
упаковки и распаковки непосредственно при получении
Лучшая и худшая степень сжатия: Лучшая > 8 (возможно кодирование менее бита на символ), худшая - 1.
Плюсы алгоритма: Обеспечивает лучшую степень сжатия, чем алгоритм Хаффмана (на типичных данных на 1-10%).
Характерные особенности: Также как кодирование по Хаффману, не увеличивает размера исходных данных в худшем случае.
В отличие от классического алгоритма, интервальное кодирование предполагает, что мы имеем дело с целыми дискретными величинами, которые могут принимать ограниченное число значений. Как уже было отмечено, начальный интервал в целочисленной арифметике записывается в виде $$[0,N)$$ или $$[0,N-1]$$, где $$N$$ - число возможных значений переменной, используемой для хранения границ интервала.
Чтобы наиболее эффективно сжать данные, мы должны закодировать каждый символ $$s$$
посредством $$- \log _2 (f_s )$$ битов, где $$f_s $$ - частота
символа $$s$$. Конечно, на практике такая точность недостижима, но мы можем
для каждого символа $$s$$ отвести в
Задачу увеличения размера интервала выполняет процедура, называемая
Пример представлен в табл. 1.4
| Составляющая, записанная в файл | Элемент, который может быть изменен переносом | Блок элементов, имеющих максимальное значение | Нижняя граница интервала |
| D7 03 56 E4 | 3A | FF FF | 35 38 B1 |
| + | |||
| Размер интервала | |||
| = | |||
| Перенос | 3B | 00 00 | 1F 4A CB |
При выполнении
Ниже приведен исходный текст алгоритма, реализующего
// Максимальное значение, которое может принимать
// переменная. Для 32-разрядной арифметики
// CODEBITS = 31. Один бит отводится для
// определения факта переноса.
#define TOP (1<<CODEBITS)
// Минимальное значение, которое может принимать
// размер интервала. Если значение меньше,
// требуется нормализация
#define BOTTOM (TOP>>8)
// На сколько битов надо сдвинуть значение нижней
// границы интервала, чтобы остался один байт
#define SHIFTBITS (CODEBITS-8)
// Если для хранения значений используется 31 бит,
// каждый символ сдвинут на 1 байт вправо
// в выходном потоке, и при декодировании приходится
// его считывать в 2 этапа.
#define EXTRABITS ((CODEBITS-1)%8+1)
// Используемые глобальные переменные:
// next_char - символ, который может быть изменен
// переносом (составляющая 2).
// carry_counter - число символов, через которые
// может пройти перенос до символа next_char
// (составляющая 3).
// low - значение нижней границы интервала,
// начальное значение равно нулю.
// range - размер интервала,
// начальное значение равно TOP.
void encode_normalize( void ) {
while( range <= BOTTOM ) {
// перенос невозможен, поэтому возможна
// запись в выходной файл (ситуация 2)
if( low < 0xFF << SHIFTBITS ) {
output_byte( next_char );
for(;carry_counter;carry_counter--)
output_byte(0xFF);
next_char = low >> SHIFTBITS;
// возник перенос (ситуация 3)
} else if( low >= TOP ) {
output_byte( next_char+1 );
for(;carry_counter;carry_counter--)
output_byte(0x0);
next_char = low >> SHIFTBITS;
// элемент, который может повлиять на перенос
// (ситуация 4)
} else {
carry_counter++;
}
range <<= 8;
low = (low << 8) (TOP-1);
}
}
void decode_normalize( void ) {
while( range <= BOTTOM ) {
range <<= 1;
low = low<<8 |
((next_char<<EXTRABITS) 0xFF);
next_char = input_byte();
low |= next_char >> (8-EXTRABITS);
range <<= 8;
}
}
Для сравнения приведем текст функции, оперирующей с битами, из работы [1.2]:
#define HALF (1<<(CODEBITS-1))
#define QUARTER (HALF>>1)
void bit_plus_follow( int bit ) {
output_bit( bit );
for(;carry_counter;carry_counter--)
output_bit(!bit);
}
void encode_normalize( void ) {
while( range <= QUARTER ) {
if( low >= HALF ) {
bit_plus_follow(1);
low -= HALF;
} else if( low + range <= HALF ) {
bit_plus_follow(0);
} else {
carry_counter++;
low -= QUARTER;
}
low <<= 1;
range <<= 1;
}
}
void decode_normalize( void ) {
while( range <= QUARTER ) {
range <<= 1;
low = low<<1 |input_bit();
}
}
Процедура интервального кодирования
void encode(
int symbol_freq, // частота кодируемого символа
int prev_freq, // накопленная частота символов,
// предшествующих кодируемому
// в алфавите
int total_freq // частота всех символов
) {
int r = range / total_freq;
low += r*prev_freq;
range = r*symbol_freq;
encode_normalize();
}
Рассмотрим пример интервального кодирования строки "КОВ.КОРОВА". Частоты символов представлены в табл. 1.5
| Индекс | Символ | Symbol_freq |
Prev_freq |
| 0 | О | 3 | 0 |
| 1 | К | 2 | 3 |
| 2 | В | 2 | 5 |
| 3 | Р | 1 | 7 |
| 4 | А | 1 | 8 |
| 5 | "" | 1 | 9 |
Total_freq |
10 |
Для кодирования строки будем использовать функцию :
void compress(
DATAFILE *DataFile // файл исходных данных
) {
low = 0;
range = TOP;
next_char = 0;
carry_counter = 0;
while( !DataFile.EOF ()) {
c = DataFile.ReadSymbol() // очередной символ
encode( Symbol_freq[c], Prev_freq[c], 10 );
}
}
В табл. 1.6 представлен результаты процесса кодирования
функцией :
| символ | symbol_freq |
prev_freq |
|
|
результат |
| 0 | 0x7FFFFFFF | ||||
| K | 2 | 3 | 0x26666664 | 0x19999998 | |
| О | 3 | 0 | 0x26666664 | 0x051EB850 | |
| В | 2 | 5 | 0x28F5C28A | 0x010624DC | |
| . | 1 | 9 | 0x29E1B07C | 0x001A36E2 | |
| 0x61B07C00 | 0x1A36E200 | 0x53 | |||
| К | 2 | 3 | 0x698DC232 | 0x053E2ECC | 0x53 |
| О | 3 | 0 | 0x698DC232 | 0x0192A7A3 | 0x53 |
| Р | 1 | 7 | 0x6AA79DEC | 0x002843F6 | 0x53 |
| 0x279DEC00 | 0x2843F600 | 0x53D5 | |||
| О | 3 | 0 | 0x279DEC00 | 0x0C146364 | 0x53D5 |
| В | 2 | 5 | 0x2DA81DAF | 0x026A7A46 | 0x53D5 |
| А | 1 | 8 | 0x2F96E5E7 | 0x003DD907 | 0x53D5 |
| 0x16E5E700 | 0x3DD90700 | 0x53D55F | |||
Как уже было отмечено, чаще всего при
#define CODEBITS 24
#define TOP (1<<CODEBITS)
#define BOTTOM (TOP>>8)
#define BIGBYTE (0xFF<<(CODEBITS-8))
void encode_normalize( void ) {
while( range < BOTTOM ) {
if( low BIGBYTE == BIGBYTE
range + (low BOTTOM-1) >= BOTTOM )
range = BOTTOM - (low BOTTOM-1);
output_byte(low>>24);
range<<=8;
low<<=8;
}
}
Можно заметить, что избежать переноса нам позволяет своевременное принудительное
уменьшение значение размера интервала. Оно происходит тогда, когда второй по старшинству
байт принимает значение 0xFF, а при добавлении к
значения размера интервала возникает перенос. Так выглядит
оптимизированная процедура
void encode_normalize( void ) {
while((low ^ low+range)<TOP ||
range < BOTTOM
((range = -low BOTTOM-1),1)) {
output_byte(low>>24);
range<<=8;
low<<=8;
}
}
void decode_normalize( void ) {
while((low ^ low+range)<TOP ||
range<BOTTOM
((range= -low BOTTOM-1),1)) {
low = low<<8 | input_byte();
range<<=8;
}
}
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.