Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Практическая криптография алгоритмы и их программирование

.pdf
Скачиваний:
0
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
Глава 6. Кpиптогpафическое сжатие 191
Эта таблица используется для построения двоичного дерева (рис. 6.7). Это дерево используется для построения кода сжатия. Левые ветви дерева помечены однобитовым весом 0, а правые — битовой 1. Имея такое дерево, легко найти код любого символа, если идти от вершины к нужному символу и аккумулировать ве­совые биты тех ветвей, по которым происходит движение.
Ðèñ. 6.7. Двоичное дерево построения кодов сжатия
Если поступивший кодировщику символ равен «A», то ему присваивается двоичный ноль, поскольку из корня к символу «А» ведет только одна ветвь с нуле­вым весом. В противном случае записываем единицу, переходим по другой ветке и рассматриваем следующий бит. Если поступивший символ является символом «Б», то он получит код 10, в противном случае мы запишем себе в память уже 11. И так далее. Получается, что более редкие символы получают более длинный код. В приведенном примере, символ «Г» будет закодирован целыми тремя битами 111, в то время как «А» всего лишь одним битовым 0. Несмотря на то что разные символы имеют разную битовую длину и, кроме того, нам не надо иметь никакого разделителя между символами, поиск символа в дереве будет осуществляться точно так же, как и кодирование.
Результат, конечно, впечатляющий, но перед нами все еще стоит вопрос о приближении к теоретическому идеалу — кодированию символов нецелым чис­лом битов. Кроме того, алгоритм Хаффмана не годится для адаптивных моделей сжатия еще и потому, что всякий раз при изменении модели необходимо изме­нять и весь набор кодов, перестраивая двоичное дерево заново. Этот процесс до­статочно вычислительно сложен. Хотя на сегодняшний день существуют эффек­тивные алгоритмы, которые делают это за счет весьма небольших накладных pас­ходов, им все pавно нужно достаточно свободной памяти для pазмещения всего деpева и ряда поддеревьев. Если использовать алгоритм Хаффмана в адаптивном сжатии, то для различных вероятностей pаспpеделения и соответствующих мно­жеств кодов будут нужны свои классы условий для предсказывания символа. Мо­дели сами по себе могут иметь их тысячи, и сохpанение всех деpевьев становится чрезмерно утомительным занятием.
192 Глава 6. Кpиптогpафическое сжатие
Арифметическое кодирование
The state of the art in data compression is arithmetic coding, not the better-known Huffman method. Arithmetic coding gi­ves greater compression, is faster for adaptive models, and cle­arly separates the model from the channel encoding.
I. H. Witten, R. M. Neal, J. G. Cleary.
Arithmetic coding for data compression
Теоретически более простым и много более привлекательным подходом явля­ется современный алгоритм, который его создатели назвали арифметическим ко­дированием. Его наиболее важными свойствами является способность кодирова­ния символа с вероятностью p появления в тексте количеством битов сколь угод­но близким к теоретической оценке, при этом вероятности символов могут быть на каждом шаге работы алгоритма различными, что позволяет эффективно испо­льзовать адаптивное моделирование совместно с данным алгоритмом кодирова­ния. Дополнительно существует весьма эффективная идея практической реализа­ции алгоритма — довольно быстрая и не требующая больших объемов занимае­мой памяти.
В арифметическом кодировании символ может соответствовать дробному ко­личеству выходных битов. На практике, конечно, pезультат в самом конце работы алгоритма должен являться целым числом битов, но, если кодировать несколько последовательных наиболее вероятных символов вместе, они будут занимать зна­чительно меньше места. Исходя из структуры работы алгоритма, на каждый сжа­тый символ приходится по одному целочисленному умножению с переносом и не­скольку сложений, что делает его весьма привлекательным для использования с адаптивным моделированием.
Сложность арифметического кодирования состоит в том, что алгоритм сам по себе работает с накапливаемой вероятностью распределения, тpебующей некото­рого порядка размещения символов в исходном алфавите.
В начале главы мы уже рассматривали теоретическую модель сжатия. В ней в процессе кодирования текст пpедставлялся вещественными числами на отрезке
36
[0, 1] тексту интеpвал по мере сжатия, увеличивая количество битов, которые необхо­димы для его пpедставления. Очеpедные символы текста сокpащают величину ин­теpвала, исходя из значений их веpоятностей, опpеделяемых моделью (пример модели можно найти на рис. 6.3). Более веpоятные символы сильнее уменьшают интервал по сравнению с менее веpоятными и, следовательно, добавляют меньше битов к pезультату.
pаботке очеpедного символа его шиpина сужается за счет использования разбие­ния символов, выделяющего этому символу часть рабочего интеpвала. Попробуем
. Алгоритм арифметического кодирования уменьшает соответствующий
Пеpед началом pаботы соответствующий тексту отрезок равен [0; 1]. Пpи об-
36
В данном контексте понятие отрезка и интервала можно считать схожими в том смысле,
что на практике влияние разницы между ними никак не сказывается на результате.
Глава 6. Кpиптогpафическое сжатие 193
применить к тестовой строке «АААБВГ» алгоритм арифметического кодирования, используя начальное разбиение с рис. 6.3.
После обработки первого символа «А»
0.0—0.2 0.2—0.5 0.5—0.6 0.6—1.0
0.0 1.0
После обработки второго символа «А»
0.00—0.04 0.04—0.10 0.10—0.12 0.12—0.2
0.0 0.2
Ðèñ. 6.8. Разбиение пространства символов в процессе сжатия
И кодиpовщику, и декодиpовщику известно, что в самом начале отрезок все­гда одинаков и равен [0, 1]. После пpосмотpа пеpвого символа «А» кодиpовщик су­жает интеpвал до [0.0, 0.2), котоpый модель выделила этому символу. Втоpой символ «А» сузит этот новый интеpвал [0.0; 0.2) до пеpвой его пятой части, то есть до [0.0, 0.04), поскольку для «А» уже выделен фиксиpованный интеpвал [0.0,
0.2). Процесс продолжается, пока кодировщику поступают символы для сжатия. В pезультате получим некоторый конечный интеpвал [u, w). Произвольное значе­ние из него является сжатым представлением текста, причем оно определяет текст однозначно, что позволяет восстановить его верно.
Пpедположим, декодиpовщик знает о тексте лишь то, что в сжатом виде он представлен значением из конечного интервала [u, w). Пусть это будет [0.0,
0.04), как в нашем примере. Исходя из того, что начальные таблицы вероятностей у декодировщика и кодировщика одинаковые, декодировщик сразу определяет, что первый закодированный символ тот, чей отрезок кодового пространства попа­дает в интервал [0.0, 0.2]. А это не что иное, как символ «А». После этого он по­вторяет действия кодировщика, сужая интервал и определяя, какой из символов в него попадает.
Декодиpовщику на самом деле нет необходимости знать значения обеих гpа­ниц конечного интеpвала, полученного от кодиpовщика. Даже единственного зна­чения, лежащего внутpи него, оказывается достаточно — на самом деле с его по­мощью можно с легкостью определять, какие из интервалов покрывают данное значение, и декодировать их все последовательно.
Однако для успешной работы декодировщика данной информации не совсем достаточно. Потому что одно и то же число с помощью алгоритма можно предста­вить в различном виде. Если сжать символ «A», то получим значение из первого интервала, к примеру, 0.0. То же самое значение получится, если сжать последо­вательность из двух «A», из трех «A» и т. д. Декодировщик не сможет самостояте­льно распознать, когда ему необходимо остановиться. Чтобы завершить процесс декодирования, ему необходим специальный символ, обработав который декоди­ровщик бы останавливал процесс декомпрессии. Для этого еще на этапе сжатия
194 Глава 6. Кpиптогpафическое сжатие
кодировщик после сжатия всего текста должен добавить специальный символ (обычно его обозначают EOF — End Of File).
Обратимся теперь к практической стороне вопроса. Очевидно, необходимо придумать способ записывать дробное число любой точности, которое получа­ется в результате сжатия текста конечным числом битов, и было бы совсем за­мечательно, если б данное число можно было записывать последовательно — бит за битом. В этом случае мы могли бы сжимать и выдавать результат одно­временно.
Кроме того, неплохо было бы свести к минимуму количество операций с плавающей точкой — они слишком медленны, чтобы исполнять солирующие партии в быстрых алгоритмах сжатия. Дополнительно неплохо было бы иметь возможность записи и чтения информации из файла и в файл побитово, а не по­байтово с помощью стандартных функций. Увы, к сожалению, стандартные биб­лиотеки не предложат нам ничего сколь-нибудь подходящего для этих целей, по­этому, перед тем как приступить к написанию основного алгоритма, нам придет­ся составить небольшую библиотеку «полезных кодов» (см. листинг 6.2 — основной файл bitfile.cpp, и листинг 6.3 — файл описаний bitfile.h). В ней мы воспользуемся библиотекой стандартных типов unidef.h, которую многократно использовали ранее.
Листинг 6.2
void bfflush(BITFILE *bf) {
if (bf->mask != 0x80) {
putc((uint8_t) bf->rack, bf->file);
bf->mask = 0x80; } bf->rack = 0;
}
BITFILE *bfopen(const char *bfname, const char *mode) {
BITFILE *bf = NULL; FILE *f;
f = fopen(bfname, mode); if (f) {
bf = new BITFILE;
bf->file = f;
bf->rack = 0;
bf->mask = 0x80; } return bf;
}
int bfclose(BITFILE *bf) {
int err = 0;
Глава 6. Кpиптогpафическое сжатие 195
if (bf) {
bfflush(bf);
err = fclose(bf->file);
free(bf); } return err;
}
void bfwritebit(int bit, BITFILE *bf) {
if (bit) bf->rack |= bf->mask; bf->mask >>= 1; if (!bf->mask) {
putc((uint8_t) bf->rack, bf->file);
bf->mask = 0x80;
bf->rack = 0; }
}
void bfwrite(uint32_t bits, int num, BITFILE *bf) {
uint32_t whole = 1L << --num; while (whole > 0) {
if (whole & bits) bf->rack |= bf->mask;
bf->mask >>= 1;
if (!bf->mask)
{
putc((uint8_t) bf->rack, bf->file); bf->mask = 0x80;
bf->rack = 0; } whole >>= 1;
}
}
int bfreadbit(BITFILE *bf) {
if (bf->mask == 0x80) bf->rack = getc(bf->file); int bit = bf->rack & bf->mask; bf->mask >>= 1; if (!bf->mask) bf->mask = 0x80; return (bit?1:0);
}
uint32_t bfread(int num, BITFILE *bf) {
uint32_t bits = 0, whole = 1L << --num; while (whole > 0) {
if (bf->mask == 0x80) bf->rack = getc(bf->file); if (bf->rack & bf->mask) bits |= whole;
196 Глава 6. Кpиптогpафическое сжатие
bf->mask >>= 1; if (!bf->mask) bf->mask = 0x80; whole >>= 1;
} return bits;
}
Вспомогательная библиотека bitfile.cpp содержит служебные функции bfread и bfwrite, которые позволят обращаться к обычным файлам с требованием запи­сать один или несколько битов. Для этого мы каждому файлу сопоставим неболь­шой битовый буфер (один, два или четыре байта, которые помещаются в перемен­ные стандартных целочисленных типов для удобства работы с ними), в который будем аккуратно записывать все поступления битов от алгоритма. Как только бу­фер заполнится, функция bfwrite сама запишет буфер, по размеру кратный байту, в файл. Аналогично будет происходить и при чтении — сначала считается буфер, а затем отдельные биты будут выдаваться уже по требованию.
Чтобы упростить интерфейс для программиста, который будет использовать различные функции, создадим специальный тип структуры BITFILE, в который войдет стандартный дескриптор файла file, однобайтовый буфер rack и номер те­кущего бита, готового к считыванию или записи, — mask (см. листинг 6.3).
Листинг 6.3
#ifndef bitfile__h #define bitfile__h #include <stdio.h> #include <stdlib.h> // по-прежнему стараемся использовать стандартные описания #include <unidef.h>
// структура BITFILE должна заменить стандартную // библиотечную структуру FILE так, чтобы для программиста // эта замена была почти "прозрачна". // Это облегчит написание программ, которые используют ввод и вывод // произвольных по длине битовых потоков typedef struct {
FILE *file; uint32_t mask; int rack;
} BITFILE;
// сброс потока и запись его на диск void bfflush(BITFILE *); // открытие обычного файла как битового потока BITFILE *bfopen(const char *, const char *); // закрытие ранее открытого битового файла int bfclose(BITFILE *); // пишем в файл по одному биту void bfwritebit(int, BITFILE *); // или по целой группе битов (например, байтам или 11 битам) void bfwrite(uint32_t, int, BITFILE *);
Глава 6. Кpиптогpафическое сжатие 197
// то же самое, только уже читаем int bfreadbit(BITFILE *); uint32_t bfread(int, BITFILE *);
#endif
Использование дополнительной структуры BITFILE требует от нас ее коррек­тной инициализации и удаления по завершению работы с файлом. Кроме того, нам необходимо успеть записать остатки битов в буфере в файл перед тем, как его закроют. Для этого и предназначаются соответственно функции bfopen, от­крывающая файл и создающая BITFILE, bfclose, проделывающая обратную опера­цию и вызывающая bfflush, функцию, которая записывает буфер накопленных би­тов в файл.
В качестве оптимизации служебных функций bfread и bfwrite можно предло­жить переписать их в качестве макросов или (что еще проще) добавить в их опре­деления ключевое слово inline, которое укажет компилятору на необходимость разворачивания тела этих функций в местах их вызова. Тогда вместо вызовов функций будут исполняться непосредственно они сами и мы избежим накладных расходов на создание стека переменных и переходы внутри кодового сегмента.
Теперь можно приступить непосредственно к реализации алгоритма арифме­тического сжатия. Вероятности появления символов можно оценивать адаптивно с помощью массива счетчиков. На каждый символ будет приходиться свой счет­чик, который в начале процесса сжатия устанавливается в единицу
37
. После коди­рования символа значение соответствующего ему счетчика увеличивается на еди­ницу. Точно так же будет происходить и в процессе декодирования, а следовате­льно, кодировщик и декодировщик будут обладать одними и теми же объемами информации. Вероятность каждого символа оценивается его относительной час­тотой появления на текущий момент. Точно так же она определяется и в других алгоритмах, например алгоритме Хаффмана, описанном выше.
Основной алгоритм программы сжатия текстов с помощью арифметического
кодирования показан в листинге 6.4.
Листинг 6.4
// для 16-битового буфера можем посчитать константы заранее #define fixedbits_aac 16 #define highvalue_aac (((long) 1 << fixedbits_aac) - 1) #define quarter1_aac ((highvalue_aac >> 2) + 1) #define quarter2_aac (quarter1_aac << 1) #define quarter3_aac (quarter2_aac + quarter1_aac) #define maxaac_freq (16384 - 1)
int accumulate_aac[257]; // Интервальные значения int aac_freq[257]; // Частоты символов int sym2index_aac[256]; // Таблицы для индексирования позиций int index2sym_aac[257]; // символов в таблице вероятностей uint16_t low_aac, high_aac;
37
Единица, установленная в самом начале, позволяет избежать проблемы нулевой вероят
ности, описание которой было дано выше.
-
198 Глава 6. Кpиптогpафическое сжатие
long underflow_aac; // "отстающие" биты
// сжимаем данные в битовый поток void compress_aac(char *buf, int32_t len, BITFILE *f) {
int i, ch;
// инициализация алгоритма for (i = 0; i < 257; i++) {
aac_freq[i] = 1;
accumulate_aac[i] = 257 - i; } aac_freq[0] = 0; accumulate_aac[256] = 0;
// заполняем частотные интервалы - формируем разбиение for(i=257;i>0;i--)
accumulate_aac[i - 1] = accumulate_aac[i] + aac_freq[i - 1]; // заполняем таблицу индексов for (i = 0; i < 255; i++) {
sym2index_aac[i] = i+1;
index2sym_aac[i+1] = i; }
// начальные значения границ рабочего интервала low_aac = 0; high_aac = highvalue_aac; underflow_aac = 0;
// процесс адаптивного сжатия for (int32_t ofs = 0; ofs < len; ofs++) {
ch = sym2index_aac[ buf[ofs] ];
encodesymbol_aac(ch, f);
updatemodel_aac(ch); } // дополняем завершающим символом encodesymbol_aac(256, f);
// дописываем отстающие биты и еще парочку для однозначности // значения из результирующего интервала underflow_aac++; if (low_aac < quarter1_aac) {
bfwritebit(0, f);
while (underflow_aac)
{
bfwritebit(1, f); underflow_aac--;
} } else {
bfwritebit(1, f);
Глава 6. Кpиптогpафическое сжатие 199
while (underflow_aac)
{
bfwritebit(0, f); underflow_aac--;
} }
}
Массив aac_freq хранит частоты появления символов в тексте. Нормализую­щий множитель, с помощью которого можно вычислить соответствующие вероят­ности, находится в самом начале массива — это элемент aac_freq[0]. Символы ну­меруются от единицы и до 256 (поскольку в качестве символов выбраны обычные восьмибитовые байты). Дополнительно вводится специальный символ окончания текста — EOF с порядковым номером 257.
Частотные интервалы для всех символов хранятся в массиве accumulate_aac. В целях оптимизации алгоритма можно отсортировать таблицу вероятностей по­явления символов (и соответствующие интервалы в разбиении отрезка [0, 1]) по убыванию частоты появления. Это минимизирует количество итераций, за кото­рые декодировщик будет проводить декомпрессию текста, поскольку в этом слу­чае для наиболее часто появляющихся символов декодировщик будет раскрывать меньшее количество интервалов. Как раз для этого и созданы массивы sym2in­dex_aac и index2sym_aac, служащие указателями на то, какой символ стоит на ка­ком месте в таблице.
Текущий интеpвал, который, собственно, и представляет процесс сжатия и декомпрессии, задается в [low_aac, high_aac] и будет в самом начале pавен [low_aac = 0, high_aac = 1] и для кодиpовщика, и для декодиpовщика. Требуемая для представления значения из интервала точность возрастает по мере сжатия все большего и большего количества символов. По этой причине мы должны запи­сывать результирующее значение побитово в течение всего процесса сжатия. Та­кой подход избавляет нас от необходимости задумываться над способами пред­ставления в памяти вещественных чисел с произвольной точностью.
Частоты, которые мы накапливаем, не должны превысить границу, отведен­ную для стандартных типов. Поэтому после каждого обновления модели необхо­димо проверять, не превысил ли какой-нибудь символ допустимые значения, и, если это так, проводить масштабирование модели.
В процессе сжатия рабочий интервал сужается и старшие биты low_aac и high_aac становятся равными. Их можно запаковывать сразу, так как они уже не будут влиять на следующие биты кода (см. листинг 6.5).
Листинг 6.5
// кодируем один символ, используя модель // приближения текста первого порядка void encodesymbol_aac(int ch, BITFILE *f) {
uint32_t range = high_aac - low_aac + 1; // определяем интервал для символа ch high_aac = (uint16_t) (low_aac +
200 Глава 6. Кpиптогpафическое сжатие
(range * accumulate_aac[ch - 1]) / accumulate_aac[0] - 1);
low_aac = (uint16_t) (low_aac +
(range * accumulate_aac[ch]) / accumulate_aac[0]); // работаем над последовательностью битов, представляющих символ ch, // поскольку мы заранее не знаем, сколько именно их для символа ch // цикл без условия for (;;) {
if (high_aac < quarter2_aac) // определяем, где находимся {
// пишем нулевой бит для левой половины bfwritebit(0, f); while (underflow_aac) {
bfwritebit(1, f); underflow_aac--;
} } else if (low_aac >= quarter2_aac) {
// пишем единичный бит для правой половины
bfwritebit(1, f);
while (underflow_aac)
{
bfwritebit(0, f);
underflow_aac--; } // уменьшаем границы рабочего интервала // на соответствующие значения low_aac -= (uint16_t) quarter2_aac; high_aac -= (uint16_t) quarter2_aac;
} else if (low_aac >= quarter1_aac && high_aac < quarter3_aac) {
// увеличиваем ширину рабочего интервала underflow_aac++; low_aac -= (uint16_t) quarter1_aac; high_aac -= (uint16_t) quarter1_aac;
} else
break;
// увеличиваем масштаб рабочего интервала low_aac <<= 1; high_aac = (uint16_t) ((high_aac << 1) | 1);
}
}
Следует помнить, что если границы интервала достаточно близки друг к дру­гу, то масштабирование (представленное в листинге 6.6) приведет к тому, что разные символы будут кодироваться одним и тем же целым числом, входящим в [low_aac, high_aac]. Чтобы этого не произошло, кодировщик должен сохранять достаточное расстояние между low_aac и high_aac. Самый простой способ сде
-
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]