Московский государственный университет имени М.В.Ломоносова
Опубликован: 10.10.2007 | Доступ: свободный | Студентов: 1288 / 86 | Оценка: 4.36 / 4.18 | Длительность: 14:22:00
Специальности: Программист
Лекция 2:

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

< Лекция 1 || Лекция 2: 1234 || Лекция 3 >

Алгоритм LZSS

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

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

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

Пример

Закодируем строку "кот_ломом_колол_слона" из предыдущего примера и сравним коэффициент сжатия для LZ77 и LZSS.

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

Процесс кодирования представлен в табл. 2.5

Таблица 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); // знак конца файла
}
Пример 2.2.
Упражнение: Из-за наличия порога 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 заключается в том, что если исходные данные порождены источником с определенными характеристиками (он должен быть стационарным1Многомерные распределения вероятностей генерации последовательностей (слов) из n символов не меняются во времени, причем n - любое конечное число и эргодическим2Среднее по времени равно среднему по числу реализаций; иначе говоря, для оценки свойств источника достаточно только одной длинной сгенерированной последовательности ), то коэффициент сжатия приближается по мере кодирования к минимальному достижимому [2.13]. Иначе говоря, количество битов, затрачиваемых на кодирование каждого символа, в среднем равно так называемой энтропии источника. Но, к сожалению, сходимость медленная, и на данных реальной длины алгоритм ведет себя не лучшим образом. Так, например, коэффициент сжатия текстов в зависимости от их размера обычно колеблется от 3.5 до 5 битов/символ. Кроме того, нередки ситуации, когда обрабатываемые данные порождены источником с ярко выраженной нестационарностью. Поэтому при оценке реального поведения алгоритма следует относиться с большой осторожностью к теоретическим выкладкам, обращая внимание на выполнение соответствующих условий.

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

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

  1. Info-ZIP group. Info-ZIP's portable Zip - C sources. http://www.infozip.org
  2. Jung R. ARJ archiver. http://www.arjsoftware.com
  3. Jung R. JAR archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/jar102x.exe
  4. Lemke M. ACE archiver. http://www.winace.com
  5. Microlog Cabinet Manager 2001 for Win9x/NT - Compression tool for .cab files. ftp://ftp.elf.stuba.sk/pub/pc/pack/cab2001.zip
  6. Microsoft Corporation. Cabinet Software Development Tool. http://msdn.microsoft.com/library/en-us/dnsamples/cab-sdk.exe
  7. Nico Mak Computing. WinZip archiver. http://www.winzip.com
  8. Pavlov I. 7-Zip archiver. http://www.7-zip.org
  9. PKWARE Inc. PKZIP archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/pk250dos.exe
  10. Roshal E. RAR for Windows. http://www.rarsoft.com
  11. Technelysium Pty Ltd. IMP archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/imp112.exe
  12. Ziganshin B. ARJZ archiver. ftp://ftp.elf.stuba.sk/pub/pc/pack/arjz015.zip
< Лекция 1 || Лекция 2: 1234 || Лекция 3 >
Анатолий Логинов
Анатолий Логинов
Россия, Сочи, МИУ