Методы сжатия изображений

Словарные методы сжатия данных

Разбить на страницы
Показывать лекцию целиком

Идея словарных методов

Входную последовательность символов можно рассматривать как последовательность строк, содержащих произвольное количество символов. Идея словарных методов состоит в замене строк символов на такие коды, что их можно трактовать как индексы строк некоторого словаря. Образующие словарь строки будем далее называть фразами. При декодировании осуществляется обратная замена индекса на соответствующую ему фразу словаря.

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

Словарь - это набор таких фраз, которые, как мы полагаем, будут встречаться в обрабатываемой последовательности. Индексы фраз должны быть построены таким образом, чтобы в среднем их представление занимало меньше места, чем требуют замещаемые строки. За счет этого и происходит сжатие.

Уменьшение размера возможно в первую очередь за счет того, что обычно в сжимаемых данных встречается лишь малая толика всех возможных строк длины 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

Иначе говоря, размер (мощность) алфавита равен 136 символам, но реально используется только $${{{\rm{2536}}} \over {136 \cdot 136}} \cdot 100\% \approx 13.7\%$$ от всех возможных двухсимвольных строк, и т.д.

Далее, если у нас есть заслуживающие доверия гипотезы о частоте использования тех или иных фраз, либо проводился какой-то частотный анализ обрабатываемых данных, то мы можем назначить более вероятным фразам коды меньшей длины. Например, для той же электронной версии романа "Бесы" статистика встречаемости строк длины 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.6, 2.9].

Ниже будут рассмотрены алгоритмы словарного сжатия, относимые к классу методов Зива-Лемпела. В качестве примера словарного алгоритма иного класса укажем [2.7].

Методы Зива-Лемпела ориентированы на сжатие качественных данных, причем эффективность применения достигается в том случае, когда статистические характеристики обрабатываемых данных соответствуют модели источника с памятью.

Классические алгоритмы Зива-Лемпела

Алгоритмы словарного сжатия Зива-Лемпела появились во второй половине 1970-х годов. Это были так называемые алгоритмы LZ77 и LZ78, разработанные совместно Зивом (Ziv) и Лемпелом (Lempel). В дальнейшем первоначальные схемы подвергались множественным изменениям, в результате чего мы сегодня имеем десятки достаточно самостоятельных алгоритмов и бессчетное количество модификаций.

LZ77 и LZ78 являются универсальными алгоритмами сжатия, в которых словарь формируется на основании уже обработанной части входного потока, т.е. адаптивно. Принципиальным отличием является лишь способ формирования фраз. В модификациях первоначальных алгоритмов это свойство сохраняется. Поэтому словарные алгоритмы Зива-Лемпела разделяют на два семейства - алгоритмы типа LZ77 и алгоритмы типа LZ78. Иногда также говорят о словарных методах LZ1 и LZ2.

Публикации Зива и Лемпела носили чисто теоретический характер, так как эти исследователи на самом деле занимались проблемой измерения "сложности" строки, и применение выработанных алгоритмов к сжатию данных явилось, скорее, лишь частным результатом. Потребовалось некоторое время, чтобы идея организации словаря, часто в переложении уже других людей, достигла разработчиков программного и аппаратного обеспечения. Поэтому практическое использование алгоритмов началось спустя пару лет.

С тех пор методы данного семейства неизменно являются самыми популярными среди всех методов сжатия данных, хотя в последнее время ситуация начала меняться в пользу BWT и PPM, как обеспечивающих лучшее сжатие. Кроме того, практически все реально используемые словарные алгоритмы относятся к семейству Зива-Лемпела.

Необходимо сказать несколько слов о наименованиях алгоритмов и методов. При обозначении семейства общепринятой является аббревиатура "LZ", но расшифровываться она должна как "Ziv-Lempel", поэтому и алгоритмы "Зива-Лемпела", а не "Лемпела-Зива". Согласно общепринятому объяснению этого курьеза, Якоб Зив внес больший вклад в открытие соответствующих словарных схем и исследование их свойств и таким образом заслужил, чтобы первым стояла его фамилия, что мы и видим в заголовках статей [2.12, 2.13]. Но случайно была допущена ошибка, и прикрепилось сокращение "LZ" (буквы упорядочены в алфавитном порядке). Иногда, кстати, встречается и обозначение "ZL" (порядок букв соответствует порядку фамилий авторов в публикациях [2.12, 2.13]). В дальнейшем, если некий исследователь существенно изменял какой-то алгоритм, относимый к семейству LZ, то в названии полученной модификации к строчке "LZ" обычно дописывалась первая буква его фамилии, например: алгоритм LZB, автор Белл (Bell).

Подчеркнем также наличие большой путаницы с классификацией алгоритмов. Обычно она проявляется в нежелании признавать существование двух самостоятельных семейств LZ, а также в неправильном отнесении алгоритмов к конкретному семейству. Беспорядку часто способствуют сами разработчики: многим невыгодно раскрывать, на основе какого алгоритма создана данная модификация из-за коммерческих, патентных или иных меркантильных соображений. Например, в случае коммерческого программного обеспечения общепринятой является практика классификации используемого алгоритма сжатия как "модификации LZ77". И в этом нет ничего удивительного, ведь алгоритм LZ77 не запатентован.

Алгоритм LZ77

Этот словарный алгоритм сжатия является самым старым среди методов LZ. Описание было опубликовано в 1977 году [2.12], но сам алгоритм разработан не позднее 1975 года.

Алгоритм LZ77 является "родоначальником" целого семейства словарных схем - так называемых алгоритмов со скользящим словарем, или скользящим окном. Действительно, в 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 + 2}}} {\rm{, }}...,{\rm{ s}}_{{\rm{t + n}}} {\rm{. }}$$. Очевидно, что если $${\rm{W}} \ge {\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, непосредственно следующий за совпавшей строкой буфера.

    Таким образом, на каждом шаге кодер выдает описание трех объектов: смещения и длины соответствия, образующих код фразы, равной обработанной строке буфера, и одного символа s (литерала). Затем окно смещается на j+1 символов вправо и осуществляется переход к новому циклу кодирования. Величина сдвига объясняется тем, что мы реально закодировали именно j+1 символов: j с помощью указателя на фразу в словаре, и 1 с помощью тривиального копирования. Передача одного символа в явном виде позволяет разрешить проблему обработки еще ни разу не виденных символов, но существенно увеличивает размер сжатого блока.

    Пример

    Попробуем сжать строку "кот_ломом_колол_слона" длиной 21 символ. Пусть длина буфера равна 7 символам, а размер словаря больше длины сжимаемой строки. Условимся также, что:

  • нулевое смещение зарезервировали для обозначения конца кодирования;
  • символ $$s_t$$ соответствует единичному смещению относительно символа $$s_{t + 1} $$, с которого начинается буфер;
  • если имеется несколько фраз с одинаковой длиной совпадения, то выбираем ближайшую к буферу;
  • в неопределенных ситуациях - когда длина совпадения нулевая - смещению присваиваем единичное значение.
  • Шаг Скользящее окно Совпадающая фраза Закодированные данные
    Словарь Буфер 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

    Из таблицы следует, что в целях минимизации закодированного представления для j = 6 следует использовать код наименьшей длины, так как эта длина совпадения встречается чаще всего.

    Хотя авторы алгоритма и доказали, что LZ77 может сжать данные не хуже, чем любой специально на них настроенный полуадаптивный словарный метод, из-за указанных недостатков это выполняется только для последовательностей достаточно большого размера.

    Что касается декодирования сжатых данных, то оно осуществляется путем простой замены кода на блок символов, состоящий из фразы словаря и явно передаваемого символа. Естественно, декодер должен выполнять те же действия по изменению окна, что и кодер. Фраза словаря элементарно определяется по смещению и длине, поэтому важным свойством 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);
      }

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

    Упражнение: Предложите несколько более эффективных способов кодирования результатов работы LZ77, чем использование простых кодов фиксированной длины.

    Алгоритм LZSS

    Алгоритм LZSS позволяет достаточно гибко сочетать в выходной последовательности символы и указатели (коды фраз), что до некоторой степени устраняет присущую LZ77 расточительность, проявляющуюся в регулярной передаче одного символа в прямом виде. Эта модификация LZ77 была предложена в 1982 году Сторером (Storer) и Жимански (Szymanski) [2.10].

    Идея алгоритма заключается в добавление к каждому указателю и символу однобитового префикса $$f$$, позволяющего различать эти объекты. Иначе говоря, однобитовый флаг $$f$$ указывает тип и, соответственно, длину непосредственно следующих за ним данных. Такая техника позволяет:

  • записывать символы в явном виде, когда соответствующий им код имеет большую длину, и, следовательно, словарное кодирование только вредит;
  • обрабатывать ни разу не встреченные до текущего момента символы.
  • Пример

    Закодируем строку "кот_ломом_колол_слона" из предыдущего примера и сравним коэффициент сжатия для 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

    Алгоритм 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 и его потомков, например LZW, существуют эффективные реализации процедур поиска и добавления фраз в словарь, что обеспечивает значительное преимущество над алгоритмами семейства LZ77 в скорости сжатия.

    Несмотря на относительную быстроту кодирования LZ78, при грамотной реализации алгоритма оно все же медленнее декодирования, соотношение скоростей равно обычно 3:2.

    Интересное свойство LZ78 заключается в том, что если исходные данные порождены источником с определенными характеристиками (он должен быть стационарнымМногомерные распределения вероятностей генерации последовательностей (слов) из n символов не меняются во времени, причем n - любое конечное число и эргодическимСреднее по времени равно среднему по числу реализаций; иначе говоря, для оценки свойств источника достаточно только одной длинной сгенерированной последовательности ), то коэффициент сжатия приближается по мере кодирования к минимальному достижимому [2.13]. Иначе говоря, количество битов, затрачиваемых на кодирование каждого символа, в среднем равно так называемой энтропии источника. Но, к сожалению, сходимость медленная, и на данных реальной длины алгоритм ведет себя не лучшим образом. Так, например, коэффициент сжатия текстов в зависимости от их размера обычно колеблется от 3.5 до 5 битов/символ. Кроме того, нередки ситуации, когда обрабатываемые данные порождены источником с ярко выраженной нестационарностью. Поэтому при оценке реального поведения алгоритма следует относиться с большой осторожностью к теоретическим выкладкам, обращая внимание на выполнение соответствующих условий.

    Доказано, что аналогичным свойством сходимости обладает и классический алгоритм LZ77, но скорость приближения к энтропии источника меньше, чем у алгоритма LZ78 [2.12].

    Список архиваторов и компрессоров

  • Info-ZIP group. Info-ZIP's portable Zip - C sources. http://www.infozip.org
  • Jung R. ARJ archiver. http://www.arjsoftware.com
  • Jung R. JAR archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/jar102x.exe
  • Lemke M. ACE archiver. http://www.winace.com
  • Microlog Cabinet Manager 2001 for Win9x/NT - Compression tool for .cab files. ftp://ftp.elf.stuba.sk/pub/pc/pack/cab2001.zip
  • Microsoft Corporation. Cabinet Software Development Tool. http://msdn.microsoft.com/library/en-us/dnsamples/cab-sdk.exe
  • Nico Mak Computing. WinZip archiver. http://www.winzip.com
  • Pavlov I. 7-Zip archiver. http://www.7-zip.org
  • PKWARE Inc. PKZIP archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/pk250dos.exe
  • Roshal E. RAR for Windows. http://www.rarsoft.com
  • Technelysium Pty Ltd. IMP archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/imp112.exe
  • Ziganshin B. ARJZ archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/arjz015.zip
  • Страницы:

    Идея словарных методов

    Входную последовательность символов можно рассматривать как последовательность строк, содержащих произвольное количество символов. Идея словарных методов состоит в замене строк символов на такие коды, что их можно трактовать как индексы строк некоторого словаря. Образующие словарь строки будем далее называть фразами. При декодировании осуществляется обратная замена индекса на соответствующую ему фразу словаря.

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

    Словарь - это набор таких фраз, которые, как мы полагаем, будут встречаться в обрабатываемой последовательности. Индексы фраз должны быть построены таким образом, чтобы в среднем их представление занимало меньше места, чем требуют замещаемые строки. За счет этого и происходит сжатие.

    Уменьшение размера возможно в первую очередь за счет того, что обычно в сжимаемых данных встречается лишь малая толика всех возможных строк длины 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

    Иначе говоря, размер (мощность) алфавита равен 136 символам, но реально используется только $${{{\rm{2536}}} \over {136 \cdot 136}} \cdot 100\% \approx 13.7\%$$ от всех возможных двухсимвольных строк, и т.д.

    Далее, если у нас есть заслуживающие доверия гипотезы о частоте использования тех или иных фраз, либо проводился какой-то частотный анализ обрабатываемых данных, то мы можем назначить более вероятным фразам коды меньшей длины. Например, для той же электронной версии романа "Бесы" статистика встречаемости строк длины 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.6, 2.9].

    Ниже будут рассмотрены алгоритмы словарного сжатия, относимые к классу методов Зива-Лемпела. В качестве примера словарного алгоритма иного класса укажем [2.7].

    Методы Зива-Лемпела ориентированы на сжатие качественных данных, причем эффективность применения достигается в том случае, когда статистические характеристики обрабатываемых данных соответствуют модели источника с памятью.

    Классические алгоритмы Зива-Лемпела

    Алгоритмы словарного сжатия Зива-Лемпела появились во второй половине 1970-х годов. Это были так называемые алгоритмы LZ77 и LZ78, разработанные совместно Зивом (Ziv) и Лемпелом (Lempel). В дальнейшем первоначальные схемы подвергались множественным изменениям, в результате чего мы сегодня имеем десятки достаточно самостоятельных алгоритмов и бессчетное количество модификаций.

    LZ77 и LZ78 являются универсальными алгоритмами сжатия, в которых словарь формируется на основании уже обработанной части входного потока, т.е. адаптивно. Принципиальным отличием является лишь способ формирования фраз. В модификациях первоначальных алгоритмов это свойство сохраняется. Поэтому словарные алгоритмы Зива-Лемпела разделяют на два семейства - алгоритмы типа LZ77 и алгоритмы типа LZ78. Иногда также говорят о словарных методах LZ1 и LZ2.

    Публикации Зива и Лемпела носили чисто теоретический характер, так как эти исследователи на самом деле занимались проблемой измерения "сложности" строки, и применение выработанных алгоритмов к сжатию данных явилось, скорее, лишь частным результатом. Потребовалось некоторое время, чтобы идея организации словаря, часто в переложении уже других людей, достигла разработчиков программного и аппаратного обеспечения. Поэтому практическое использование алгоритмов началось спустя пару лет.

    С тех пор методы данного семейства неизменно являются самыми популярными среди всех методов сжатия данных, хотя в последнее время ситуация начала меняться в пользу BWT и PPM, как обеспечивающих лучшее сжатие. Кроме того, практически все реально используемые словарные алгоритмы относятся к семейству Зива-Лемпела.

    Необходимо сказать несколько слов о наименованиях алгоритмов и методов. При обозначении семейства общепринятой является аббревиатура "LZ", но расшифровываться она должна как "Ziv-Lempel", поэтому и алгоритмы "Зива-Лемпела", а не "Лемпела-Зива". Согласно общепринятому объяснению этого курьеза, Якоб Зив внес больший вклад в открытие соответствующих словарных схем и исследование их свойств и таким образом заслужил, чтобы первым стояла его фамилия, что мы и видим в заголовках статей [2.12, 2.13]. Но случайно была допущена ошибка, и прикрепилось сокращение "LZ" (буквы упорядочены в алфавитном порядке). Иногда, кстати, встречается и обозначение "ZL" (порядок букв соответствует порядку фамилий авторов в публикациях [2.12, 2.13]). В дальнейшем, если некий исследователь существенно изменял какой-то алгоритм, относимый к семейству LZ, то в названии полученной модификации к строчке "LZ" обычно дописывалась первая буква его фамилии, например: алгоритм LZB, автор Белл (Bell).

    Подчеркнем также наличие большой путаницы с классификацией алгоритмов. Обычно она проявляется в нежелании признавать существование двух самостоятельных семейств LZ, а также в неправильном отнесении алгоритмов к конкретному семейству. Беспорядку часто способствуют сами разработчики: многим невыгодно раскрывать, на основе какого алгоритма создана данная модификация из-за коммерческих, патентных или иных меркантильных соображений. Например, в случае коммерческого программного обеспечения общепринятой является практика классификации используемого алгоритма сжатия как "модификации LZ77". И в этом нет ничего удивительного, ведь алгоритм LZ77 не запатентован.

    Алгоритм LZ77

    Этот словарный алгоритм сжатия является самым старым среди методов LZ. Описание было опубликовано в 1977 году [2.12], но сам алгоритм разработан не позднее 1975 года.

    Алгоритм LZ77 является "родоначальником" целого семейства словарных схем - так называемых алгоритмов со скользящим словарем, или скользящим окном. Действительно, в 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 + 2}}} {\rm{, }}...,{\rm{ s}}_{{\rm{t + n}}} {\rm{. }}$$. Очевидно, что если $${\rm{W}} \ge {\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, непосредственно следующий за совпавшей строкой буфера.

    Таким образом, на каждом шаге кодер выдает описание трех объектов: смещения и длины соответствия, образующих код фразы, равной обработанной строке буфера, и одного символа s (литерала). Затем окно смещается на j+1 символов вправо и осуществляется переход к новому циклу кодирования. Величина сдвига объясняется тем, что мы реально закодировали именно j+1 символов: j с помощью указателя на фразу в словаре, и 1 с помощью тривиального копирования. Передача одного символа в явном виде позволяет разрешить проблему обработки еще ни разу не виденных символов, но существенно увеличивает размер сжатого блока.

    Пример

    Попробуем сжать строку "кот_ломом_колол_слона" длиной 21 символ. Пусть длина буфера равна 7 символам, а размер словаря больше длины сжимаемой строки. Условимся также, что:

  • нулевое смещение зарезервировали для обозначения конца кодирования;
  • символ $$s_t$$ соответствует единичному смещению относительно символа $$s_{t + 1} $$, с которого начинается буфер;
  • если имеется несколько фраз с одинаковой длиной совпадения, то выбираем ближайшую к буферу;
  • в неопределенных ситуациях - когда длина совпадения нулевая - смещению присваиваем единичное значение.
  • Шаг Скользящее окно Совпадающая фраза Закодированные данные
    Словарь Буфер 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

    Из таблицы следует, что в целях минимизации закодированного представления для j = 6 следует использовать код наименьшей длины, так как эта длина совпадения встречается чаще всего.

    Хотя авторы алгоритма и доказали, что LZ77 может сжать данные не хуже, чем любой специально на них настроенный полуадаптивный словарный метод, из-за указанных недостатков это выполняется только для последовательностей достаточно большого размера.

    Что касается декодирования сжатых данных, то оно осуществляется путем простой замены кода на блок символов, состоящий из фразы словаря и явно передаваемого символа. Естественно, декодер должен выполнять те же действия по изменению окна, что и кодер. Фраза словаря элементарно определяется по смещению и длине, поэтому важным свойством 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);
      }

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

    Упражнение: Предложите несколько более эффективных способов кодирования результатов работы LZ77, чем использование простых кодов фиксированной длины.

    Алгоритм LZSS

    Алгоритм LZSS позволяет достаточно гибко сочетать в выходной последовательности символы и указатели (коды фраз), что до некоторой степени устраняет присущую LZ77 расточительность, проявляющуюся в регулярной передаче одного символа в прямом виде. Эта модификация LZ77 была предложена в 1982 году Сторером (Storer) и Жимански (Szymanski) [2.10].

    Идея алгоритма заключается в добавление к каждому указателю и символу однобитового префикса $$f$$, позволяющего различать эти объекты. Иначе говоря, однобитовый флаг $$f$$ указывает тип и, соответственно, длину непосредственно следующих за ним данных. Такая техника позволяет:

  • записывать символы в явном виде, когда соответствующий им код имеет большую длину, и, следовательно, словарное кодирование только вредит;
  • обрабатывать ни разу не встреченные до текущего момента символы.
  • Пример

    Закодируем строку "кот_ломом_колол_слона" из предыдущего примера и сравним коэффициент сжатия для 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

    Алгоритм 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 и его потомков, например LZW, существуют эффективные реализации процедур поиска и добавления фраз в словарь, что обеспечивает значительное преимущество над алгоритмами семейства LZ77 в скорости сжатия.

    Несмотря на относительную быстроту кодирования LZ78, при грамотной реализации алгоритма оно все же медленнее декодирования, соотношение скоростей равно обычно 3:2.

    Интересное свойство LZ78 заключается в том, что если исходные данные порождены источником с определенными характеристиками (он должен быть стационарнымМногомерные распределения вероятностей генерации последовательностей (слов) из n символов не меняются во времени, причем n - любое конечное число и эргодическимСреднее по времени равно среднему по числу реализаций; иначе говоря, для оценки свойств источника достаточно только одной длинной сгенерированной последовательности ), то коэффициент сжатия приближается по мере кодирования к минимальному достижимому [2.13]. Иначе говоря, количество битов, затрачиваемых на кодирование каждого символа, в среднем равно так называемой энтропии источника. Но, к сожалению, сходимость медленная, и на данных реальной длины алгоритм ведет себя не лучшим образом. Так, например, коэффициент сжатия текстов в зависимости от их размера обычно колеблется от 3.5 до 5 битов/символ. Кроме того, нередки ситуации, когда обрабатываемые данные порождены источником с ярко выраженной нестационарностью. Поэтому при оценке реального поведения алгоритма следует относиться с большой осторожностью к теоретическим выкладкам, обращая внимание на выполнение соответствующих условий.

    Доказано, что аналогичным свойством сходимости обладает и классический алгоритм LZ77, но скорость приближения к энтропии источника меньше, чем у алгоритма LZ78 [2.12].

    Список архиваторов и компрессоров

  • Info-ZIP group. Info-ZIP's portable Zip - C sources. http://www.infozip.org
  • Jung R. ARJ archiver. http://www.arjsoftware.com
  • Jung R. JAR archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/jar102x.exe
  • Lemke M. ACE archiver. http://www.winace.com
  • Microlog Cabinet Manager 2001 for Win9x/NT - Compression tool for .cab files. ftp://ftp.elf.stuba.sk/pub/pc/pack/cab2001.zip
  • Microsoft Corporation. Cabinet Software Development Tool. http://msdn.microsoft.com/library/en-us/dnsamples/cab-sdk.exe
  • Nico Mak Computing. WinZip archiver. http://www.winzip.com
  • Pavlov I. 7-Zip archiver. http://www.7-zip.org
  • PKWARE Inc. PKZIP archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/pk250dos.exe
  • Roshal E. RAR for Windows. http://www.rarsoft.com
  • Technelysium Pty Ltd. IMP archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/imp112.exe
  • Ziganshin B. ARJZ archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/arjz015.zip
  • Вернуться к учебному плану