Методы кодирования. Алгоритм RLE: описание, особенности, правила и примеры

  • Tutorial

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

Поэтому целью данной статьи является дать представление о простейших алгоритмах сжатия тем, кому знания и опыт пока ещё не позволяют сходу понимать более профессиональную литературу, или же чей профиль и вовсе далёк от подобной тематики. Т.е. я «на пальцах» расскажу об одних из простейших алгоритмах и приведу примеры их реализации без километровых листингов кода.

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

RLE - компактность единообразия

Алгоритм RLE является, наверное, самым простейшим из всех: суть его заключается в кодировании повторов. Другими словами, мы берём последовательности одинаковых элементов, и «схлопываем» их в пары «количество/значение». Например, строка вида «AAAAAAAABCCCC» может быть преобразована в запись вроде «8×A, B, 4×C». Это, в общем-то, всё, что надо знать об алгоритме.

Пример реализации

Допустим, у нас имеется набор неких целочисленных коэффициентов, которые могут принимать значения от 0 до 255. Логичным образом мы пришли к выводу, что разумно хранить этот набор в виде массива байт:
unsigned char data = { 0, 0, 0, 0, 0, 0, 4, 2, 0, 4, 4, 4, 4, 4, 4, 4, 80, 80, 80, 80, 0, 2, 2, 2, 2, 255, 255, 255, 255, 255, 0, 0 };

Для многих гораздо привычней будет видеть эти данные в виде hex-дампа:
0000: 00 00 00 00 00 00 04 02 00 04 04 04 04 04 04 04
0010: 50 50 50 50 00 02 02 02 02 FF FF FF FF FF 00 00

Подумав, мы решили, что хорошо бы для экономии места как-то сжимать такие наборы. Для этого мы проанализировали их и выявили закономерность: очень часто попадаются подпоследовательности, состоящие из одинаковых элементов. Конечно же, RLE для этого подойдёт как нельзя кстати!

Закодируем наши данные, используя свежеполученные знания: 6×0, 4, 2, 0, 7×4, 4×80, 0, 4×2, 5×255, 2×0.

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

Есть как минимум два выхода из этой ситуации:

  1. В качестве индикатора сжатой цепочки выделить одно значение байта, а в случае коллизии с реальными данными экранировать их. Например, если использовать в «служебных» целях значение 255, то при встрече этого значения во входных данных мы вынуждены будем писать «255, 255» и после индикатора использовать максимум 254.
  2. Структурировать закодированные данные, указывая количество не только для повторяемых, но и последующих далее одиночных элементов. Тогда мы будем заранее знать, где какие данные.
Первый способ в нашем случае не кажется эффективным, поэтому, пожалуй, прибегнем ко второму.

Итак, теперь у нас имеется два вида последовательностей: цепочки одиночных элементов (вроде «4, 2, 0») и цепочки одинаковых элементов (вроде «0, 0, 0, 0, 0, 0»). Выделим в «служебных» байтах один бит под тип последовательности: 0 - одиночные элементы, 1 - одинаковые. Возьмём для этого, скажем, старший бит байта.

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

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

Первое, что должно броситься в глаза - при таком раскладе у нас есть парочка неиспользуемых значений. Не может быть последовательностей с нулевой длиной, поэтому мы можем увеличить максимальную длину до 128 байт, отнимая от длины единицу при кодировании и прибавляя при декодировании. Таким образом, мы можем кодировать длины от 1 до 128 вместо длин от 0 до 127.

Второе, что можно заметить - не бывает последовательностей одинаковых элементов единичной длины. Поэтому, от значения длины таких последовательностей при кодировании мы будем отнимать ещё единичку, увеличив тем самым их максимальную длину до 129 (максимальная длина цепочки одиночных элементов по-прежнему равна 128). Т.е. цепочки одинаковых элементов у нас могут иметь длину от 2 до 129.

Закодируем наши данные снова, но теперь уже и в понятном для компьютера виде. Будем записывать служебные байты как , где T - тип последовательности, а L - длина. Будем сразу учитывать, что длины мы записываем в изменённом виде: при T=0 отнимаем от L единицу, при T=1 - двойку.

0, , 4, 2, 0, , 4, , 80, , 0, , 2, , 255, , 0

Попробуем декодировать наш результат:

  • . T=1, значит следующий байт будет повторяться L+2 (4+2) раз: 0, 0, 0, 0, 0, 0.
  • . T=0, значит просто читаем L+1 (2+1) байт: 4, 2, 0.
  • . T=1, повторяем следующий байт 5+2 раз: 4, 4, 4, 4, 4, 4, 4.
  • . T=1, повторяем следующий байт 2+2 раз: 80, 80, 80, 80.
  • . T=0, читаем 0+1 байт: 0.
  • . T=1, повторяем байт 2+2 раз: 2, 2, 2, 2.
  • . T=1, повторяем байт 3+2 раз: 255, 255, 255, 255, 255.
  • . T=1, повторяем байт 0+2 раз: 0, 0.

А теперь последний шаг: сохраним полученный результат как массив байт. Например, пара , упакованная в байт, будет выглядеть вот так:

В итоге получаем следующее:
0000: 84 00 02 04 02 00 85 04 82 80 00 00 82 02 83 FF
0010: 80 00

Таким вот нехитрым образом на данном примере входных данных мы из 32 байт получили 18. Неплохой результат, особенно если учесть, что на более длинных цепочках он может оказаться гораздо лучше.

Возможные улучшения

Эффективность алгоритма зависит не только от собственно алгоритма, но и от способа его реализации. Поэтому, для разных данных можно разрабатывать разные вариации кодирования и представления закодированных данных. Например, при кодировании изображений можно сделать цепочки переменной длины: выделить один бит под индикацию длинной цепочки, и если он выставлен в единицу, то хранить длину и в следующем байте тоже. Так мы жертвуем длиной коротких цепочек (65 элементов вместо 129), но зато даём возможность всего тремя байтами закодировать цепочки длиною до 16385 элементов (2 14 + 2)!

Дополнительная эффективность может быть достигнута путём использования эвристических методов кодирования. Например, закодируем нашим способом следующую цепочку: «ABBA». Мы получим « , A, , B, , A» - т.е. 4 байта мы превратили в 6, раздули исходные данные аж в полтора раза! И чем больше таких коротких чередующихся разнотипных последовательностей, тем больше избыточных данных. Если это учесть, то можно было бы закодировать результат как « , A, B, B, A» - мы бы потратили всего один лишний байт.

LZ77 - краткость в повторении

LZ77 - один из наиболее простых и известных алгоритмов в семействе LZ. Назван так в честь своих создателей: Абрахама Лемпеля (Abraham L empel) и Якоба Зива (Jacob Z iv). Цифры 77 в названии означают 1977 год, в котором была опубликована статья с описанием этого алгоритма.

Основная идея заключается в том, чтобы кодировать одинаковые последовательности элементов. Т.е., если во входных данных какая-то цепочка элементов встречается более одного раза, то все последующие её вхождения можно заменить «ссылками» на её первый экземпляр.

Как и остальные алгоритмы этого семейства семейства, LZ77 использует словарь, в котором хранятся встречаемые ранее последовательности. Для этого он применяет принцип т.н. «скользящего окна»: области, всегда находящейся перед текущей позицией кодирования, в рамках которой мы можем адресовать ссылки. Это окно и является динамическим словарём для данного алгоритма - каждому элементу в нём соответствует два атрибута: позиция в окне и длина. Хотя в физическом смысле это просто участок памяти, который мы уже закодировали.

Пример реализации

Попробуем теперь что-нибудь закодировать. Сгенерируем для этого какую-нибудь подходящую строку (заранее извиняюсь за её нелепость):

«The compression and the decompression leave an impression. Hahahahaha!»

Вот как она будет выглядеть в памяти (кодировка ANSI):
0000: 54 68 65 20 63 6F 6D 70 72 65 73 73 69 6F 6E 20 The compression
0010: 61 6E 64 20 74 68 65 20 64 65 63 6F 6D 70 72 65 and the decompre
0020: 73 73 69 6F 6E 20 6C 65 61 76 65 20 61 6E 20 69 ssion leave an i
0030: 6D 70 72 65 73 73 69 6F 6E 2E 20 48 61 68 61 68 mpression. Hahah
0040: 61 68 61 68 61 21 ahaha!

Мы ещё не определились с размером окна, но условимся, что он больше размера кодируемой строки. Попробуем найти все повторяющиеся цепочки символов. Цепочкой будем считать последовательность символов длиной более единицы. Если цепочка входит в состав более длинной повторяющейся цепочки, будем её игнорировать.

«The compression and t de leave[ an] i . Hah !»

Для пущей наглядности посмотрим на схему, где видны соответствия повторяемых последовательностей и их первых вхождений:

Пожалуй, единственным неясным моментом здесь будет последовательность «Hahahahaha!», ведь цепочке символов «ahahaha» соответствует короткая цепочка «ah». Но здесь нет ничего необычного, мы использовали кое-какой приём, позволяющий алгоритму иногда работать как описанный ранее RLE.

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

С этим разобрались. Теперь заменим найденные повторы на ссылки в словарь. Будем записывать ссылку в формате , где P - позиция первого вхождения цепочки в строке, L - её длина.

«The compression and t de leave i . Hah !»

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

«The compression and t de leave i . Hah !»

Теперь нам достаточно отнять P от текущей позиции кодирования, чтобы получить абсолютную позицию в строке.

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

А вот от размера окна уже зависит, насколько глубоко мы будем искать одинаковые цепочки. Поскольку мы имеем дело с небольшими текстами, то в самый раз будет дополнить используемое нами количество бит до двух байт: будем адресовать ссылки в диапазоне из 4096 байт, используя для этого 12 бит.

По опыту с RLE мы знаем, что не всякие значения могут быть использованы. Очевидно, что ссылка может иметь минимальное значение 1, поэтому, чтобы адресовать назад в диапазоне 1..4096, мы должны при кодировании отнимать от ссылки единицу, а при декодировании прибавлять обратно. То же самое с длинами последовательностей: вместо 0..15 будем использовать диапазон 2..17, поскольку с нулевыми длинами мы не работаем, а отдельные символы последовательностями не являются.

Итак, представим наш закодированный текст с учётом этих поправок:

«The compression and t de leave i . Hah !»

Теперь, опять же, нам надо как-то отделить сжатые цепочки от остальных данных. Самый распространённый способ - снова использовать структуру и прямо указывать, где сжатые данные, а где нет. Для этого мы разделим закодированные данные на группы по восемь элементов (символов или ссылок), а перед каждой из таких групп будем вставлять байт, где каждый бит соответствует типу элемента: 0 для символа и 1 для ссылки.

Разделяем на группы:

  • «The comp»
  • «ression »
  • «and t de»
  • « leave »
  • «i . Hah »
Компонуем группы:

«{0,0,0,0,0,0,0,0} The comp{0,0,0,0,0,0,0,0} ression {0,0,0,0,0,1,0,0} and t de{1,0,0,0,0,0,1,0} leave {0,1,0,0,0,0,0,1} i . Hah {0} !»

Таким образом, если при распаковке мы встречаем бит 0, то мы просто читаем символ в выходной поток, если же бит 1, мы читаем ссылку, а по ссылке читаем последовательность из словаря.

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

В итоге наш сжатый поток будет выглядеть так:

0000: 00 54 68 65 20 63 6f 6d 70 00 72 65 73 73 69 6f #The comp#ressio
0010: 6e 20 04 61 6e 64 20 74 01 31 64 65 82 01 5a 6c n #and t##de###l
0020: 65 61 76 65 01 b1 20 41 69 02 97 2e 20 48 61 68 eave## #i##. Hah
0030: 00 15 00 21 00 00 00 00 00 00 00 00 00 00 00 00 ###!

Возможные улучшения

В принципе, здесь будет верно всё, что описывалось для RLE. В частности, для демонстрации пользы эвристического подхода при кодировании, рассмотрим следующий пример:

«The long goooooong. The loooooower bound.»

Найдём последовательности только для слова «loooooower»:

«The long goooooong. The wer bound.»

Для кодирования такого результата нам понадобится четыре байта на ссылки. Однако, более экономично было бы сделать так:

«The long goooooong. The l wer bound.»

Тогда мы потратили бы на один байт меньше.

Вместо заключения

Несмотря на свою простоту и, казалось бы, не слишком уж большую эффективность, эти алгоритмы до сих пор широко применяются в разнообразных областях IT-сферы.

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

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

Данных, который поддерживается большинством форматов файлов растровых изображений: TIFF, BMP и PCX. RLE подходит для сжатия любого типа данных независимо от его информационного содержимого, но содержание данных влияет на коэффициент сжатия. Несмотря на то что большинство алгоритмов RLE не могут обеспечить высокие коэффициенты сжатия более сложных методов, данный инструмент легко реализовать и быстро выполнить, что делает его хорошей альтернативой.

На чем основан алгоритм сжатия RLE?

RLE работает, уменьшая физический размер повторяющейся строки символов. Эта строка, называемая run, обычно кодируется в два байта. Первый байт представляет количество символов в пробеге и называется счетчиком прогона. На практике кодированный прогон может включать от 1 до 128 и 256 символов. Счетчик обычно содержит число символов минус один (значение в диапазоне значений от 0 до 127 и 255). Второй байт — это значение символа в прогоне, которое содержится в диапазоне значений от 0 до 255 и именуется значением запуска.

Без сжатия символьный пробег в 15 символов обычно требует 15 байтов для хранения:

AAAAAAAAAAAAAAA.

В той же строке после RLE-кодирования потребуется только два байта: 15А.

Кодировка 15A, сгенерированная для обозначения символьной строки, называется RLE-пакетом. В данном коде первый байт, 15, является счетчиком прогона и содержит необходимое количество повторений. Второй байт, A, является значением run и содержит фактическое повторяющееся значение в пробеге.

Новый пакет генерируется каждый раз, когда символ запуска изменяется, или каждый раз, когда количество символов в пробеге превышает максимальное количество. Предположим, что 15-символьная строка согласно условиям содержит 4 разных символа:

Используя кодировку с длиной строки, она может быть сжата в четыре двухбайтовых пакета:

После кодирования по длине строки для 15-байтовой строки потребуется всего восемь байтов данных для представления строки, в отличие от исходных 15 байтов. В этом случае кодирование во время выполнения давало коэффициент сжатия почти 2 к 1.

Особенности

Длинные прогоны редки в некоторых типах данных. Например, открытый текст ASCII редко содержит длинные прогоны. В предыдущем примере последний пробег (содержащий символ t) был только одним символом в длину. 1-символьный прогон все еще работает. И счет запуска, и значение запуска должны быть записаны для каждого пробега в 2 символа. Для кодирования пробега с помощью алгоритма RLE требуется информация, состоящая не менее чем из двух символов. В связи с чем запуск одиночных символов на самом деле занимает больше места. По тем же причинам данные, состоящие полностью из 2-символьных прогонов, остаются неизменными после кодирования RLE.

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

Варианты кодирования по длине

Существует несколько вариантов кодировки во время выполнения. Данные изображения, как правило, закодированы в последовательном процессе, который обрабатывает визуальный контент как 1D-поток, а не как 2D-карту данных. При последовательной обработке растровое изображение кодируется, стартуя с верхнего левого угла, и направляется слева направо по каждой строке сканирования в нижний правый угол растрового изображения. Но альтернативные схемы RLE также могут быть записаны для кодирования данных по длине растрового изображения вдоль столбцов для сжатия в 2D-плитки или даже для кодирования пикселей по диагонали зигзагообразным способом. Нечетные варианты RLE могут использоваться в узкоспециализированных приложениях, но обычно встречаются довольно редко.

Алгоритм кодирования с ошибкой длины пробега

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

Кросс-кодирование

Кросс-кодирование — это объединение строк сканирования, которое происходит, когда процесс сжатия теряет различие между исходными линиями. Если данные отдельных линий объединены алгоритмом кодирования повторов RLE, точка, где одна линия сканирования остановлена, а другая - потеряна, является уязвимой и сложной для обнаружения.

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

Как с помощью алгоритма RLE закодировать изображение?

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

Альтернативный вариант

Другим вариантом для определения начальной точки любой конкретной строки сканирования в блоке кодированных данных является построение таблицы строк развертки. Данная табличная структура обычно содержит один элемент для каждой строки сканирования на изображении, и каждый элемент содержит значение смещения соответствующей строки сканирования. Чтобы найти первый RLE-пакет строки 10 сканирования, все, что нужно сделать декодеру, — это найти значения позиции смещения, хранящегося в десятом элементе таблицы поиска строки сканирования. Таблица строк развертки также может содержать количество байтов, используемых для кодирования каждой строки. Используя этот метод с целью найти первый RLE-пакет строки сканирования 10, ваш декодер будет объединять значения первых 9-ти элементов таблицы строк развертки. Первый пакет для строки 10 сканирования будет начинаться с этого смещения байта от начала данных изображения с кодированием RLE.

Единицы измерения

Части алгоритмов кодирования по длине, которые различаются, — это решения, которые принимаются на основе типа данных, которые декодируются (например, длина прогона данных). Схемы RLE, используемые для сжатия растровой графики, обычно подразделяются на классы по типу атомных (то есть наиболее фундаментальных) элементов, которые они кодируют. Три класса, используемые большинством графических форматов файлов, — это бит, байты и пиксельные RLE.

Качество сжатия

Битовые уровни RLE-схем кодируют прогоны нескольких бит в строке сканирования и игнорируют границы байтов и слов. Только монохромные (черно-белые), 1-битные изображения содержат достаточное количество бит, чтобы сделать этот класс RLE-кодирования эффективным. Типичная схема RLE на уровне бит кодирует от одного до 128 бит в однобайтовом пакете. Семь младших значащих бит содержат счетчик запуска минус один, а старший бит содержит значение битового прогона, равное 0 или 1. Прогон длиной более 128 пикселей разбит на несколько RLE-кодированных пакетов.

Исключения

Схемы RLE на уровне байта кодируют прогоны одинаковых байтовых значений, игнорируя некоторые биты и границы слов в строке сканирования. Наиболее распространенная схема RLE на байтовом уровне кодирует прогоны байтов в 2-байтовые пакеты. Первый байт содержит счетчик от 0 до 255, а второй — содержит значение байтового запуска. Также распространено дополнение двухбайтовой схемы кодирования с возможностью хранения литеральных, незаписанных прогонов байтов в потоке кодированных данных.

В такой схеме семь младших значащих бит первого байта содержат счетчик прогона минус один, а самый старший бит первого байта является индикатором типа запуска, который следует за байтом подсчета прогона. Если старший бит установлен в 1, он обозначает кодированный прогон. Закодированные прогоны декодируются путем считывания значения пробега и повторения его количества раз, указанного числом циклов. Если самый старший бит установлен в 0, отображается литеральный прогон, а это означает, что байты подсчета следующего прогона считываются буквально из данных кодированного изображения. Затем байт счетчика выполнения содержит значение в диапазоне от 0 до 127 (счетчик запуска минус один). Схемы RLE на уровне байта хороши для данных изображения, которые сохраняются как один байт на пиксель.

Схемы RLE на уровне пикселей используются, когда два или более последовательных байта данных изображения используются для хранения значений одного пикселя. На уровне пикселей биты игнорируются, а байты подсчитываются только для идентификации каждого значения пикселя. Размер кодированных пакетов зависит от размера кодируемых значений пикселей. Количество бит или байтов на пиксель сохраняется в заголовке файла изображения. Запуск данных изображения, сохраненных в виде 3-байтовых значений пикселей, кодируется в 4-байтовый пакет, с одним байтом подсчета количества операций, за которым следуют три байта с байтами. Метод кодирования остается таким же, как и с байтовым RLE.

  • Tutorial

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

Поэтому целью данной статьи является дать представление о простейших алгоритмах сжатия тем, кому знания и опыт пока ещё не позволяют сходу понимать более профессиональную литературу, или же чей профиль и вовсе далёк от подобной тематики. Т.е. я «на пальцах» расскажу об одних из простейших алгоритмах и приведу примеры их реализации без километровых листингов кода.

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

RLE - компактность единообразия

Алгоритм RLE является, наверное, самым простейшим из всех: суть его заключается в кодировании повторов. Другими словами, мы берём последовательности одинаковых элементов, и «схлопываем» их в пары «количество/значение». Например, строка вида «AAAAAAAABCCCC» может быть преобразована в запись вроде «8×A, B, 4×C». Это, в общем-то, всё, что надо знать об алгоритме.

Пример реализации

Допустим, у нас имеется набор неких целочисленных коэффициентов, которые могут принимать значения от 0 до 255. Логичным образом мы пришли к выводу, что разумно хранить этот набор в виде массива байт:
unsigned char data = { 0, 0, 0, 0, 0, 0, 4, 2, 0, 4, 4, 4, 4, 4, 4, 4, 80, 80, 80, 80, 0, 2, 2, 2, 2, 255, 255, 255, 255, 255, 0, 0 };

Для многих гораздо привычней будет видеть эти данные в виде hex-дампа:
0000: 00 00 00 00 00 00 04 02 00 04 04 04 04 04 04 04
0010: 50 50 50 50 00 02 02 02 02 FF FF FF FF FF 00 00

Подумав, мы решили, что хорошо бы для экономии места как-то сжимать такие наборы. Для этого мы проанализировали их и выявили закономерность: очень часто попадаются подпоследовательности, состоящие из одинаковых элементов. Конечно же, RLE для этого подойдёт как нельзя кстати!

Закодируем наши данные, используя свежеполученные знания: 6×0, 4, 2, 0, 7×4, 4×80, 0, 4×2, 5×255, 2×0.

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

Есть как минимум два выхода из этой ситуации:

  1. В качестве индикатора сжатой цепочки выделить одно значение байта, а в случае коллизии с реальными данными экранировать их. Например, если использовать в «служебных» целях значение 255, то при встрече этого значения во входных данных мы вынуждены будем писать «255, 255» и после индикатора использовать максимум 254.
  2. Структурировать закодированные данные, указывая количество не только для повторяемых, но и последующих далее одиночных элементов. Тогда мы будем заранее знать, где какие данные.
Первый способ в нашем случае не кажется эффективным, поэтому, пожалуй, прибегнем ко второму.

Итак, теперь у нас имеется два вида последовательностей: цепочки одиночных элементов (вроде «4, 2, 0») и цепочки одинаковых элементов (вроде «0, 0, 0, 0, 0, 0»). Выделим в «служебных» байтах один бит под тип последовательности: 0 - одиночные элементы, 1 - одинаковые. Возьмём для этого, скажем, старший бит байта.

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

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

Первое, что должно броситься в глаза - при таком раскладе у нас есть парочка неиспользуемых значений. Не может быть последовательностей с нулевой длиной, поэтому мы можем увеличить максимальную длину до 128 байт, отнимая от длины единицу при кодировании и прибавляя при декодировании. Таким образом, мы можем кодировать длины от 1 до 128 вместо длин от 0 до 127.

Второе, что можно заметить - не бывает последовательностей одинаковых элементов единичной длины. Поэтому, от значения длины таких последовательностей при кодировании мы будем отнимать ещё единичку, увеличив тем самым их максимальную длину до 129 (максимальная длина цепочки одиночных элементов по-прежнему равна 128). Т.е. цепочки одинаковых элементов у нас могут иметь длину от 2 до 129.

Закодируем наши данные снова, но теперь уже и в понятном для компьютера виде. Будем записывать служебные байты как , где T - тип последовательности, а L - длина. Будем сразу учитывать, что длины мы записываем в изменённом виде: при T=0 отнимаем от L единицу, при T=1 - двойку.

0, , 4, 2, 0, , 4, , 80, , 0, , 2, , 255, , 0

Попробуем декодировать наш результат:

  • . T=1, значит следующий байт будет повторяться L+2 (4+2) раз: 0, 0, 0, 0, 0, 0.
  • . T=0, значит просто читаем L+1 (2+1) байт: 4, 2, 0.
  • . T=1, повторяем следующий байт 5+2 раз: 4, 4, 4, 4, 4, 4, 4.
  • . T=1, повторяем следующий байт 2+2 раз: 80, 80, 80, 80.
  • . T=0, читаем 0+1 байт: 0.
  • . T=1, повторяем байт 2+2 раз: 2, 2, 2, 2.
  • . T=1, повторяем байт 3+2 раз: 255, 255, 255, 255, 255.
  • . T=1, повторяем байт 0+2 раз: 0, 0.

А теперь последний шаг: сохраним полученный результат как массив байт. Например, пара , упакованная в байт, будет выглядеть вот так:

В итоге получаем следующее:
0000: 84 00 02 04 02 00 85 04 82 80 00 00 82 02 83 FF
0010: 80 00

Таким вот нехитрым образом на данном примере входных данных мы из 32 байт получили 18. Неплохой результат, особенно если учесть, что на более длинных цепочках он может оказаться гораздо лучше.

Возможные улучшения

Эффективность алгоритма зависит не только от собственно алгоритма, но и от способа его реализации. Поэтому, для разных данных можно разрабатывать разные вариации кодирования и представления закодированных данных. Например, при кодировании изображений можно сделать цепочки переменной длины: выделить один бит под индикацию длинной цепочки, и если он выставлен в единицу, то хранить длину и в следующем байте тоже. Так мы жертвуем длиной коротких цепочек (65 элементов вместо 129), но зато даём возможность всего тремя байтами закодировать цепочки длиною до 16385 элементов (2 14 + 2)!

Дополнительная эффективность может быть достигнута путём использования эвристических методов кодирования. Например, закодируем нашим способом следующую цепочку: «ABBA». Мы получим « , A, , B, , A» - т.е. 4 байта мы превратили в 6, раздули исходные данные аж в полтора раза! И чем больше таких коротких чередующихся разнотипных последовательностей, тем больше избыточных данных. Если это учесть, то можно было бы закодировать результат как « , A, B, B, A» - мы бы потратили всего один лишний байт.

LZ77 - краткость в повторении

LZ77 - один из наиболее простых и известных алгоритмов в семействе LZ. Назван так в честь своих создателей: Абрахама Лемпеля (Abraham L empel) и Якоба Зива (Jacob Z iv). Цифры 77 в названии означают 1977 год, в котором была опубликована статья с описанием этого алгоритма.

Основная идея заключается в том, чтобы кодировать одинаковые последовательности элементов. Т.е., если во входных данных какая-то цепочка элементов встречается более одного раза, то все последующие её вхождения можно заменить «ссылками» на её первый экземпляр.

Как и остальные алгоритмы этого семейства семейства, LZ77 использует словарь, в котором хранятся встречаемые ранее последовательности. Для этого он применяет принцип т.н. «скользящего окна»: области, всегда находящейся перед текущей позицией кодирования, в рамках которой мы можем адресовать ссылки. Это окно и является динамическим словарём для данного алгоритма - каждому элементу в нём соответствует два атрибута: позиция в окне и длина. Хотя в физическом смысле это просто участок памяти, который мы уже закодировали.

Пример реализации

Попробуем теперь что-нибудь закодировать. Сгенерируем для этого какую-нибудь подходящую строку (заранее извиняюсь за её нелепость):

«The compression and the decompression leave an impression. Hahahahaha!»

Вот как она будет выглядеть в памяти (кодировка ANSI):
0000: 54 68 65 20 63 6F 6D 70 72 65 73 73 69 6F 6E 20 The compression
0010: 61 6E 64 20 74 68 65 20 64 65 63 6F 6D 70 72 65 and the decompre
0020: 73 73 69 6F 6E 20 6C 65 61 76 65 20 61 6E 20 69 ssion leave an i
0030: 6D 70 72 65 73 73 69 6F 6E 2E 20 48 61 68 61 68 mpression. Hahah
0040: 61 68 61 68 61 21 ahaha!

Мы ещё не определились с размером окна, но условимся, что он больше размера кодируемой строки. Попробуем найти все повторяющиеся цепочки символов. Цепочкой будем считать последовательность символов длиной более единицы. Если цепочка входит в состав более длинной повторяющейся цепочки, будем её игнорировать.

«The compression and t de leave[ an] i . Hah !»

Для пущей наглядности посмотрим на схему, где видны соответствия повторяемых последовательностей и их первых вхождений:

Пожалуй, единственным неясным моментом здесь будет последовательность «Hahahahaha!», ведь цепочке символов «ahahaha» соответствует короткая цепочка «ah». Но здесь нет ничего необычного, мы использовали кое-какой приём, позволяющий алгоритму иногда работать как описанный ранее RLE.

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

С этим разобрались. Теперь заменим найденные повторы на ссылки в словарь. Будем записывать ссылку в формате , где P - позиция первого вхождения цепочки в строке, L - её длина.

«The compression and t de leave i . Hah !»

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

«The compression and t de leave i . Hah !»

Теперь нам достаточно отнять P от текущей позиции кодирования, чтобы получить абсолютную позицию в строке.

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

А вот от размера окна уже зависит, насколько глубоко мы будем искать одинаковые цепочки. Поскольку мы имеем дело с небольшими текстами, то в самый раз будет дополнить используемое нами количество бит до двух байт: будем адресовать ссылки в диапазоне из 4096 байт, используя для этого 12 бит.

По опыту с RLE мы знаем, что не всякие значения могут быть использованы. Очевидно, что ссылка может иметь минимальное значение 1, поэтому, чтобы адресовать назад в диапазоне 1..4096, мы должны при кодировании отнимать от ссылки единицу, а при декодировании прибавлять обратно. То же самое с длинами последовательностей: вместо 0..15 будем использовать диапазон 2..17, поскольку с нулевыми длинами мы не работаем, а отдельные символы последовательностями не являются.

Итак, представим наш закодированный текст с учётом этих поправок:

«The compression and t de leave i . Hah !»

Теперь, опять же, нам надо как-то отделить сжатые цепочки от остальных данных. Самый распространённый способ - снова использовать структуру и прямо указывать, где сжатые данные, а где нет. Для этого мы разделим закодированные данные на группы по восемь элементов (символов или ссылок), а перед каждой из таких групп будем вставлять байт, где каждый бит соответствует типу элемента: 0 для символа и 1 для ссылки.

Разделяем на группы:

  • «The comp»
  • «ression »
  • «and t de»
  • « leave »
  • «i . Hah »
Компонуем группы:

«{0,0,0,0,0,0,0,0} The comp{0,0,0,0,0,0,0,0} ression {0,0,0,0,0,1,0,0} and t de{1,0,0,0,0,0,1,0} leave {0,1,0,0,0,0,0,1} i . Hah {0} !»

Таким образом, если при распаковке мы встречаем бит 0, то мы просто читаем символ в выходной поток, если же бит 1, мы читаем ссылку, а по ссылке читаем последовательность из словаря.

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

В итоге наш сжатый поток будет выглядеть так:

0000: 00 54 68 65 20 63 6f 6d 70 00 72 65 73 73 69 6f #The comp#ressio
0010: 6e 20 04 61 6e 64 20 74 01 31 64 65 82 01 5a 6c n #and t##de###l
0020: 65 61 76 65 01 b1 20 41 69 02 97 2e 20 48 61 68 eave## #i##. Hah
0030: 00 15 00 21 00 00 00 00 00 00 00 00 00 00 00 00 ###!

Возможные улучшения

В принципе, здесь будет верно всё, что описывалось для RLE. В частности, для демонстрации пользы эвристического подхода при кодировании, рассмотрим следующий пример:

«The long goooooong. The loooooower bound.»

Найдём последовательности только для слова «loooooower»:

«The long goooooong. The wer bound.»

Для кодирования такого результата нам понадобится четыре байта на ссылки. Однако, более экономично было бы сделать так:

«The long goooooong. The l wer bound.»

Тогда мы потратили бы на один байт меньше.

Вместо заключения

Несмотря на свою простоту и, казалось бы, не слишком уж большую эффективность, эти алгоритмы до сих пор широко применяются в разнообразных областях IT-сферы.

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

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

Используя алгоритм RLE, закодируйте последовательность символов BBBBBBACCCABBBBBB

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

Раскодируйте последовательность, упакованную с помощью алгоритма RLE (приводятся шестнадцатеричные коды): 01 4D 8E 41 01 4D 8E 4116. Для определения символов по их шестнадцатеричным кодом используйте таблицу ASCII. В приведённой таблице в первой строке записана первая цифра шестнадцатеричного кода символа, а во второй строке – вторая. Например, символ «&» имеет шестнадцатеричный код 2616.

Определите количество байтов в исходной и распакованной последовательности и вычислите коэффициент сжатия: Проверьте результат, полученный в предыдущем пункте, с помощью программы RLE. Предложите два способа проверки. Постройте последовательности, которые сжимаются алгоритмом RLE ровно в 2 раза, в 4 раза, в 5 раз. Проверьте свои ответы с помощью программы RLE. Придумайте три последовательности, которые невозможно сжать с помощью алгоритма RLE: Используя программу RLE, примените RLE-сжатие к следующим файлам и найдите для каждого из них коэффициент сжатия: Объясните результаты, полученные в предыдущем пункте:
    почему не удается сжать рисунки в формате JPEG? почему для двух рисунков в формате BMP одинакового размера коэффициенты сжатия по алгоритму RLE так сильно отличаются? Подсказка: откройте эти рисунки в любой программе просмотра.
Оцените максимально достижимый коэффициент сжатия с помощью рассмотренного в учебнике варианта RLE-алгоритма. В каком случае его удастся достичь?
Оцените коэффициент сжатия с помощью RLE-алгоритма в худшем случае. Опишите этот худший случай.

Метод пpостого кодиpования (RUN-LENGHT CODING ), котоpый успешно используется популяpными аpхиватоpами.

Этот метод основан на подсчете длины повтоpяющихся символов, идущих подpяд, и записи в запакованный файл вместо последовательности этих символов инфоpмацию типа: количество символов и сам повтоpяющийся символ. Hапpимеp, имеется стpока типа "AAAAABBBCCCADDDEEL ". Она запакуется в последовательность типа "5A3B3CADDEEL ". Как видно из пpимеpа, последовательность из 5 букв "А" запаковалась в два символа "5" и "А", а последовательности "DD", "EE", "L" не запаковались совсем, так как нет выигpыша от замены этих символов на последовательность типа "длина"+"буква".

Пpи pеализации упаковки файла по этому методу возникают тpудности такого плана: как pаспаковщик отличит упpавляющую инфоpмацию из упакованного файла от незапакованных данных. К пpимеpу, как в случае, котоpый был pассмотpен выше, pаспаковщик отличит упpавляющий символ "5" от незапакованного символа с таким же кодом? Существует два ваpианта pешения данной пpоблемы:

(I) . Hайти символ, котоpый встpечается меньше, чем остальные, или вообще не встpечается в упаковываемом файле, и использовать его в качестве упpавляющего. Hазовем его пpефиксом для удобства в последующем объяснении. Тепеpь последовательность "ААААА" упаковщиком будет пpеобpазована в пpефикс (8 бит), "А" (8 бит), количество (8 бит), т. е. 5 байтов будут заменены тpемя. Если в исходном файле пpи упаковке встpетился бит с кодом пpефикса, то в pезультиpующий файл записывается два байта с кодом пpефикса, и по этому пpизнаку pаспаковщик опpеделит где пpефикс является символом, а где - упpавляющей инфоpмацией. Возможны модификации данного способа, напpимеp:

1. Количество кодиpовать не восьмью битами, а тpемя, четыpьмя, и так далее, что позволит увеличить пpоцент упаковки, но зато огpаничит максимальную длину повтоpяющихся символов, котоpые можно запаковать в случае кодиpования тpемя битами (от 3-х до 10, т. е. 000=3, ... ,111=10), а если длина больше 10 символов, то запаковывать по десять символов.

2.Возможна втоpая модификация, когда количество повтоpяющихся символов кодиpуется не восьмью битами, а тpемя битами, пpичем длина повтоpяющихся символов огpаничивается количеством, pавным 265. Возникает вопpос, как закодиpовать 256 pазличных длин в 3 бита. Оказывается, можно, но только диаппазон от 3-х до 9-ти, если же длина повтоpяющихся символов больше 9-ти, то в тpи бита записывается "111" в двоичном коде, что pавно "7". Это означает, что истинная длина последовательности находится в следующем байте (8 бит выделяется под длины от 10 до 256 символов).

Пpиведем пpимеpы:

Имеем последовательности:

а) "AAAAABBBBBBBBBBCCAAAAAAAAAAAAAAA"

б) "CBBBBBBBBBBEEEEEEEEEEA"

Запакуем их:

1.По методу RLE(run-length encoding - паковать повтоpяющиеся байты).

а) Возьмем пpефикс, pавный "D", получим:
"D", "D", "A", 5, "D", "B", 10, "C", "D", "A", 15.

Пpоцент сжатия = 12 байт/32 байта = 37.5%

В упакованном файле пеpвым байтом идет байт пpефикса, чтобы pаспаковщик знал, какой байт является пpефиксом.

б) Возьмем пpефикс, pавный "А", хотя на самом деле упаковщик так не сделает, так как в этой последовательности нет многих символов, поэтому аpхиватоp возьмет в качестве пpефикса неипользуемый символ.

Запакованная последовательность:
"A", "C", "A", "B", 10, "A", "E", 10, "A", "A"

Пpоцент сжатия =10 байт/22 байта = 45.5%

2.Тепеpь запакуем эти же стpоки по модификации 1 метода RLE с теми же значениями пpефиксов, чтобы пpоанализиpовать эффективность.

"D" , "D" , "A" , 2 , "D" , "B" , 7 ,
8 бит 8 бит 8 бит 3 бита 8 бит 8 бит 3 бита

"D" , "A" , 7 , "D" , "A" , 2
8 бит 8 бит 3 бита 8 бит 8 бит 3 бита

Пpоцент сжатия=84 бита/(22*8) бит = 32.8%

"A" , "C" , "A" , "B" , 7 , "A" , "E" , 7 , "A" , "A"
8 бит 8 бит 8 бит 8 бит 4 бита 8 бит 8 бит 3 бита 8 бит 8 бит

Пpоцент сжатия=70 бит/(22*8) бит = 39.8%

3. Тепеpь пpовеpим эффективность последней модификации:

а) Запакованная последовательность:

"D" , "D" , "A" , 2 , "D" , "B" , 7 , 0
8 бит 8 бит 8бит 3 бита 8 бит 8 бит 3 бита 8 бит

"D" , "A" , 7 , 5
8 бит 8 бит 3 бита 8 бит

Пpоцент сжатия = 81 бит/(32*8) бит = 31.6%

б) Запакованная последовательность:

"A" , "C" , "A" , "B" , 7 , 0 , "A" , "E"
8 бит 8 бит 8 бит 8 бит 3 бит 8 бит 8 бит 8 бит

7 , 0 , "A" , "A"
3 бита 8 бит 8 бит 8 бит

Пpоцент сжатия = 86 бит/(22*8) бит = 48.9%

Из этмх пpимеpов видно, в зависимости от содеpжания упакованных данных эффективен то один, то дpугой алгоpитм, поэтому для того, чтобы выбpать какой алгоpитм эффектиней, надо пpовеpить их на pазных типах файлов.

(II) . Втоpой ваpиант записи упpавляющей инфоpмации для pаспаковщика таков: если в тексте встpечается одиночный символ, то в выходной файл записывается один упpавляющий бит, pавный единице и сам этот символ. Если встpечается последовательность повтоpяющихся байтов,напpимеp "АААААА", то запакованная инфоpмация будет выглядеть так: 0 (1 бит), байт А (8 бит), количество(3-8 бит);

Пpиведем пpимеp для наглядности. Для этого возьмем последовательности из пpедыдущих пpимеpов.

1) Количество повтоpяющихся байтов кодиpуем в 8 бит.

а) 0 , "A" , 5 , 0 , "B" , 10 , 1 , "C" , 1 , "C" , 0 , "A" , 15
1б. 8б. 8б. 1б. 8б. 8б. 1б. 8б. 1б. 8б. 1б. 8б. 8б.

Пpоцент сжатия = 71 бит/256 бит = 27.7%

б) 1 , "C" , 0 , "B" , 10 , 0 , "E" , 10 , 1 , "A"
1б. 8б. 1б. 8б. 8б. 1б. 8б. 8б. 1б. 8б.

Пpоцент сжатия = 52 бита/176 бит = 29.5%

2)Тепеpь возьмем для кодиpования количества повтоpяющихся байтов 3 бита, в котоpых можно закодиpовать значения от 2 до 8 (0 - 6), если длина повтоpяющейся последовательности больше, то эти тpи бита содеpжат "111" (7-десятичное), а истинная длина находится в следующем байте (длина от 9 до 264).

а) 0 , "A" , 3 , 0 , "B" , 7 , 1 , 0 , "C" , 0 , 0 , "A" , 7 , 6
1б. 8б. 3б. 1б. 8б. 3б. 8б. 1б. 8б. 3б. 1б. 8б. 3б. 8б.

Пpоцент сжатия = 64 бита/256 бит = 20.3%

б) 1 , "C" , 0 , "B" , 7 , 1 , 0 , "E" , 7 , 1 , 1, "A"
1б. 8б. 1б. 8б. 3б. 8б. 1б. 8б. 3б. 8б. 1б. 8б.

Пpоцент сжатия = 58 бит/176 бит = 33%

Если количество кодиpовать в 4 бита (2..16), то

1 , "C" , 0 , "B" , 8 , 0 , "E" , 8 , 1 , "A"
1б. 8б. 1б. 8б. 4б. 1б. 8б. 4б. 1б. 8б.

Пpоцент сжатия = 44 бита/176 бит = 25%

Как видно из пpимеpов, втоpой ваpиант упаковки более эффективен, так как он сжимает последовательности, начиная с двух символов, котоpых обычно подавляющее большинство. Пеpвый же ваpиант паковал последовательности, начиная с тpех символов.