Федеральное агентство по образованию Государственное образовательное учреждение высшего профессионального образования «Ивановский государственный энергетический университет имени В.И. Ленина» № 1273 Структуры данных и алгоритмы сжатия информации без потерь Методические указания к лабораторной работе по дисциплине «Структуры и алгоритмы обработки данных» 2-е издание, стереотипное Иваново, 2006 Составители: Е.Р. ПАНТЕЛЕЕВ П.А. ФОМИН Редактор: И.Д.РАТМАНОВА Методические указания содержат описание статистических, словарных и арифметических алгоритмов сжатия данных без потерь комплект заданий для выполнения лабораторной работы по теме. Рекомендуются для студентов 3 курса специальностей 220300 и 220400, изучающих дисциплину «Структуры и алгоритмы обработки данных». Утверждены цикловой методической комиссией ИВТФ. Рецензент Кафедра программного обеспечения компьютерных систем Ивановского государственного энергетического университета СТРУКТУРЫ И АЛГОРИТМЫ СЖАТИЯ ДАННЫХ БЕЗ ПОТЕРЬ Методические указания к лабораторным работам по курсу «Структуры и алгоритмы обработки данных» Составители: ПАНТЕЛЕЕВ Евгений Рафаилович ФОМИН Павел Александрович Редактор Н.Б. Михалева Лицензия ЛР № 020264 от 15 декабря 1996 г. Подписано в печать 10.04.2006. Формат 60х84 1/16 Бумага писчая. Печать плоская. Усл.печ.л. 1,62 Тираж 80 экз. Заказ № 27 ГОУ ВПО «Ивановский государственный энергетический университет им. В.И.Ленина» Отпечатано в РИО ИГЭУ 153003, Иваново, ул. Рабфаковская, 34 Введение. Значение алгоритмов сжатия информации1 в современной информатике трудно переоценить. Благодаря эффективным алгоритмам сжатия аудио-, видео- и графической информации, в частности, стала возможной технологическая революция в области Multimedia. Целью данной лабораторной работы является реализация и исследование эффективности алгоритмов сжатия информации без потерь, которые гарантируют полное соответствие восстановленной информации исходной. Эта группа алгоритмов сжатия исторически возникла первой, и представляющие ее алгоритмы успешно используются для сжатия текстовых и исполняемых файлов, а также баз данных, где потери информации недопустимы. Авторы выражают свою благодарность Булату Зиганшину, Дмитрию Шкарину, Вадиму Юкину, Юрию Решетову, Максиму Смиpнову и всем остальным, чьи статьи и публикации использовались при подготовке материала, а также студентам специальностей 220300-220400, принявшим деятельное участие в верификации рассматриваемых алгоритмов. Так как, по глубоко укоренившейся традиции, все приложения, реализующие алгоритмы сжатия, называют архиваторами, необходимо внести ясность в терминологию. Архиватор - программа, объединяющая несколько файлов в один (несколько). Упаковщик - программа, пытающаяся уменьшить размер входного файла. Для DOS исторически все архиваторы являлись и упаковщиками, однако существуют как только архиваторы (tar), так и только упаковщики (gzip, bzip2). 1 Алгоритм RLE Самый простой и очевидный метод сжатия - RLE (Run Length Encoding) - кодирование повторяющихся последовательностей символов. Суть его в замене последовательности одинаковых символов во входном потоке флагом (признаком) кодированных данных, символ и количеством его повторений в выходном потоке. Здесь возникает проблема - что использовать в качестве флага. Оптимальным было бы использовать символ, который не встречается в кодируемых данных, т.к. в противном случае придется особо обрабатывать ситуацию кодирования последовательности из одного этого символа. Например, поток ЭЭтооооооооо примеррррррр!!! ! закодируется в ЭЭт!о9 приме!р7!!3 !!1, если в качестве флага использовать символ «!». Возможны разные способы представления флага, например, в PCX это установка двух старших битов байта. Достоинства этого метода - простота и высокая скорость работы, недостаток - низкая степень сжатия в силу специфичности требований к данным. Он применяется в чистом виде в форматах BMP(compressed), PCX и упаковщике EXEPACK, но чаще используется в совокупности с другими методами. Статистические методы сжатия Идея статистических методов сжатия информации основана на замене кодовых слов постоянной длины уникальными кодовыми словами переменной длины (обычно префиксными кодами 2) таким образом, что наиболее часто встречающимся во входном потоке кодовым словам соответствуют кодовые слова минимальной длины в выходном потоке, а наиболее редко встречающимся – слова максимальной длины. Классический метод Хаффмена Метод Хаффмена является исторически первым методом этой группы. Оригинальная работа была опубликована в 1952 г. под названием "A Method for The Construction of Minimum Redundancy Codes". В основе метода лежит модель кодирования в виде бинарного дерева с минимальной длиной взвешенных путей. Бинарным деревом называется ориентированное дерево, полустепень исхода любой из вершин которого не превышает двух. Вершина бинарного дерева, полустепень захода которой равна нулю, называется корнем. Для остальных вершин дерева полустепень захода равна единице. Префиксными называются коды, в которых ни одно кодовое слово не может являться началом другого 2 Вершина m-дерева, полустепень исхода которой равна нулю, называется листом. Для остальных вершин полустепень исхода составляет 1 или 2. Пусть T – бинарное дерево, А = (0,1) – двоичный алфавит, и каждому ребру Т -дерева приписана одна из букв алфавита таким образом, что все ребра, исходящие из одной вершины, помечены различными буквами. Тогда любому листу T - дерева можно приписать уникальное кодовое слово, образованное из букв, которыми помечены ребра, встречающиеся при движении от корня к соответствующему листу. Особенность описанного способа кодирования в том, что полученные коды являются префиксными. Очевидно, что стоимость хранения информации, закодированной при помощи T – дерева, равна сумме длин путей из корня к каждому листу дерева, взвешенных частотой соответствующего кодового слова, или длиной взвешенных путей: Σwili, где wi – частота кодового слова длины li во входном потоке. Рассмотрим в качестве примера кодировку символов в стандарте ASCII. Здесь каждый символ представляет собой кодовое слово фиксированной (8 бит) длины, поэтому стоимость хранения определится выражением 8Σwi = 8W, где W – количество кодовых слов во входном потоке. Поэтому стоимость хранения 39 кодовых слов в кодировке ASCII равна 312, независимо от относительной частоты отдельных символов в этом потоке. Алгоритм Хаффмена позволяет уменьшить стоимость хранения потока кодовых слов путем такого подбора длин кодовых слов, который минимизирует длину взвешенных путей. Будем называть дерево с минимальной длиной взвешенных путей деревом Хаффмена. Классический алгоритм Хаффмена на входе получает таблицу частот символов во входном потоке. Ниже описан алгоритм построения дерева Хаффмена. 1. Символы входного алфавита образуют список свободных узлов. Каждый имеет вес, который может быть равен либо вероятности, либо количеству вхождений символа в ожидаемое сообщение. 2. Выбираются два свободных узла дерева с наименьшими весами. 3. Создается их родитель с весом, равным их суммарному весу. 4. Родитель добавляется в список свободных узлов, а двое его детей удаляются из этого списка. 5. Одной дуге, выходящей из родителя, ставится в соответствие бит 1, другой – бит 0. 6. Шаги, начиная со второго, повторяются до тех пор, пока в списке свободных узлов не останется только один свободный узел. Он и будет считаться корнем дерева. Допустим, у нас есть следующая таблица частот: 15 7 6 6 5 А Б В Г Д На первом шаге из листьев дерева выбираются два с наименьшими весами – Г и Д. Они присоединяются к новому узлу-родителю, вес которого устанавливается 5 + 6 = 11. Затем узлы Г и Д удаляются из списка свободных. Узел Г соответствует ветви 0 родителя, узел Д – ветви 1. На следующем шаге то же происходит с узлами Б и В, так как теперь эта пара имеет самый меньший вес в дереве. Создается новый узел с весом 13, а узлы Б и В удаляются из списка свободных. После всего этого дерево кодирования выглядит так, как показано на рис. 1. 13 0 15 А 11 0 1 1 7 6 6 5 Б В Г Д Рис. 1. Дерево кодирования Хаффмена после второго шага На следующем шаге «наилегчайшей» парой оказываются узлы Б/В и Г/Д. Для них еще раз создается родитель, теперь уже с весом 24. Узел Б/В соответствует ветви 0 родителя, Г/Д–ветви 1. На последнем шаге в списке свободных осталось только два узла – это А узел Б (Б/В)/(Г/Д). В очередной раз создается родитель с весом 39 и бывшие свободными узлы присоединяются к разным его ветвям. Поскольку свободным остался только один узел, то алгоритм построения дерева кодирования Хаффмена завершается. Н-дерево представлено на рис. 2. 0 0 1 24 13 1 39 0 15 А 1 0 1 11 15 7 6 6 5 Б В Г Д Рис. 2. Окончательное дерево кодирования Хаффмена Каждый символ, входящий в сообщение, определяется как конкатенация нулей и единиц, сопоставленных ребрам дерева Хаффмена, на пути от корня к соответствующему листу. Для данной таблицы символов коды Хаффмена будут выглядеть следующим образом. А 0 Б 100 В 101 Г 110 Д 111 Наиболее частый символ сообщения А закодирован наименьшим количеством битов, а наиболее редкий символ Д – наибольшим. Стоимость хранения кодированного потока, определенная как сумма длин взвешенных путей, определится выражением: 15*1+7*3+6*3+6*3+5*3 =87, что существенно меньше стоимости хранения входного потока (312). Поскольку ни один из полученных кодов не является префиксом другого, они могут быть однозначно декодированы при чтении их из потока. Алгоритм декодирования предполагает просмотр потока битов и синхронное перемещение от корня вниз по дереву Хаффмена в соответствии со считанным значением до тех пор, пока не будет достигнут лист, то есть декодировано очередное кодовое слово, после чего распознавание следующего слова вновь начинается с вершины дерева. Классический алгоритм Хаффмена имеет один существенный недостаток. Для восстановления содержимого сжатого сообщения декодер должен знать таблицу частот, которой пользовался кодер. Следовательно, длина сжатого сообщения увеличивается на длину таблицы частот, которая должна посылаться впереди данных, что может свести на нет все усилия по сжатию сообщения. Кроме того, необходимость наличия полной частотной статистики перед началом собственно кодирования требует двух проходов по сообщению: одного – для построения модели сообщения (таблицы частот и дерева Хаффмена), другого – для собственно кодирования. Адаптивный метод Хаффмена Адаптивное сжатие позволяет не передавать модель сообщения вместе с ним самим и ограничиться одним проходом по сообщению как при кодировании, так и при декодировании. В общем случае программа, реализующая адаптивное сжатие, может быть выражена в следующей форме: ИнициализироватьМодель(); Пока не конец сообщения Символ = ВзятьСледующийСимвол(); Закодировать(Символ); ОбновитьМодельСимволом(Символ); Конец Пока Декодер в адаптивной схеме работает аналогичным образом: ИнициализироватьМодель(); Пока не конец сжатой информации Символ = РаскодироватьСледующийСимвол(); ВыдатьСимвол(Символ); ОбновитьМодельСимволом(Символ); Конец Пока Схема адаптивного кодирования/декодирования работает благодаря тому, что и при кодировании, и при декодировании используются одни и те же процедуры «ИнициализироватьМодель» и «ОбновитьМодельСимволом». И кодер, и декодер начинают с «пустой» модели (не содержащей информации о сообщении) и с каждым просмотренным символом обновляют ее одинаковым образом. В создании алгоритма адаптивного кодирования Хаффмена наибольшие сложности возникают при разработке процедуры ОбновитьМодельСимволом(). Модель данных, лежащая в основе этой процедуры, носит название упорядоченного дерева. Дерево обладает свойством упорядоченности, если его узлы могут быть перечислены в порядке возрастания веса, и в этом перечислении каждый узел находится рядом со своим «братом». Пример упорядоченного дерева приведен на рис. 3. 9.W=17 7. W=7 8.W=10 Д 5. W=3 1. W=1 6. W=4 2. W=2 3. W=2 4. W=2 А Б В Г Рис. 3. Упорядоченное дерево. Указан порядковый номер узла в списке. Примем без доказательства утверждение о том, что двоичное дерево является деревом кодирования Хаффмена тогда и только тогда, когда оно удовлетворяет свойству упорядоченности. Сохранение свойства упорядоченности в процессе обновления дерева позволяет нам быть уверенными в том, что двоичное дерево, с которым мы работаем, - это дерево Хаффмена и до, и после обновления веса у листьев дерева. Обновление дерева при считывании очередного символа сообщения состоит из двух операций. Первая – увеличение веса узлов дерева – представлена на рис. 4. 9.W=18 7. W=8 8.W=10 Д 5. W=4 1. W=2 6. W=4 2. W=2 3. W=2 4. W=2 А Б В Г Рис. 4. Обновление дерева кодирования при увеличении веса листа А Вначале увеличиваем вес листа, соответствующего считанному символу, на единицу. Затем увеличиваем вес родителя, чтобы привести его в соответствие с новыми значениями веса у детей. Этот процесс продолжается до тех пор, пока мы не доберемся до корня дерева. Среднее число операций увеличения веса равно среднему количеству битов, необходимых для того, чтобы закодировать символ. Вторая операция – перестановка узлов дерева – требуется тогда, когда увеличение веса узла приводит к нарушению свойства упорядоченности, то есть тогда, когда увеличенный вес узла стал больше, чем вес следующего по порядку узла (рис. 5). Если и дальше продолжать обрабатывать увеличение веса, двигаясь к корню дерева, то наше дерево перестанет быть деревом Хаффмена. 9.W=19 8.W=10 7. W=9 Д 6. W=4 5. W=5 4. W=2 3. W=2 2. W=2 1. W=3 Г В Б Рис. 5. Нарушение свойства упорядоченности Чтобы сохранить упорядоченность, алгоритм работает следующим образом. Пусть новый увеличенный вес узла равен W + 1. Тогда начинаем двигаться по списку в сторону увеличения веса, пока не найдем последний узел с весом W. Переставим текущий и найденный узлы между собой в списке (рис. 6), восстанавливая таким образом порядок в дереве. (При этом родители каждого из узлов тоже изменятся). На этом операция перестановки заканчивается. А 9.W=19 7. W=9 8.W=10 Д 5. W=4 1. W=2 6. W=5 2. W=2 3. W=2 4. W=3 Г Б В А Рис. 6. Дерево кодирования после первой перестановки узлов ( Г и А ) После перестановки операция увеличения веса узлов продолжается дальше. Следующий узел, вес которого будет увеличен алгоритмом, - это новый родитель узла, увеличение веса которого вызвало перестановку. Вид дерева кодирования после двукратного вызова процедуры обновления символом A показан на рис. 7. 9.W=21 7.W=10 8.W=11 Д 5. W=5 6. W=6 А 3. W=2 4. W=4 В 1. W=2 2. W=2 Г Б Рис. 7. Дерево после завершения процедуры обновления В целом алгоритм обновления дерева может быть записан следующим образом: ОбновитьМодельСимволом(Символ) { Текущий узел – ЛистСоответствующий(Символ) Всегда УвеличитьВес(ТекущийУзел) Если ТекущийУзел – КореньДерева Выход: Если Вес(ТекущийУзел)> Вес(СледующийЗа(ТекущийУзел)) Перестановка(); ТекущийУзел – Родитель(ТекущийУзел); Конец Всегда } Первой проблемой, возникающей при реализации адаптивного метода Хаффмена, является инициализация кодировочного дерева. Если в классическом алгоритме Хаффмена в результате предварительного просмотра входного потока известна таблица частот, по которой может быть восстановлено дерево кодировки, то в адаптивной версии алгоритма статистика заранее неизвестна. Можно проинициализировать дерево Хаффмена так, чтобы оно имело все 256 символов алфавита (для 8-битовых кодов) с частотой, равной 1. В начале кодирования каждый код будет иметь длину 8 битов. По мере адаптации модели наиболее часто встречающиеся символы будут кодироваться все меньшим и меньшим количеством битов. Такой подход работоспособен, но он значительно снижает степень сжатия, особенно на коротких сообщениях. Лучше начинать моделирование с пустого дерева и добавлять в него символы только по мере их появления в сжимаемом сообщении. Но это приводит к очевидному противоречию: когда символ появляется в сообщении в первый раз, он не может быть закодирован, так как его еще нет в дереве кодирования. Чтобы разрешить это противоречие, введем специальный ESCAPE код, который для декодера будет означать, что следующий символ закодирован вне контекста модели сообщения. Например, его можно передать в поток сжатой информации как есть, не кодируя вообще. Метод «ЗакодироватьСимвол» в алгоритме адаптивного кодирования Хаффмена можно записать следующим образом. ЗакодироватьСимвол(Символ) { Если СимволУжеЕстьВТаблице(Символ) ВыдатьКодХаффменаДляСимвола(Символ) Иначе { ВыдатьКодХаффменаДляСимвола(ESCAPE) ВыдатьСимвол(Символ) } } Использование специального символа ESCAPE подразумевает определенную инициализацию дерева до начала кодирования и декодирования: в него помещаются 2 специальных символа: ESCAPE и EOF (конец файла), с весом, равным 1 (рис. 8). 3. W=2 1. W=1 2.W=1 EOF ESC Рис. 8. Дерево кодирования после иницализации Поскольку процесс обновления дерева не коснется их веса, то по ходу кодирования они будут перемещаться на самые удаленные ветви дерева и иметь самые длинные коды. Метод Шеннона-Фано Этот метод также стоит кодировочное дерево, однако алгоритм его построения отличается от описанного выше: 1. Символы исходного алфавита упорядочиваются по невозрастанию вероятностей. В результате получаем последовательность (ai1,:,aiN), в которой для всех j = 1, 2,:, N – 1 выполняется соотношение aij >= aij+1. 2. Переменной-счётчику t присваивается значение 1. 3. Рассматриваемую последовательность разбиваем на M групп (в частности, две), не меняя порядка следования символов, так, чтобы суммарные вероятности символов в каждой группе были примерно одинаковы и максимально близки к 1/M. Получаем совокупность подпоследовательностей G1 = (ai1, ai2, : ,ai k1), G2 = (ai k1+1, ai k1+2, : ,aik2), …Gm. 4. Формируем t-й символ кодовых слов. Всем символам из подпоследовательности GS приписываем символ bS,s=1,2, …, M. 5. Переменная-счётчик увеличивается на 1. 6. Просматриваем все группы - подпоследовательности. Если некоторая группа GS состоит из одного символа aj то для этого aj процесс построения кодового слова считается законченным. Для каждой из этих групп, содержащих по 2 и более символов, выполняем действия, соответствующие Шагу 3 и Шагу 4. В результате получаем очередные t-е символы кодовых слов. После просмотра всех групп, осуществляется переход к Шагу 5. Процесс работы алгоритма заканчивается, когда все группы будут содержать ровно по одному символу исходного алфавита. Следует заметить, что дерево Шеннона-Фано строится несколько быстрее и проще, чем в методе Хаффмана, но дает меньшую степень сжатия. Словарные методы сжатия Алгоритм Лемпела-Зива (LZ) В 1977 году Лемпел и Зив опубликовали алгоритм сжатия под названием LZ77. Он основан на динамическом формировании словаря последовательностей символов, встретившихся при кодировании входного потока. Если последовательность символов при кодировании ранее не встречалась, в выходной поток выводится 0 (флаг отсутствия в словаре) и символ, в противном случае - длина последовательности и расстояние до ее предыдущей встречи. Например, слово MAMA закодируется в 0,М,0,A,2,2 Алгоритм LZW манипулирует тремя объектами: потоком символов, потоком кодов и таблицей цепочек. При сжатии поток символов является входным, а поток кодов – выходным, при раскрытии - наоборот. Таблица цепочек порождается и при сжатии и при раскрытии, но никогда не передается с этапа сжатия на этап раскрытия (сравните с адаптивным кодированием Хаффмена). Сжатие по методу LZW начинается с инициализации цепочки символов. Для этого необходимо выбрать размер кода (количество бит), а также количество возможных значений символов. Пусть размер кода равен 12 битам, что означает возможность запоминания 0FFF, или 4096, элементов в нашей таблице цепочек. Также предположим, что существует 32 различных символа. Это соответствует, например, картинке с 32 возможными цветами для каждого пиксела или тексту, где встречаются только 32 символа. Чтобы инициализировать таблицу, мы установим соответствие кода #0 символу #0, кода #1 to символу #1, и т.д., до кода #31 и символа #31. На самом деле мы указали, что каждый код от 0 до 31 является корневым. Больше в таблице не будет других кодов, обладающих этим свойством. Термин Символ Поток символов Цепочка Глоссарий метода Значение Фундаментальный элемент данных. В обычных текстовых файлах это отдельный байт, в растровых изображениях это индекс, который указывает цвет данного пиксела. Будем ссылаться на произвольный символ как на "K". Файл исходных данных. Несколько последовательных символов. Длина цепочки может изменяться от 1 до очень большого числа символов. Будем ссылаться на произвольную цепочку как на "[...]K". Термин Префикс Корень Код Поток кодов Элемент Таблица цепочек Значение Почти то же самое, что цепочка, но подразумевается, что префикс непосредственно предшествует символу, и префикс может иметь нулевую длину. Будем ссылаться на произвольный префикс как на "[...]". Односимвольная цепочка. Для большинства целей это просто символ. Это [...]K, где [...] пуста. Число, определяемое известным количеством бит, которое кодирует цепочку. Выходной поток кодов, таких как "растровые данные". Код и его цепочка. Список элементов, обычно (но не обязательно) уникальных. Модификация алгоритмов LZ, предложенная Велчем (LZW) Сжатие данных начинается с определения текущего префикса. Этот префикс запоминается и используется для сравнения здесь и в дальнейшем. Обозначим его "[.c.]". Изначально текущий префикс ничего не содержит. Определим также "текущую цепочку", которая образуется текущим префиксом и следующим символом в потоке символов. Обозначим ее "[.c.]K", где K - некоторый символ. Назовем первый символ в потоке символов P. Сделаем [.c.]P текущей цепочкой. В данной точке это корень P. Теперь выполним поиск в таблице цепочек, чтобы определить, входит ли в нее [.c.]P. Конечно, сейчас это произойдет, поскольку в таблицу при инициализации были помещены все корни. В этом случае мы ничего не делаем. Теперь делаем текущим префиксом [.c.]P. Берем следующий символ из потока символов. Назовем его Q. Добавим текущий префикс, чтобы сформировать [.c.]Q, т.е. текущую цепочку. Выполняем поиск в таблице цепочек, чтобы определить входит ли в нее [.c.]Q. В данном случае этого, конечно, не будет. Поэтому добавим [.c.]Q (которая в данном случае есть PQ) в таблицу цепочек под кодом #32, и выведем код для [.c.] в поток кодов. Теперь начнем опять с текущего префикса, соответствующего корню P. Продолжаем добавление символов к [.c.], чтобы сформировать [.c.]K, до тех пор, пока мы не сможем найти [.c.]K в таблице цепочек. Затем выводим код для [.c.] и добавляем [.c.]K в таблицу цепочек. На псевдокоде алгоритм будет описан приблизительно так: Инициализация таблицы цепочек; [.c.] ← пусто; пока не исчерпан входной поток K ← следующий символ в потоке символов; Если [.c.]K входит в таблицу цепочек то [.c.] ← [.c.]K; иначе добавить [.c.]K в таблицу цепочек; вывести код для [.c.] в поток кодов; [.c.] ← K; Рассмотрим пример. Предположим, что мы имеем 4-символьный алфавит: A,B,C,D, а поток символов, который необходимо сжать, выглядит как ABACABA. Инициализируем таблицу цепочек: #0=A, #1=B, #2=C, #3=D. Первый символ A входит в таблицу цепочек, следовательно, [.c.] становится равным A. Берем цепочку AB, которая не входит в таблицу, поэтому выводим код #0 (для [.c.]), и добавляем AB в таблицу цепочек с кодом #4; [.c.] становится равным B. Берем [.c.]A = BA, которая не входит в таблицу цепочек, поэтому выводим код #1 и добавляем BA в таблицу цепочек с кодом #5; [.c.] становится равным A. Берем AC, которая не входит в таблицу цепочек. Выводим код #0 и добавляем AC в таблицу цепочек с кодом #6; теперь [.c.] равно C. Берем цепочку [.c.]A = CA, которая не входит в таблицу. Выводим #2 для C и добавляем CA к таблице под кодом #7; теперь [.c.]=A. Берем AB, которая ВХОДИТ в таблицу цепочек, следовательно, [.c.] становится равным AB. Ищем ABA, которой нет в таблице цепочек, поэтому выводим код для AB, который равен #4, и добавляем ABA в таблицу цепочек под кодом #8; теперь [.c.] равно A. Во входном потоке больше нет символов, поэтому выводим код #0 для A и заканчиваем. Следовательно, поток кодов равен #0#1#0#2#4#0. Необходимо сказать несколько слов о возможностях повышения эффективности алгоритма LZW: Так как поиск в таблице цепочек может быть сопряжен со значительными вычислительными затратами, для его реализации рекомендуется стратегия хеширования. Алгоритм «прямого» LZW - сжатия может привести к переполнению таблицы цепочек: получается код, который не может быть представлен ранее установленных числом битов. Существует несколько способов справиться с этой проблемой. Например, сброс таблицы цепочек после ее заполнения (сразу или когда это будет выгодно сделать). В любой точке во время сжатия выполняется условие: если [...]K входит в таблицу цепочек, то [...] тоже входит в нее. Это обстоятельство приводит к эффективному методу запоминания цепочек в таблице. Вместо того, чтобы запоминать в таблице всю цепочку, можно использовать тот факт, любая цепочка может быть представлена как префикс плюс символ: [...]K. Ясно, что если [...]K вносится в таблицу, то [...] уже находится в ней, поэтому можно запомнить код для [...] плюс замыкающий символ K. Раскрытие, возможно более сложно концептуально, однако программная реализация его проще. Оно также начинается с инициализации таблицы цепочек. Необходимо определить нечто, называемое "текущим кодом", на что мы будем ссылаться как "<code>", и "старым кодом", на который будем ссылаться как "<old>". Чтобы начать распаковку возьмем первый код. Теперь он становится <code>. Этот код будет инициализировать таблицу цепочек в качестве корневого. Выводим корень в поток символов. Делаем этот код старым кодом <old>. Далее повторяем до конца потока: Берем следующий код и присваиваем его <code>. Возможно, что этот код не входит в таблицу цепочек, но пока предположим, что он там есть. Выводим цепочку, соответствующую <code> в поток символов. Теперь найдем первый символ в цепочке, которую вы только что получили. Назовем его K. Добавим его к префиксу [...], сгенерированному посредством <old>, чтобы получить новую цепочку [...]K. Добавим эту цепочку в таблицу цепочек и установим старый код <old> равным текущему коду <code>. Теперь рассмотрим случай, когда <code> не входит в таблицу цепочек. Вернемся обратно к сжатию и постараемся понять, что происходит, если во входном потоке появляется цепочка типа P[...]P[...]PQ. Предположим, что P[...] уже находится в таблице, а P[...]P - нет. Кодировщик выполнит грамматический разбор P[...], и обнаружит, что P[...]P отсутствует в таблице. Это приведет к выводу кода для P[...] и добавлению P[...]P в таблицу цепочек. Затем он возьмет P[...]P для следующей цепочки и определит, что P[...]P есть в таблице и выдаст выходной код для P[...]P, если окажется, что P[...]PQ в таблице отсутствует. Декодер всегда находится "на один шаг сзади" кодера. Когда декодер увидит код для P[...]P, он не добавит этот код к своей таблице сразу, поскольку ему нужен начальный символ P[...]P для добавления к цепочке для по- следнего кода P[...], чтобы сформировать код для P[...]P. Однако, когда декодер найдет код, который ему еще неизвестен, он всегда будет на 1 больше последнего добавленного к таблице. Следовательно, он может догадаться, что цепочка для этого кода должна быть и фактически всегда будет правильной. Если декодер увидел код #124 и его таблица цепочек содержит последний код только с #123, он может считать, что код с #124 должен быть, добавить его к своей таблице цепочек и вывести саму цепочку. Если код #123 генерирует цепочку, на которую он сошлется здесь как на префикс [...], то код #124 в этом особом случае будет [...] плюс первый символ [...]. Поэтому он должен добавить первый символ [...] к ней самой. В качестве довольно часто встречающегося примера предположим, что сжимается растровое изображение, в котором первые три пиксела имеют одинаковый цвет, т.е. поток символов выглядит как QQQ.... Для определенности скажем, что мы имеем 32 цвета и Q соответствует цвету #12. Кодировщик сгенерирует последовательность кодов 12,32,.... Вспомним, что код #32 не входит в начальную таблицу, которая содержит коды от #0 до #31. Декодер увидит код #12 и транслирует его как цвет Q. Затем он увидит код #32, о значении которого он пока не знает. Однако ясно, что QQ должно быть элементом #32 в таблице и QQ должна быть следующей цепочкой вывода. Таким образом, псевдокод декодирования можно представить следующим образом: Инициализация строки цепочек; взять первый код: <code>; вывести цепочку для <code> в поток символов; <old> = <code>; пока есть коды во входном потоке <code> ← следующий код в потоке кодов; если существует <code> в таблице цепочек, то (вывод цепочки для <code> в поток символов; [...] ← трансляция для <old>; K ← первый символ трансляции для <code>; добавить [...]K в таблицу цепочек; иначе ( [...] ← трансляция для <old>; K ← первый символ [...]; вывод [...]K в поток символов добавление его к таблице цепочек; <old> ← <code> Здесь описан "жадный" способ, выбирающий перед выводом кода столько символов, сколько возможно. Фактически такой способ является стандартным для LZW и дает в результате наилучшую степень сжатия. Однако нет правила, которое запрещало бы остановиться и вывести код для текущего префикса вне зависимости от того, есть он в таблице или нет, и добавить эту цепочку плюс следующий символ в таблицу цепочек. Для этого могут быть различные причины, например, если цепочка слишком длинна и порождает трудности при хешировании. При заполнении таблицы цепочек можно просто сбрасывать статистику, можно не обновлять и пользоваться старой до тех пор, пока анализатор не решит, что выгоднее сбросить и начать построение сначала (LZC) (превращение динамического метода в статический) или вытеснять новыми цепочками наиболее редко использующиеся/давно не использовавшиеся (LZT) (LFU3/LRU4). Достоинства методов - в быстроте распаковки данных (что часто более важно, чем запаковка), а для LZW - и запаковки. Но эти алгоритмы часто уступают статистическим по степени сжатия. LZW - коммерческий алгоритм, поэтому за его использования необходимо платить, а при использовании GIF – форматов использовать только программы, имеющие лицензию. Модификации LZ77 часто используются в архиваторах (ARJ, ZIP,RAR, HA A1), LZW используется в формате GIF. Алгоритмы арифметического сжатия Недостатком метода Хаффмана в том, что он использует целое число бит на символ. Алгоритмы арифметического сжатия позволяют обойти это ограничение. При арифметическом кодировании текст отображается на интервал вещественных чисел от 0 до 1. По мере кодирования текста отобpажающий его интеpвал уменьшается, а количество битов для его представления возрастает. Очередные символы текста сокращают величину интервала исходя из значений их вероятностей, определяемых моделью. Более вероятные символы делают это в меньшей степени, чем менее вероятные, и, следовательно, добавляют меньше битов к результату. Перед началом работы тексту соответствует интервал [0; 1). Пpи обработке очередного символа его ширина сужается за счет выделения этому символу части интервала. Hапpимеp, применим к тексту "eaii!" алфавита { a,e,i,o,u,! } модель с постоянными вероятностями, заданными в Таблице 1. 3 4 Least Frequently Used – наименее часто используемый Last Recently Used – наиболее давно встречавшийся) Таблица 1. Пример постоянной модели для алфавита { a,e,i,o,u,! }. Символ A E I O U ! Веpоятность .2 .3 .1 .2 .1 .1 Интеpвал [0.0; 0.2) [0.2; 0.5) [0.5; 0.6) [0.6; 0.8) [0.8; 0.9) [0.9; 1.0) И кодеру, и декодеру известно, что в самом начале интервал есть [0; 1). После просмотра первого символа "e", кодер сужает интервал до [0.2; 0.5), который модель выделяет этому символу. Второй символ "a" сузит этот новый интервал до первой его пятой части, поскольку для "a" выделен фиксированный интервал [0.0; 0.2). В результате получим рабочий интервал [0.2; 0.26), т.к. предыдущий интервал имел ширину в 0.3 единицы и одна пятая от него есть 0.06. Следующему символу "i" соответствует фиксированный интервал [0.5; 0.6), что применительно к рабочему интервалу [0.2; 0.26) суживает его до интервала [0.23, 0.236). Продолжая в том же духе, имеем: Таблица 2. Изменение интервала при кодировании После просмотра e a i i ! Ширина интервала [0.2; 0.5) [0.2; 0.26) [0.23; 0.236) [0.233; 0.2336) [0.23354; 0.2336) Предположим, что все, что декодер знает о тексте, это конечный интервал [0.23354; 0.2336). Он сразу же понимает, что первый закодированный символ есть "e", т.к. итоговый интервал целиком лежит в интервале, выделенном моделью этому символу согласно Таблице 1. Теперь повторим действия декодера. Начальное состояние интервала: [0.0; 1.0); после просмотра "e" - [0.2; 0.5). Отсюда ясно, что второй символ - это "a", поскольку это при- ведет к интервалу [0.2; 0.26), который полностью вмещает итоговый интервал [0.23354; 0.2336). Продолжая работать таким же образом, декодер извлечет весь текст. Декодеру нет необходимости знать значения обеих границ итогового интервала, полученного от кодера. Даже единственного значения, лежащего внутри него, например 0.23355, уже достаточно. (Другие числа 0.23354,0.23357 или даже 0.23354321 – вполне годятся). Однако, чтобы завершить процесс, декодеру нужно вовремя распознать конец текста. Кроме того, одно и то же число 0.0 можно представить и как "a", и как "aa", "aaa" и т.д. Для устранения неясности мы должны обозначить завершение каждого текста специальным символом EOF, известным и кодеру, и декодеру. Для алфавита из Таблицы 1 для этой цели, и только для нее, будет использоваться символ "!". Когда декодер встречает этот символ, он прекращает свой процесс. Для фиксированной модели, задаваемой моделью Таблицы 1, энтропия 5-символьного текста "eaii!" будет: -log 0.3 -log 0.2 -log 0.1 -log 0.1 -log 0.1 = -log 0.00006 ~ 4.22. Здесь применен логарифм по основанию 10, т.к. вышерассмотренное кодирование выполнялось для десятичных чисел. Это объясняет, почему требуется 5 десятичных цифр для кодирования этого текста. По сути, ширина итогового интервала есть 0.2336 - 0.23354 = 0.00006, а энтропия - отрицательный десятичный логарифм этого числа. Конечно, обычно мы работаем с двоичной арифметикой, передаем двоичные числа и измеряем энтропию в битах. Пяти десятичных цифр слишком много для кодирования текста из 4-х гласных. Ясно, что разные модели дают разную энтропию. Лучшая модель, построенная на анализе отдельных символов текста "eaii!", есть следующее множество частот символов: { "e"(0.2), "a"(0.2), "i"(0.4), "!"(0.2) }. Она дает энтропию, равную 2.89 в десятичной системе счисления, т.е. кодирует исходный текст числом из 3-х цифр. Однако более сложные модели, как отмечалось ранее, дают в общем случае гораздо лучший результат. Ниже показан фрагмент псевдокода, объединяющего изложенные ранее процедуры кодирования и декодирования. Символы в нем нумеруются как 1,2,3... Частотный интервал для i-го символа задается от cum_freq[i] до cum_freq[i-1]. Пpи убывании i cum_freq[i] возрастает так, что cum_freq[0] = 1. (Причина такого "обратного" соглашения состоит в том, что cum_freq[0] будет потом содержать нормализующий множитель, который удобно хранить в начале массива). Текущий рабочий интервал задается в [low; high) и будет в самом начале равен [0; 1) и для кодера, и для декодера. Этот псевдокод очень упрощен, тогда как на практике существует несколько факторов, осложняющих и кодирование, и декодирование. АЛГОРИТМ АРИФМЕТИЧЕСКОГО КОДИРОВАHИЯ С каждым символом текста обращаться к процедуре encode_symbol() Проверить, что "завершающий" символ закодирован последним Вывести полученное значение интервала [low; high) encode_symbol(symbol,cum_freq) range = high - low high = low + range*cum_freq[symbol-1] low = low + range*cum_freq[symbol] АЛГОРИТМ АРИФМЕТИЧЕСКОГО ДЕКОДИРОВАHИЯ Value - это поступившее на вход число Обращение к процедуpе decode_symbol() пока она не возвратит "завершающий" символ decode_symbol(cum_freq) поиск такого символа, что cum_freq[symbol] <= (value - low)/(high - low) < cum_freq[symbol-1] Это обеспечивает размещение value внутри нового интервала [low; high), что отражено в оставшейся части программы range = high - low high = low + range*cum_freq[symbol-1] low = low + range*cum_freq[symbol] return symbol Возможна модификация алгоритма кодирования, позволяющая получать байты на выходе: HижняяГpаница=0.0 ВеpхняяГpаница=256.0 Пока ((ОчеpеднойСимвол=ДайОчеpеднойСимвол() != КОHЕЦ) { Интервал = ВеpхняяГpаница - HижняяГpаница + Интервал * ВеpхняяГpаницаИнтеpвалаДля (ОчеpеднойСимвол); HижняяГpаница = HижняяГpаница + Интервал * HижняяГpаницаИнтеpвалаДля (ОчеpеднойСимвол); } Выдать (HижняяГpаница) Для этого верхнюю границу при инициализации надо делать равной 256 и в процессе работы отслеживать, когда целая часть в нижней и верхней границе станет равна, после чего целую часть в формате байта отправить в выходной поток, а дробные части верхней и нижней границы увеличить в 256 раз. Эта модификация возможна как для кодирования при помощи вещественных чисел, так и для целочисленного. Может возникнуть такая ситуация, что верхняя и нижняя границы отведенного символу интервала равны с точностью до ошибки округления, а их целые части не совпадают, в результате чего происходит зацикливание. чтобы избежать этого, надо отслеживать приближение ширины интервала к своему минимально допустимому значению и уменьшать верхнюю границу до совпадения целых частей. Алгоритм декодирования: Всегда { Символ= HайтиСимволИнтеpвалКотоpогоПопадаетЧисло(Число) Выдать(Символ) Интервал=ВерхняяГpаницаДля(Символ)HижняяГpаницаИнтеpвалаДля(Символ); Число=Число-HижняяГpаницаИнтеpвалаДля(Символ); Число=Число/Интервал; } Для извлечения байтов из файла в декодере необходимо ввести дополнительную переменную (например v_value), отслеживающую, во сколько раз уменьшается интервал, то есть каждый раз: v_value*=interval. Как только она примет значение менее 1/256 (0.00000001 в бинарном представлении), то надо извлечь очередной байт из входного потока, присвоить его вещественной переменной (предположим, b_value) и скорректировать ее: b_value*=v_value. Коррекция нужна, чтобы уложиться нацело в 256, так как границы интервалов нечеткие. Корректировку надо проводить перед вычис- лением интервала. Тепеpь остается только помножить верхнюю и нижнюю границу на b_value; И так до конца входного файла. Оригинальный алгоритм ориентирован на работу с битами и запатентован фирмой IBM, вследствие чего он обладает тем же недостатком, что и LZW. Существует так называемый "range coder", "байт-ориентированный арифметик" и свободный от патентных требований, работающий с целыми, а не вещественными числами. В качестве достоинства арифметических методов можно отметить более высокую, чем у статистических методов, степень сжатия, а в качестве недостатка – несколько меньшую производительность. Эти методы используются во многих архиваторах, например, в HA. Задания для лабораторного практикума5 № вар. 1 Уровень сложности * 2 * 3 ** 4 ** 5 *** 6 *** 7 *** 5 Текст задания Написать кодер файлов BMP – формата, использующий алгоритм RLE. Исследовать его эффективность (степень сжатия) на множестве файлов этого формата. Написать декодер файлов BMP – формата, использующий алгоритм RLE. Проверить корректность обратного преобразования при помощи приложений, обеспечивающих возможность просмотра BMP – файлов. Написать кодер, использующий статический алгоритм Хаффмана. Исследовать его эффективность (степень сжатия) в зависимости от типа и размера сжимаемых файлов. Написать декодер, использующий статический алгоритм Хаффмана. Проверить корректность обратного преобразования путем запуска исполняемых файлов, сжатых при помощи статического алгоритма Хаффмана. Написать кодер, использующий динамический алгоритм Хаффмана. Исследовать его эффективность (степень и время сжатия) в зависимости от типа и размера сжимаемых файлов. Написать декодер, использующий динамический алгоритм Хаффмана. Проверить корректность обратного преобразования путем запуска исполняемых файлов, сжатых при помощи динамического алгоритма Хаффмана. Написать кодер, использующий алгоритм ЛемпеляЗива-Велча. Исследовать его эффективность (степень и время сжатия) в зависимости от типа и размера сжимаемых файлов. количество звездочек соответствует степени сложности варианта № вар. 8 Уровень сложности *** 9 *** 10 *** Текст задания Написать декодер, использующий алгоритм Лемпеля-Зива Велча. Проверить корректность обратного преобразования путем запуска исполняемых файлов, сжатых при помощи алгоритма Лемпеля-Зива Велча. Написать кодер, использующий алгоритм целочисленного арифметического сжатия. Исследовать его эффективность (степень и время сжатия) в зависимости от типа и размера сжимаемых файлов. Написать декодер, использующий алгоритм целочисленного арифметического сжатия. Проверить корректность обратного преобразования путем запуска исполняемых файлов, сжатых при помощи алгоритма целочисленного арифметического сжатия. Библиография Свами М., Тхуласираман К. Графы, сети и алгоритмы: Пер. с англ. – М.: Мир, 1984. – 455 с. 2. Мастрюков Д. Алгоритмы сжатия информации //Монитор № 7-8, 1993 г. 3. Архивы эхоконференции RU.COMPRESS сети FIDONet (news-группа fido7.ru.compress). 4. ftp://ftp.elf.stuba.sk/pub/pc/pack/ - архиваторы, упаковщики, утилиты (в т.ч. и с исходными текстами) 5. www.come.to/dario_phong 6. http://algo.4u.ru, раздел "Компрессия" 7. http://www.compressconsult.com/rangecoder/ 8. http://video.ee.ntu.edu.tw/~standard/standards.html - Информация о JPEG, MPEG 9. http://dogma.net/DataCompression/ 10. http://www.sr3.t.u-tokyo.ac.jp/~arimura/compression_links.html 11. http://www.internz.com/compression-pointers.html 12. http://cotty.mebius.net/win/hobby/compress/compress.html 1. Оглавление Введение. ............................................................................................................. 2 Алгоритм RLE ..................................................................................................... 4 Статистические методы сжатия ......................................................................... 4 Классический метод Хаффмена ......................................................................... 4 Адаптивный метод Хаффмена ........................................................................... 7 Метод Шеннона-Фано ...................................................................................... 13 Словарные методы сжатия ............................................................................... 14 Алгоритм Лемпела-Зива (LZ) .......................................................................... 14 Модификация алгоритмов LZ, предложенная Велчем (LZW) ................................................................................................................. 15 Алгоримы арифметического сжатия ............................................................... 19 Задания для лабораторного практикума ......................................................... 26 Библиография .................................................................................................... 28