Входную последовательность символов можно рассматривать как последовательность строк, содержащих произвольное количество символов. Идея словарных методов состоит в замене
Можно сказать, что мы пытаемся преобразовать исходную последовательность путем ее представления в таком
Словарь - это набор таких фраз, которые, как мы полагаем, будут встречаться в обрабатываемой последовательности. Индексы фраз должны быть построены таким образом, чтобы в среднем их представление занимало меньше места, чем требуют замещаемые строки. За счет этого и происходит сжатие.
Уменьшение размера возможно в первую очередь за счет того, что обычно в сжимаемых данных встречается лишь малая толика всех возможных строк длины n, поэтому для представления индекса фразы требуется, как правило, меньшее число битов, чем для представления исходной строки. Например, рассмотрим количество взаимно различных строк длины от 1 до 5 в тексте на русском языке (роман Ф.М. Достоевского "Бесы", обычный неформатированный текст, размер около 1.3 Мбайт) - см. табл. 2.1:
| Количество различных строк | Использовано комбинаций, % от всех возможных | |
|---|---|---|
| 5 | 196969 | 0.0004 |
| 4 | 72882 | 0.0213 |
| 3 | 17481 | 0.6949 |
| 2 | 2536 | 13.7111 |
| 1 | 136 | 100.0000 |
Иначе говоря, размер (
Далее, если у нас есть заслуживающие доверия гипотезы о частоте использования тех или иных фраз, либо проводился какой-то частотный анализ обрабатываемых данных, то мы можем назначить более вероятным фразам коды меньшей длины. Например, для той же электронной версии романа "Бесы" статистика встречаемости строк длины 5 представлена в табл. 2.2:
| N | Количество строк длины 5, встретившихся ровно N раз |
Количество относительно общего числа всех различных строк длины 5, % |
|---|---|---|
| 1 | 91227 | 46.3% |
| 2 | 30650 | 15.6% |
| 3 | 16483 | 8.4% |
| 4 | 10391 | 5.3% |
| 5 | 7224 | 3.7% |
| $$\ge$$ 6 | 40994 | 20.7% |
| Всего | 196969 | 100.0% |
Заметим, что из всех 197 тысяч различных строк длины 5 почти половина встретилась лишь один раз, поэтому они вообще не будут использованы как фразы при словарном кодировании в том случае, если словарь строится только из строк обработанной части потока. Наблюдаемые частоты оставшейся части строк быстро уменьшаются с увеличением N, что указывает на выгодность применения статистического кодирования, когда часто используемым фразам ставятся в соответствие коды меньшей длины.
Обычно же просто предполагается, что короткие фразы используются чаще длинных. Поэтому в большинстве случаев индексы строятся таким образом, чтобы длина индекса короткой фразы была меньше длины индекса длинной фразы. Такой прием обычно способствует улучшению сжатия.
Очевидно, что процессы моделирования и кодирования, рассматриваемые в главе "Методы контекстного моделирования", для словарных методов сливаются. Моделирование в явном виде может выполняться уже только для индексов. Заметим, что апологеты идеи универсальных моделирования и кодирования последовательно изучают любой метод, не вписывающийся явно в их модель, и обычно достаточно убедительно доказывают, что для него можно построить аналог в виде
Ниже будут рассмотрены алгоритмы словарного сжатия, относимые к классу методов Зива-Лемпела. В качестве примера словарного алгоритма иного класса укажем [2.7].
Методы Зива-Лемпела ориентированы на сжатие качественных данных, причем эффективность применения достигается в том случае, когда статистические характеристики обрабатываемых данных соответствуют модели источника с памятью.
Алгоритмы словарного сжатия Зива-Лемпела появились во второй половине 1970-х годов. Это были так называемые алгоритмы LZ77 и LZ78, разработанные совместно Зивом (Ziv) и Лемпелом (Lempel). В дальнейшем первоначальные схемы подвергались множественным изменениям, в результате чего мы сегодня имеем десятки достаточно самостоятельных алгоритмов и бессчетное количество модификаций.
LZ77 и LZ78 являются
Публикации Зива и Лемпела носили чисто теоретический характер, так как эти исследователи на самом деле занимались проблемой измерения "сложности" строки, и применение выработанных алгоритмов к сжатию данных явилось, скорее, лишь частным результатом. Потребовалось некоторое время, чтобы идея организации словаря, часто в переложении уже других людей, достигла разработчиков программного и
С тех пор методы данного семейства неизменно являются самыми популярными среди всех методов сжатия данных, хотя в последнее время ситуация начала меняться в пользу BWT и
Необходимо сказать несколько слов о наименованиях алгоритмов и методов. При обозначении семейства общепринятой является аббревиатура "LZ", но расшифровываться она должна как "Ziv-Lempel", поэтому и алгоритмы "Зива-Лемпела", а не "Лемпела-Зива". Согласно общепринятому объяснению этого курьеза, Якоб Зив внес больший вклад в открытие соответствующих словарных схем и исследование их свойств и таким образом заслужил, чтобы первым стояла его фамилия, что мы и видим в заголовках статей [2.12, 2.13]. Но случайно была допущена ошибка, и прикрепилось сокращение "LZ" (буквы упорядочены в алфавитном порядке). Иногда, кстати, встречается и обозначение "ZL" (порядок букв соответствует порядку фамилий авторов в публикациях [2.12, 2.13]). В дальнейшем, если некий исследователь существенно изменял какой-то алгоритм, относимый к семейству LZ, то в названии полученной модификации к строчке "LZ" обычно дописывалась первая буква его фамилии, например:
алгоритм LZB, автор Белл (
Подчеркнем также наличие большой путаницы с классификацией алгоритмов. Обычно она проявляется в нежелании признавать существование двух самостоятельных семейств LZ, а также в неправильном отнесении алгоритмов к конкретному семейству. Беспорядку часто способствуют сами разработчики: многим невыгодно раскрывать, на основе какого алгоритма создана данная модификация из-за коммерческих, патентных или иных меркантильных соображений. Например, в случае коммерческого программного обеспечения общепринятой является практика классификации используемого алгоритма сжатия как "модификации LZ77". И в этом нет ничего удивительного, ведь алгоритм LZ77 не запатентован.
Этот словарный алгоритм сжатия является самым старым среди методов LZ. Описание было опубликовано в 1977 году [2.12], но сам алгоритм разработан не позднее 1975 года.
Алгоритм LZ77 является "родоначальником" целого семейства словарных схем - так называемых алгоритмов со скользящим словарем, или
Скользящее окно имеет длину N, т.е. в него помещается N символов, и состоит из 2 частей:
W = N-n уже закодированных символов, которая и является словарем;(lookahead ), длины n ; обычно n на порядки меньше W.Пусть к текущему моменту времени мы уже закодировали t символов $$s_1 ,s_2 ,...,s_t$$. Тогда словарем будут являться W предшествующих символов $${\rm{s}}_{\rm{t}} _{{\rm{ - (W - 1)}}} {\rm{, s}}_{{\rm{t - (W - 1)}}} _{{\rm{ + 1}}} {\rm{, }}...,{\rm{ s}}_{\rm{t}}.$$ Соответственно, в буфере находятся ожидающие
Идея алгоритма заключается в поиске самого длинного совпадения между строкой буфера, начинающейся с символа $${\rm{s}}_{{\rm{t + 1}}},$$ и всеми фразами словаря. Эти фразы могут начинаться с любого символа $${\rm{s}}_{{\rm{t - (W - 1)}}} {\rm{, s}}_{{\rm{t - (W - 1) + 1}}} {\rm{, }}...,{\rm{ s}}_{\rm{t}} {\rm{ }}$$ и выходить за пределы словаря, вторгаясь в область буфера, но должны лежать в окне. Следовательно, фразы не могут начинаться с $${\rm{s}}_{{\rm{t + 1}}},$$ поэтому буфер не может сравниваться сам с собой. Длина совпадения не должна превышать размер буфера. Полученная в результате поиска фраза $${\rm{s}}_{{\rm{t - (i - 1)}}} {\rm{, s}}_{{\rm{t - (i - 1) + 1}}} {\rm{, }}...,{\rm{ s}}_{{\rm{t - (i - 1) + (j - 1) }}} $$ кодируется с помощью двух чисел:
(offset ) от начала буфера, i ;(match length ), j.Смещение и длина соответствия играют роль указателя (ссылки), однозначно определяющего фразу. Дополнительно в выходной поток записывается символ s, непосредственно следующий за совпавшей строкой буфера.
Таким образом, на каждом шаге j+1 символов вправо и осуществляется переход к новому циклу кодирования. Величина сдвига объясняется тем, что мы реально закодировали именно j+1 символов: j с помощью указателя на фразу в словаре, и 1 с помощью тривиального копирования. Передача одного символа в явном виде позволяет разрешить проблему обработки еще ни разу не виденных символов, но существенно увеличивает размер сжатого блока.
Пример
Попробуем сжать строку "кот_ломом_колол_слона" длиной 21 символ. Пусть длина буфера равна 7 символам, а размер словаря больше длины сжимаемой строки. Условимся также, что:
| Шаг | Скользящее окно | Совпадающая фраза | Закодированные данные | |||
|---|---|---|---|---|---|---|
| Словарь | Буфер | i | j | s | ||
| 1 | - | кот_лом | - | 1 | 0 | 'к' |
| 2 | к | от_ломо | - | 1 | 0 | 'о' |
| 3 | ко | т_ломом | - | 1 | 0 | 'т' |
| 4 | кот | _ломом_ | - | 1 | 0 | '_' |
| 5 | кот_ | ломом_к | - | 1 | 0 | 'л' |
| 6 | кот_л | омом_ко | о | 4 | 1 | 'м' |
| 7 | кот_лом | ом_коло | ом | 2 | 2 | '_' |
| 8 | кот_ломом_ | колол_с | ко | 10 | 2 | 'л' |
| 9 | кот_ломом_кол | ол_слон | ол | 2 | 2 | '_' |
| 10 | ..._ломом_колол_ | слона | - | 1 | 0 | 'с' |
| 11 | ...ломом_колол_с | лона | ло | 5 | 2 | 'н' |
| 12 | ...ом_колол_слон | а | - | 1 | 0 | 'а' |
Для кодирования i нам достаточно 5 битов, для j нужно 3 бита, и пусть символы требуют 1 байта для своего представления. Тогда всего мы потратим 12·(5+3+8) = 192 бита. Исходно строка занимала 21·8 = 168 битов, т.е. LZ77 кодирует нашу строку еще более расточительным образом. Не следует также забывать, что мы опустили шаг кодирования конца последовательности, который потребовал бы еще как минимум 5 битов (размер поля i = 5 битам).
Процесс кодирования можно описать следующим образом.
while ( ! DataFile.EOF() ){
/*найдем максимальное совпадение, в match_pos получим
смещение i, в match_len - длину j, в unmatched_sym
- первый несовпавший символ st+1+j; считаем также, что в
функции find_match учитывается ограничение на длину
совпадения
*/
find_match (match_pos, match_len, unmatched_sym);
/*запишем в файл сжатых данных описание найденной
фразы, при этом длина битового представления i
задается константой OFFS_LN, длина представления
j - константой LEN_LN, размер символа s принимаем
равным 8 битам
*/
CompressedFile.WriteBits (match_pos, OFFS_LN);
CompressedFile.WriteBits (match_len, LEN_LN);
CompressedFile.WriteBits (unmatched_sym, 8);
for (i = 0; i <= match_len; i++){
// прочтем очередной символ
c = DataFile.ReadSymbol();
//удалим из словаря одну самую старую фразу
DeletePhrase ();
/*добавим в словарь одну фразу, начинающуюся с
первого символа буфера
*/
AddPhrase ();
/*сдвинем окно на 1 позицию, добавим в конец буфера
символ с
*/
MoveWindow(c);
}
}
CompressedFile.WriteBits (0, OFFS_LN);
Пример подтвердил, что способ формирования кодов в LZ77 неэффективен и позволяет сжимать только сравнительно длинные последовательности. До некоторой степени сжатие небольших файлов можно улучшить, используя коды переменной длины для смещения i. Действительно, даже если мы используем словарь в 32 кбайт, но закодировали еще только 3 кбайт, то смещение реально требует не 15, а 12 битов. Кроме того, происходит существенный проигрыш из-за использования кодов одинаковой длины при указании длин совпадения j. Например, для уже упоминавшейся электронной версии романа "Бесы" были получены следующие частоты использования длин совпадения (см. табл. 2.4):
| j | Количество раз, когда максимальная длина совпадения была равна j |
|---|---|
| 0 | 136 |
| 1 | 1593 |
| 2 | 4675 |
| 3 | 11165 |
| 4 | 20047 |
| 5 | 26939 |
| 6 | 28653 |
| 7 | 24725 |
| 8 | 19702 |
| 9 | 14767 |
| 10 | 10820 |
| $$\ge$$ 11 | 27903 |
Из таблицы следует, что в целях
Хотя авторы алгоритма и доказали, что LZ77 может сжать данные не хуже, чем любой специально на них настроенный полуадаптивный словарный метод, из-за указанных недостатков это выполняется только для последовательностей достаточно большого размера.
Что касается
Алгоритм
for (;;) {
// читаем смещение
match_pos = CompressedFile.ReadBits (OFFS_LN);
if (!match_pos)
// обнаружен признак конца файла, выходим из цикла
break;
// читаем длину совпадения
match_len = CompressedFile.ReadBits (LEN_LN);
for (i = 0; i < match_len; i++) {
//находим в словаре очередной символ совпавшей фразы
c = Dict (match_pos + i);
/*сдвигаем словарь на 1 позицию, добавляем в его начало с */
MoveDict (c)
/*записываем очередной раскодированный символ в
выходной файл */
DataFile.WriteSymbol (c);
}
/*читаем несовпавший символ, добавляем его в словарь и
записываем в выходной файл
*/
c = CompressedFile.ReadBits (8);
MoveDict (c)
DataFile.WriteSymbol (c);
}
Алгоритмы со
Алгоритм LZSS позволяет достаточно гибко сочетать в выходной последовательности символы и указатели (коды фраз), что до некоторой степени устраняет присущую LZ77 расточительность, проявляющуюся в регулярной передаче одного символа в прямом виде. Эта модификация LZ77 была предложена в 1982 году Сторером (Storer) и Жимански (Szymanski) [2.10].
Идея алгоритма заключается в добавление к каждому указателю и символу однобитового
Пример
Закодируем строку "кот_ломом_колол_слона" из предыдущего примера и сравним коэффициент сжатия для LZ77 и LZSS.
Пусть мы переписываем символ в явном виде, если текущая длина максимального совпадения буфера и какой-то фразы словаря меньше или равна 1. Если мы записываем символ, то перед ним выдаем флаг со значением 0, если указатель - то со значением 1. Если имеется несколько совпадающих фраз одинаковой длины, то выбираем ближайшую к буферу.
Процесс кодирования представлен в табл. 2.5
| Шаг | Скользящее окно | Совпадающая фраза | Закодированные данные | ||||
|---|---|---|---|---|---|---|---|
| Словарь | Буфер | f | i | j | s | ||
| 1 | - | кот_лом | - | 0 | - | - | 'к' |
| 2 | к | от_ломо | - | 0 | - | - | 'о' |
| 3 | ко | т_ломом | - | 0 | - | - | 'т' |
| 4 | кот | _ломом_ | - | 0 | - | - | '_' |
| 5 | кот_ | ломом_к | - | 0 | - | - | 'л' |
| 6 | кот_л | омом_ко | о | 0 | - | - | 'о' |
| 7 | кот_ло | мом_кол | - | 0 | - | - | 'м' |
| 8 | кот_лом | ом_коло | ом | 8 | 2 | - | |
| 9 | кот_ломом | _колол_ | _ | 0 | 2 | '_' | |
| 10 | кот_ломом_ | колол_с | ко | 1 | 0 | - | |
| 11 | кот_ломом_ко | лол_сло | ло | 1 | 2 | - | |
| 12 | ...от_ломом_коло | л_слона | л | 0 | 0 | 'л' | |
| 13 | ...т_ломом_колол | _слона | _ | 0 | 0 | '_' | |
| 14 | ..._ломом_колол_ | слона | - | 0 | 0 | 'с' | |
| 15 | ...ломом_колол_с | лона | ло | 1 | 0 | - | |
| 16 | ...мом_колол_сло | на | - | 0 | 0 | 'н' | |
| 17 | ...ом_колол_слон | а | - | 0 | 0 | 'а' | |
Таким образом, для кодирования строки по алгоритму LZSS нам потребовалось 17 шагов: 13 раз символы были переданы в явном виде, и 4 раза мы применили указатели. Заметим, что при работе по алгоритму LZ77 нам потребовалось всего лишь 12 шагов. С другой стороны, если задаться теми же длинами для i и j, то размер закодированных по LZSS данных равен 13·(1+8) + 4·(1+5+3) = 153 битам. Это означает, что строка действительно была сжата, так как ее исходный размер 168 битов.
Рассмотрим алгоритм сжатия подробнее.
const int THRESHOLD = 1, // порог для включения словарного кодирования
// размер представления смещения, в битах
OFFS_LN = 14,
// размер представления длины совпадения, в битах
LEN_LN = 4;
const int WIN_SIZE = (1 << OFFS_LN), // размер окна
BUF_SIZE = (1 << LEN_LN) - 1; // размер буфера
//функция вычисления реального положения символа в окне
inline int MOD (int i) { return i (WIN_SIZE-1); };
...
//собственно алгоритм сжатия
int buf_sz = BUF_SIZE;
/* инициализация: заполнение буфера, поиск совпадения
для первого шага*/
while ( buf_sz ) {
if ( match_len > BUF_SIZE) match_len = BUF_SIZE;
if ( match_len <= THRESHOLD ) {
/*если длина совпадения меньше порога (1 в
примере), то запишем в файл сжатых данных флаг и
символ; pos определяет позицию начала буфера*/
CompressedFile.WriteBit (0);
CompressedFile.WriteBits (window [pos], 8);
// это понадобится при обновлении словаря
match_len = 1;
}else{
/*иначе запишем флаг и указатель, состоящий из
смещения и длины совпадения
*/
CompressedFile.WriteBit (1);
CompressedFile.WriteBits (match_offs, OFFS_LN);
CompressedFile.WriteBits (match_len, LEN_LN);
}
for (int i = 0; i < match_len; i++) {
/*удалим из словаря фразу, начинающуюся в позиции
MOD (pos+buf_sz)
*/
DeletePhrase ( MOD (pos+buf_sz) );
if ( (c = DataFile.ReadSymbol ()) == EOF) buf_sz--;
// мы в конце файла, надо сократить буфер
else window [MOD (pos+buf_sz)] = c;
/*иначе надо добавить в конец буфера новый символ*/
pos = MOD (pos+1); // сдвиг окна на 1 символ
if (buf_sz) AddPhrase (pos, match_offs, match_len);
/*если в буфере еще что-то есть, то добавим в
словарь новую фразу, начинающуюся в позиции pos;
считаем, что в функции AddPhrase одновременно
выполняется поиск максимального совпадения
между буфером и фразами словаря
*/
}
}
CompressedFile.WriteBit (1);
CompressedFile.WriteBits (0, OFFS_LN); // знак конца файла
}
THRESHOLD часть допустимых значений длины реально не используется, поэтому размер буфера BUF_SIZE может быть увеличен при неизменном LEN_LN. Проделайте соответствующие модификации фрагментов программ кодирования и Алгоритм LZ78 был опубликован в 1978 году [2.13], и впоследствии стал "отцом" семейства словарных методов LZ78.
Алгоритмы этой группы не используют S словаря, имеющей самое длинное совпадение со строкой буфера, и символа s. Символ s является символом, следующим за строкой буфера, для которой найдена совпадающая фраза S. В отличие от семейства LZ77, в словаре не может быть одинаковых фраз.
n "родительской" фразы S, или s.
В начале обработки словарь пуст. Далее, теоретически, словарь может расти бесконечно, т.е. на его рост сам алгоритм не налагает ограничений. На практике при достижении определенного объема занимаемой памяти словарь должен очищаться полностью или частично.
Пример
И еще раз закодируем строку "кот_ломом_колол_слона" длиной 21 символ. Для LZ78 буфер, в принципе, не требуется, поскольку достаточно легко так реализовать поиск совпадающей фразы максимальной длины, что последовательность незакодированных символов будет просматриваться только один раз. Поэтому буфер показан только с целью большей доходчивости примера. Фразу с номером 0 зарезервируем для обозначения конца сжатой строки, номером 1 будем задавать пустую фразу словаря.
Строку удалось закодировать за 13 шагов. Так как на каждом шаге выдавался один код, сжатая последовательность состоит из 13 кодов. Возможно использование 15 номеров фраз (от 0 до 14), поэтому для представления n посредством кодов фиксированной длины нам потребуется 4 бита. Тогда размер сжатой строки равен 13·(4+8) = 156 битам.
Ниже приведен пример реализации алгоритма сжатия LZ78.
n = 1;
while ( ! DataFile.EOF() ){
s = DataFile.ReadSymbol; // читаем очередной символ
/*пытаемся найти в словаре фразу, представляющую
собой конкатенацию родительской фразы с номером n и
символа s; функция возвращает номер искомой фразы
в phrase_num; если же фразы нет, то phrase_num
принимает значение 1, т.е. указывает на пустую фразу
*/
FindPhrase (phrase_num, n, s);
if (phrase_num != 1) n = phrase_num;
/*такая фраза имеется в словаре, продолжим поиск
совпадающей фразы максимальной длины
*/
else {
/*такой фразы нет, запишем в выходной файл код;
INDEX_LN - это константа, определяющая длину
битового представления номера n
*/
CompressedFile.WriteBits (n, INDEX_LN);
CompressedFile.WriteBits (s, 8);
AddPhrase (n, s); // добавим фразу в словарь
n = 1; // подготовимся к следующему шагу
}
}
// признак конца файла
CompressedFile.WriteBits (0, INDEX_LN);
При
for (;;){
// читаем индекс родительской фразы
n = CompressedFile.ReadBits (INDEX_LN);
if (!n) break; // конец файла
// читаем несовпавший символ s
s = CompressedFile.ReadBits (8);
/*находим в словаре позицию начала фразы с индексом n
и ее длину
*/
GetPhrase (pos, len, n)
/*записываем фразу с индексом n в файл
раскодированных данных
*/
for (i = 0; i < len; i++) DataFile.WriteSymbol (Dict[pos+i]);
// записываем в файл символ s
DataFile.WriteSymbol (s);
AddPhrase (n, s); // добавляем новую фразу в словарь
}
Очевидно, что скорость раскодирования для алгоритмов семейства LZ78 потенциально всегда меньше скорости для алгоритмов со
Несмотря на относительную быстроту кодирования LZ78, при грамотной реализации алгоритма оно все же медленнее
Интересное свойство LZ78 заключается в том, что если исходные данные порождены источником с определенными характеристиками (он должен быть
Доказано, что аналогичным свойством
Входную последовательность символов можно рассматривать как последовательность строк, содержащих произвольное количество символов. Идея словарных методов состоит в замене
Можно сказать, что мы пытаемся преобразовать исходную последовательность путем ее представления в таком
Словарь - это набор таких фраз, которые, как мы полагаем, будут встречаться в обрабатываемой последовательности. Индексы фраз должны быть построены таким образом, чтобы в среднем их представление занимало меньше места, чем требуют замещаемые строки. За счет этого и происходит сжатие.
Уменьшение размера возможно в первую очередь за счет того, что обычно в сжимаемых данных встречается лишь малая толика всех возможных строк длины n, поэтому для представления индекса фразы требуется, как правило, меньшее число битов, чем для представления исходной строки. Например, рассмотрим количество взаимно различных строк длины от 1 до 5 в тексте на русском языке (роман Ф.М. Достоевского "Бесы", обычный неформатированный текст, размер около 1.3 Мбайт) - см. табл. 2.1:
| Количество различных строк | Использовано комбинаций, % от всех возможных | |
|---|---|---|
| 5 | 196969 | 0.0004 |
| 4 | 72882 | 0.0213 |
| 3 | 17481 | 0.6949 |
| 2 | 2536 | 13.7111 |
| 1 | 136 | 100.0000 |
Иначе говоря, размер (
Далее, если у нас есть заслуживающие доверия гипотезы о частоте использования тех или иных фраз, либо проводился какой-то частотный анализ обрабатываемых данных, то мы можем назначить более вероятным фразам коды меньшей длины. Например, для той же электронной версии романа "Бесы" статистика встречаемости строк длины 5 представлена в табл. 2.2:
| N | Количество строк длины 5, встретившихся ровно N раз |
Количество относительно общего числа всех различных строк длины 5, % |
|---|---|---|
| 1 | 91227 | 46.3% |
| 2 | 30650 | 15.6% |
| 3 | 16483 | 8.4% |
| 4 | 10391 | 5.3% |
| 5 | 7224 | 3.7% |
| $$\ge$$ 6 | 40994 | 20.7% |
| Всего | 196969 | 100.0% |
Заметим, что из всех 197 тысяч различных строк длины 5 почти половина встретилась лишь один раз, поэтому они вообще не будут использованы как фразы при словарном кодировании в том случае, если словарь строится только из строк обработанной части потока. Наблюдаемые частоты оставшейся части строк быстро уменьшаются с увеличением N, что указывает на выгодность применения статистического кодирования, когда часто используемым фразам ставятся в соответствие коды меньшей длины.
Обычно же просто предполагается, что короткие фразы используются чаще длинных. Поэтому в большинстве случаев индексы строятся таким образом, чтобы длина индекса короткой фразы была меньше длины индекса длинной фразы. Такой прием обычно способствует улучшению сжатия.
Очевидно, что процессы моделирования и кодирования, рассматриваемые в главе "Методы контекстного моделирования", для словарных методов сливаются. Моделирование в явном виде может выполняться уже только для индексов. Заметим, что апологеты идеи универсальных моделирования и кодирования последовательно изучают любой метод, не вписывающийся явно в их модель, и обычно достаточно убедительно доказывают, что для него можно построить аналог в виде
Ниже будут рассмотрены алгоритмы словарного сжатия, относимые к классу методов Зива-Лемпела. В качестве примера словарного алгоритма иного класса укажем [2.7].
Методы Зива-Лемпела ориентированы на сжатие качественных данных, причем эффективность применения достигается в том случае, когда статистические характеристики обрабатываемых данных соответствуют модели источника с памятью.
Алгоритмы словарного сжатия Зива-Лемпела появились во второй половине 1970-х годов. Это были так называемые алгоритмы LZ77 и LZ78, разработанные совместно Зивом (Ziv) и Лемпелом (Lempel). В дальнейшем первоначальные схемы подвергались множественным изменениям, в результате чего мы сегодня имеем десятки достаточно самостоятельных алгоритмов и бессчетное количество модификаций.
LZ77 и LZ78 являются
Публикации Зива и Лемпела носили чисто теоретический характер, так как эти исследователи на самом деле занимались проблемой измерения "сложности" строки, и применение выработанных алгоритмов к сжатию данных явилось, скорее, лишь частным результатом. Потребовалось некоторое время, чтобы идея организации словаря, часто в переложении уже других людей, достигла разработчиков программного и
С тех пор методы данного семейства неизменно являются самыми популярными среди всех методов сжатия данных, хотя в последнее время ситуация начала меняться в пользу BWT и
Необходимо сказать несколько слов о наименованиях алгоритмов и методов. При обозначении семейства общепринятой является аббревиатура "LZ", но расшифровываться она должна как "Ziv-Lempel", поэтому и алгоритмы "Зива-Лемпела", а не "Лемпела-Зива". Согласно общепринятому объяснению этого курьеза, Якоб Зив внес больший вклад в открытие соответствующих словарных схем и исследование их свойств и таким образом заслужил, чтобы первым стояла его фамилия, что мы и видим в заголовках статей [2.12, 2.13]. Но случайно была допущена ошибка, и прикрепилось сокращение "LZ" (буквы упорядочены в алфавитном порядке). Иногда, кстати, встречается и обозначение "ZL" (порядок букв соответствует порядку фамилий авторов в публикациях [2.12, 2.13]). В дальнейшем, если некий исследователь существенно изменял какой-то алгоритм, относимый к семейству LZ, то в названии полученной модификации к строчке "LZ" обычно дописывалась первая буква его фамилии, например:
алгоритм LZB, автор Белл (
Подчеркнем также наличие большой путаницы с классификацией алгоритмов. Обычно она проявляется в нежелании признавать существование двух самостоятельных семейств LZ, а также в неправильном отнесении алгоритмов к конкретному семейству. Беспорядку часто способствуют сами разработчики: многим невыгодно раскрывать, на основе какого алгоритма создана данная модификация из-за коммерческих, патентных или иных меркантильных соображений. Например, в случае коммерческого программного обеспечения общепринятой является практика классификации используемого алгоритма сжатия как "модификации LZ77". И в этом нет ничего удивительного, ведь алгоритм LZ77 не запатентован.
Этот словарный алгоритм сжатия является самым старым среди методов LZ. Описание было опубликовано в 1977 году [2.12], но сам алгоритм разработан не позднее 1975 года.
Алгоритм LZ77 является "родоначальником" целого семейства словарных схем - так называемых алгоритмов со скользящим словарем, или
Скользящее окно имеет длину N, т.е. в него помещается N символов, и состоит из 2 частей:
W = N-n уже закодированных символов, которая и является словарем;(lookahead ), длины n ; обычно n на порядки меньше W.Пусть к текущему моменту времени мы уже закодировали t символов $$s_1 ,s_2 ,...,s_t$$. Тогда словарем будут являться W предшествующих символов $${\rm{s}}_{\rm{t}} _{{\rm{ - (W - 1)}}} {\rm{, s}}_{{\rm{t - (W - 1)}}} _{{\rm{ + 1}}} {\rm{, }}...,{\rm{ s}}_{\rm{t}}.$$ Соответственно, в буфере находятся ожидающие
Идея алгоритма заключается в поиске самого длинного совпадения между строкой буфера, начинающейся с символа $${\rm{s}}_{{\rm{t + 1}}},$$ и всеми фразами словаря. Эти фразы могут начинаться с любого символа $${\rm{s}}_{{\rm{t - (W - 1)}}} {\rm{, s}}_{{\rm{t - (W - 1) + 1}}} {\rm{, }}...,{\rm{ s}}_{\rm{t}} {\rm{ }}$$ и выходить за пределы словаря, вторгаясь в область буфера, но должны лежать в окне. Следовательно, фразы не могут начинаться с $${\rm{s}}_{{\rm{t + 1}}},$$ поэтому буфер не может сравниваться сам с собой. Длина совпадения не должна превышать размер буфера. Полученная в результате поиска фраза $${\rm{s}}_{{\rm{t - (i - 1)}}} {\rm{, s}}_{{\rm{t - (i - 1) + 1}}} {\rm{, }}...,{\rm{ s}}_{{\rm{t - (i - 1) + (j - 1) }}} $$ кодируется с помощью двух чисел:
(offset ) от начала буфера, i ;(match length ), j.Смещение и длина соответствия играют роль указателя (ссылки), однозначно определяющего фразу. Дополнительно в выходной поток записывается символ s, непосредственно следующий за совпавшей строкой буфера.
Таким образом, на каждом шаге j+1 символов вправо и осуществляется переход к новому циклу кодирования. Величина сдвига объясняется тем, что мы реально закодировали именно j+1 символов: j с помощью указателя на фразу в словаре, и 1 с помощью тривиального копирования. Передача одного символа в явном виде позволяет разрешить проблему обработки еще ни разу не виденных символов, но существенно увеличивает размер сжатого блока.
Пример
Попробуем сжать строку "кот_ломом_колол_слона" длиной 21 символ. Пусть длина буфера равна 7 символам, а размер словаря больше длины сжимаемой строки. Условимся также, что:
| Шаг | Скользящее окно | Совпадающая фраза | Закодированные данные | |||
|---|---|---|---|---|---|---|
| Словарь | Буфер | i | j | s | ||
| 1 | - | кот_лом | - | 1 | 0 | 'к' |
| 2 | к | от_ломо | - | 1 | 0 | 'о' |
| 3 | ко | т_ломом | - | 1 | 0 | 'т' |
| 4 | кот | _ломом_ | - | 1 | 0 | '_' |
| 5 | кот_ | ломом_к | - | 1 | 0 | 'л' |
| 6 | кот_л | омом_ко | о | 4 | 1 | 'м' |
| 7 | кот_лом | ом_коло | ом | 2 | 2 | '_' |
| 8 | кот_ломом_ | колол_с | ко | 10 | 2 | 'л' |
| 9 | кот_ломом_кол | ол_слон | ол | 2 | 2 | '_' |
| 10 | ..._ломом_колол_ | слона | - | 1 | 0 | 'с' |
| 11 | ...ломом_колол_с | лона | ло | 5 | 2 | 'н' |
| 12 | ...ом_колол_слон | а | - | 1 | 0 | 'а' |
Для кодирования i нам достаточно 5 битов, для j нужно 3 бита, и пусть символы требуют 1 байта для своего представления. Тогда всего мы потратим 12·(5+3+8) = 192 бита. Исходно строка занимала 21·8 = 168 битов, т.е. LZ77 кодирует нашу строку еще более расточительным образом. Не следует также забывать, что мы опустили шаг кодирования конца последовательности, который потребовал бы еще как минимум 5 битов (размер поля i = 5 битам).
Процесс кодирования можно описать следующим образом.
while ( ! DataFile.EOF() ){
/*найдем максимальное совпадение, в match_pos получим
смещение i, в match_len - длину j, в unmatched_sym
- первый несовпавший символ st+1+j; считаем также, что в
функции find_match учитывается ограничение на длину
совпадения
*/
find_match (match_pos, match_len, unmatched_sym);
/*запишем в файл сжатых данных описание найденной
фразы, при этом длина битового представления i
задается константой OFFS_LN, длина представления
j - константой LEN_LN, размер символа s принимаем
равным 8 битам
*/
CompressedFile.WriteBits (match_pos, OFFS_LN);
CompressedFile.WriteBits (match_len, LEN_LN);
CompressedFile.WriteBits (unmatched_sym, 8);
for (i = 0; i <= match_len; i++){
// прочтем очередной символ
c = DataFile.ReadSymbol();
//удалим из словаря одну самую старую фразу
DeletePhrase ();
/*добавим в словарь одну фразу, начинающуюся с
первого символа буфера
*/
AddPhrase ();
/*сдвинем окно на 1 позицию, добавим в конец буфера
символ с
*/
MoveWindow(c);
}
}
CompressedFile.WriteBits (0, OFFS_LN);
Пример подтвердил, что способ формирования кодов в LZ77 неэффективен и позволяет сжимать только сравнительно длинные последовательности. До некоторой степени сжатие небольших файлов можно улучшить, используя коды переменной длины для смещения i. Действительно, даже если мы используем словарь в 32 кбайт, но закодировали еще только 3 кбайт, то смещение реально требует не 15, а 12 битов. Кроме того, происходит существенный проигрыш из-за использования кодов одинаковой длины при указании длин совпадения j. Например, для уже упоминавшейся электронной версии романа "Бесы" были получены следующие частоты использования длин совпадения (см. табл. 2.4):
| j | Количество раз, когда максимальная длина совпадения была равна j |
|---|---|
| 0 | 136 |
| 1 | 1593 |
| 2 | 4675 |
| 3 | 11165 |
| 4 | 20047 |
| 5 | 26939 |
| 6 | 28653 |
| 7 | 24725 |
| 8 | 19702 |
| 9 | 14767 |
| 10 | 10820 |
| $$\ge$$ 11 | 27903 |
Из таблицы следует, что в целях
Хотя авторы алгоритма и доказали, что LZ77 может сжать данные не хуже, чем любой специально на них настроенный полуадаптивный словарный метод, из-за указанных недостатков это выполняется только для последовательностей достаточно большого размера.
Что касается
Алгоритм
for (;;) {
// читаем смещение
match_pos = CompressedFile.ReadBits (OFFS_LN);
if (!match_pos)
// обнаружен признак конца файла, выходим из цикла
break;
// читаем длину совпадения
match_len = CompressedFile.ReadBits (LEN_LN);
for (i = 0; i < match_len; i++) {
//находим в словаре очередной символ совпавшей фразы
c = Dict (match_pos + i);
/*сдвигаем словарь на 1 позицию, добавляем в его начало с */
MoveDict (c)
/*записываем очередной раскодированный символ в
выходной файл */
DataFile.WriteSymbol (c);
}
/*читаем несовпавший символ, добавляем его в словарь и
записываем в выходной файл
*/
c = CompressedFile.ReadBits (8);
MoveDict (c)
DataFile.WriteSymbol (c);
}
Алгоритмы со
Алгоритм LZSS позволяет достаточно гибко сочетать в выходной последовательности символы и указатели (коды фраз), что до некоторой степени устраняет присущую LZ77 расточительность, проявляющуюся в регулярной передаче одного символа в прямом виде. Эта модификация LZ77 была предложена в 1982 году Сторером (Storer) и Жимански (Szymanski) [2.10].
Идея алгоритма заключается в добавление к каждому указателю и символу однобитового
Пример
Закодируем строку "кот_ломом_колол_слона" из предыдущего примера и сравним коэффициент сжатия для LZ77 и LZSS.
Пусть мы переписываем символ в явном виде, если текущая длина максимального совпадения буфера и какой-то фразы словаря меньше или равна 1. Если мы записываем символ, то перед ним выдаем флаг со значением 0, если указатель - то со значением 1. Если имеется несколько совпадающих фраз одинаковой длины, то выбираем ближайшую к буферу.
Процесс кодирования представлен в табл. 2.5
| Шаг | Скользящее окно | Совпадающая фраза | Закодированные данные | ||||
|---|---|---|---|---|---|---|---|
| Словарь | Буфер | f | i | j | s | ||
| 1 | - | кот_лом | - | 0 | - | - | 'к' |
| 2 | к | от_ломо | - | 0 | - | - | 'о' |
| 3 | ко | т_ломом | - | 0 | - | - | 'т' |
| 4 | кот | _ломом_ | - | 0 | - | - | '_' |
| 5 | кот_ | ломом_к | - | 0 | - | - | 'л' |
| 6 | кот_л | омом_ко | о | 0 | - | - | 'о' |
| 7 | кот_ло | мом_кол | - | 0 | - | - | 'м' |
| 8 | кот_лом | ом_коло | ом | 8 | 2 | - | |
| 9 | кот_ломом | _колол_ | _ | 0 | 2 | '_' | |
| 10 | кот_ломом_ | колол_с | ко | 1 | 0 | - | |
| 11 | кот_ломом_ко | лол_сло | ло | 1 | 2 | - | |
| 12 | ...от_ломом_коло | л_слона | л | 0 | 0 | 'л' | |
| 13 | ...т_ломом_колол | _слона | _ | 0 | 0 | '_' | |
| 14 | ..._ломом_колол_ | слона | - | 0 | 0 | 'с' | |
| 15 | ...ломом_колол_с | лона | ло | 1 | 0 | - | |
| 16 | ...мом_колол_сло | на | - | 0 | 0 | 'н' | |
| 17 | ...ом_колол_слон | а | - | 0 | 0 | 'а' | |
Таким образом, для кодирования строки по алгоритму LZSS нам потребовалось 17 шагов: 13 раз символы были переданы в явном виде, и 4 раза мы применили указатели. Заметим, что при работе по алгоритму LZ77 нам потребовалось всего лишь 12 шагов. С другой стороны, если задаться теми же длинами для i и j, то размер закодированных по LZSS данных равен 13·(1+8) + 4·(1+5+3) = 153 битам. Это означает, что строка действительно была сжата, так как ее исходный размер 168 битов.
Рассмотрим алгоритм сжатия подробнее.
const int THRESHOLD = 1, // порог для включения словарного кодирования
// размер представления смещения, в битах
OFFS_LN = 14,
// размер представления длины совпадения, в битах
LEN_LN = 4;
const int WIN_SIZE = (1 << OFFS_LN), // размер окна
BUF_SIZE = (1 << LEN_LN) - 1; // размер буфера
//функция вычисления реального положения символа в окне
inline int MOD (int i) { return i (WIN_SIZE-1); };
...
//собственно алгоритм сжатия
int buf_sz = BUF_SIZE;
/* инициализация: заполнение буфера, поиск совпадения
для первого шага*/
while ( buf_sz ) {
if ( match_len > BUF_SIZE) match_len = BUF_SIZE;
if ( match_len <= THRESHOLD ) {
/*если длина совпадения меньше порога (1 в
примере), то запишем в файл сжатых данных флаг и
символ; pos определяет позицию начала буфера*/
CompressedFile.WriteBit (0);
CompressedFile.WriteBits (window [pos], 8);
// это понадобится при обновлении словаря
match_len = 1;
}else{
/*иначе запишем флаг и указатель, состоящий из
смещения и длины совпадения
*/
CompressedFile.WriteBit (1);
CompressedFile.WriteBits (match_offs, OFFS_LN);
CompressedFile.WriteBits (match_len, LEN_LN);
}
for (int i = 0; i < match_len; i++) {
/*удалим из словаря фразу, начинающуюся в позиции
MOD (pos+buf_sz)
*/
DeletePhrase ( MOD (pos+buf_sz) );
if ( (c = DataFile.ReadSymbol ()) == EOF) buf_sz--;
// мы в конце файла, надо сократить буфер
else window [MOD (pos+buf_sz)] = c;
/*иначе надо добавить в конец буфера новый символ*/
pos = MOD (pos+1); // сдвиг окна на 1 символ
if (buf_sz) AddPhrase (pos, match_offs, match_len);
/*если в буфере еще что-то есть, то добавим в
словарь новую фразу, начинающуюся в позиции pos;
считаем, что в функции AddPhrase одновременно
выполняется поиск максимального совпадения
между буфером и фразами словаря
*/
}
}
CompressedFile.WriteBit (1);
CompressedFile.WriteBits (0, OFFS_LN); // знак конца файла
}
THRESHOLD часть допустимых значений длины реально не используется, поэтому размер буфера BUF_SIZE может быть увеличен при неизменном LEN_LN. Проделайте соответствующие модификации фрагментов программ кодирования и Алгоритм LZ78 был опубликован в 1978 году [2.13], и впоследствии стал "отцом" семейства словарных методов LZ78.
Алгоритмы этой группы не используют S словаря, имеющей самое длинное совпадение со строкой буфера, и символа s. Символ s является символом, следующим за строкой буфера, для которой найдена совпадающая фраза S. В отличие от семейства LZ77, в словаре не может быть одинаковых фраз.
n "родительской" фразы S, или s.
В начале обработки словарь пуст. Далее, теоретически, словарь может расти бесконечно, т.е. на его рост сам алгоритм не налагает ограничений. На практике при достижении определенного объема занимаемой памяти словарь должен очищаться полностью или частично.
Пример
И еще раз закодируем строку "кот_ломом_колол_слона" длиной 21 символ. Для LZ78 буфер, в принципе, не требуется, поскольку достаточно легко так реализовать поиск совпадающей фразы максимальной длины, что последовательность незакодированных символов будет просматриваться только один раз. Поэтому буфер показан только с целью большей доходчивости примера. Фразу с номером 0 зарезервируем для обозначения конца сжатой строки, номером 1 будем задавать пустую фразу словаря.
Строку удалось закодировать за 13 шагов. Так как на каждом шаге выдавался один код, сжатая последовательность состоит из 13 кодов. Возможно использование 15 номеров фраз (от 0 до 14), поэтому для представления n посредством кодов фиксированной длины нам потребуется 4 бита. Тогда размер сжатой строки равен 13·(4+8) = 156 битам.
Ниже приведен пример реализации алгоритма сжатия LZ78.
n = 1;
while ( ! DataFile.EOF() ){
s = DataFile.ReadSymbol; // читаем очередной символ
/*пытаемся найти в словаре фразу, представляющую
собой конкатенацию родительской фразы с номером n и
символа s; функция возвращает номер искомой фразы
в phrase_num; если же фразы нет, то phrase_num
принимает значение 1, т.е. указывает на пустую фразу
*/
FindPhrase (phrase_num, n, s);
if (phrase_num != 1) n = phrase_num;
/*такая фраза имеется в словаре, продолжим поиск
совпадающей фразы максимальной длины
*/
else {
/*такой фразы нет, запишем в выходной файл код;
INDEX_LN - это константа, определяющая длину
битового представления номера n
*/
CompressedFile.WriteBits (n, INDEX_LN);
CompressedFile.WriteBits (s, 8);
AddPhrase (n, s); // добавим фразу в словарь
n = 1; // подготовимся к следующему шагу
}
}
// признак конца файла
CompressedFile.WriteBits (0, INDEX_LN);
При
for (;;){
// читаем индекс родительской фразы
n = CompressedFile.ReadBits (INDEX_LN);
if (!n) break; // конец файла
// читаем несовпавший символ s
s = CompressedFile.ReadBits (8);
/*находим в словаре позицию начала фразы с индексом n
и ее длину
*/
GetPhrase (pos, len, n)
/*записываем фразу с индексом n в файл
раскодированных данных
*/
for (i = 0; i < len; i++) DataFile.WriteSymbol (Dict[pos+i]);
// записываем в файл символ s
DataFile.WriteSymbol (s);
AddPhrase (n, s); // добавляем новую фразу в словарь
}
Очевидно, что скорость раскодирования для алгоритмов семейства LZ78 потенциально всегда меньше скорости для алгоритмов со
Несмотря на относительную быстроту кодирования LZ78, при грамотной реализации алгоритма оно все же медленнее
Интересное свойство LZ78 заключается в том, что если исходные данные порождены источником с определенными характеристиками (он должен быть
Доказано, что аналогичным свойством
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.