Алгоритмы LZ77 и LZ78 — различия между версиями
Nsychev (обсуждение | вклад) м |
м (rollbackEdits.php mass rollback) |
||
(не показано 6 промежуточных версий 4 участников) | |||
Строка 58: | Строка 58: | ||
| <tex>\fbox{$ab$}acabacabadaca</tex> || <tex>a</tex> || <tex>\langle 2, 1, c \rangle</tex> || | | <tex>\fbox{$ab$}acabacabadaca</tex> || <tex>a</tex> || <tex>\langle 2, 1, c \rangle</tex> || | ||
|- | |- | ||
− | | <tex>\fbox{$abac$}abacabadaca</tex> || <tex>abacaba</tex> || <tex>\langle 4, 7, d \rangle</tex> || Здесь выгодно сделать так, что <tex>offset | + | | <tex>\fbox{$abac$}abacabadaca</tex> || <tex>abacaba</tex> || <tex>\langle 4, 7, d \rangle</tex> || Здесь выгодно сделать так, что <tex>offset < length</tex> |
|- | |- | ||
| <tex>abacaba\fbox{$cabad$}aca</tex> || <tex>a</tex> || <tex>\langle 2, 1, c \rangle</tex> || Последовательность <tex>aca</tex> уже встречалась, но она находится за пределами окна, и <tex>\mathrm{LZ77}</tex> её не находит | | <tex>abacaba\fbox{$cabad$}aca</tex> || <tex>a</tex> || <tex>\langle 2, 1, c \rangle</tex> || Последовательность <tex>aca</tex> уже встречалась, но она находится за пределами окна, и <tex>\mathrm{LZ77}</tex> её не находит | ||
Строка 123: | Строка 123: | ||
dict[buffer + s[i]] = dict.length + 1 <font color=green>// добавляем слово в словарь</font> | dict[buffer + s[i]] = dict.length + 1 <font color=green>// добавляем слово в словарь</font> | ||
buffer = "" <font color=green>// сбрасываем буфер</font> | buffer = "" <font color=green>// сбрасываем буфер</font> | ||
+ | '''if''' not (buffer is empty): <font color=green>// если буффер не пуст - этот код уже был, нужно его добавить в конец словаря</font> | ||
+ | last_ch = buffer.peek() <font color=green>// берем последний символ буффера, как "новый" символ</font> | ||
+ | buffer.pop() <font color=green>// удаляем последний символ из буфера</font> | ||
+ | ans.push({dict[buffer], last_ch}) <font color=green>// добавляем пару в ответ</font> | ||
'''return''' ans | '''return''' ans | ||
</code> | </code> | ||
Строка 135: | Строка 139: | ||
| <tex>\varnothing</tex> || <tex>abacababacabc</tex> || {{---}} || <tex>\langle 0, a \rangle</tex> || В словаре ничего не нашлось, вместо номера в словаре указываем <tex>0</tex> | | <tex>\varnothing</tex> || <tex>abacababacabc</tex> || {{---}} || <tex>\langle 0, a \rangle</tex> || В словаре ничего не нашлось, вместо номера в словаре указываем <tex>0</tex> | ||
|- | |- | ||
− | | <tex>a</tex> || <tex> | + | | <tex>a</tex> || <tex>bacababacabc</tex> || {{---}} || <tex>\langle 0, b \rangle</tex> || |
|- | |- | ||
| <tex>a</tex>, <tex>b</tex> || <tex>acababacabc</tex> || <tex>a</tex> || <tex>\langle 1, c \rangle</tex> || Нашелся префикс <tex>a</tex> (слова в словаре нумеруются с <tex>1</tex>), добавляем <tex>ac</tex> | | <tex>a</tex>, <tex>b</tex> || <tex>acababacabc</tex> || <tex>a</tex> || <tex>\langle 1, c \rangle</tex> || Нашелся префикс <tex>a</tex> (слова в словаре нумеруются с <tex>1</tex>), добавляем <tex>ac</tex> |
Текущая версия на 19:05, 4 сентября 2022
[1] и Якобом Зивом[2] в и годах соответственно. Эти алгоритмы стали основой других алгоритмов семьи : LZW, LZSS, LZMA и другие. Оба приведенных алгоритма используют словарный подход.
и — алгоритмы сжатия без потерь, опубликованные Абрахамом ЛемпелемСодержание
LZ77
Идея алгоритма
В кодируемых строках часто содержатся совпадающие длинные подстроки. Идея, лежащая в основе
, заключается в замене повторений на ссылки на позиции в тексте, где такие подстроки уже встречались.Информацию о повторении можно закодировать парой чисел — смещением назад от текущей позиции (
) и длиной совпадающей подстроки ( ). В таком случае, например, строка может быть представлена как . Выражение означает «вернись на символов назад и выведи символов».Алгоритм
кодирует ссылки блоками из трёх элементов — . В дополнение к двум уже описанным элементам, новый параметр означает первый символ после найденного совпадающего фрагмента. Если не удалось найти совпадение, то считается, что .Для эффективного поиска повторов в
применяется метод «скользящего окна» — совпадения ищутся не на всём обработанном префиксе, а в небольшом буфере, состоящем из последних обработанных символов. Обычно длина буфера равна , или . Таким образом, при больших объемах ввода алгоритм тратит меньше времени за счет того, что просматривается не вся исходная строка. С другой стороны, слишком маленькая длина словаря может привести к тому, что расположенные далеко друг от друга совпадения (на большем расстоянии, чем длина словаря) не будут учтены, и кодирование станет менее оптимальным.Также нетривиальным может оказаться то, что при кодировании
значение может быть меньше, чем (например, «вернись на символа назад и выведи символов»). Это означает, что подстрока будет повторяться несколько раз так, чтобы каждый символ совпадал с символом, стоящим на позиции раньше, и всего добавилось символов. Например: . Символ всегда можно определить однозначно, даже если он отсутствует в буфере.Реализация
Хранить результат кодирования будем в списке структур следующего вида:
struct Node: int offset int length char next
Функция findMatching
ищет в буфере строку, совпадающую с префиксом суффикса строки , начинающегося с символа и возвращает искомую пару .
Функция shiftBuffer
сдвигает буфер вперёд на символов.
//— исходная строка // функция возвращает список блоков list<Node> encodeLZ77(string s): list<Node> ans = [] pos = 0 while pos < s.length: offset, length = findMatching(buffer, pos) // ищем слово в словаре shiftBuffer(length + 1) // перемещаем скользящее окно pos += length ans.push({offset, length, s[pos]}) // добавляем в ответ очередной блок return ans
Пример
Рассмотрим пример кодирования
на строке с буфером размера . Квадратом обведен буфер.Строка | Совпадение | Закодированная последовательность | Примечание |
---|---|---|---|
— | Буфер пуст | ||
— | В буфере нет | ||
Здесь выгодно сделать так, что | |||
Последовательность | уже встречалась, но она находится за пределами окна, и её не находит|||
Символов в строке больше нет, поэтому в качестве | ставим символ конца строки
Результатом кодирования является список полученных троек:
.Декодирование
Для декодирования
необходимо пройти по уже раскодированной строке назад, вывести необходимую последовательность, затем следующий символ.Псевдокод этого алгоритма:
//— список блоков // функция возвращает декодированную строку string decodeLZ77(list<Node> encoded): ans = "" for node in encoded: if node.length > 0: // если необходим повтор start = ans.length - node.offset // возвращаемся на символов назад for i = 0 .. node.length - 1: // добавляем символов ans += ans[start + i] ans += node.next // добавляем следующий символ return ans
Модификации
Известно много различных модификаций алгоритма [3]). Алгоритм LZSS является улучшением и хранит однобитный флаг, показывающий, какие данные за ним идут: одиночный символ или пара смещения и длины. В формате [4] на каждую пару смещения и длины выделяется по байта.
. Например, вместо блоков можно хранить отдельно одиночные символы и отдельно — пары (length-distance pairТакже на [5] и [6] (последний является широко используемым), которые совмещают с алгоритмом Хаффмана, кодируя элементы пар и одиночные символы.
основаны алгоритмы
LZ78
Идея алгоритма
Алгоритм
имеет немного другую идею: этот алгоритм в явном виде использует словарный подход, генерируя временный словарь во время кодирования и декодирования.Изначально словарь пуст, а алгоритм пытается закодировать первый символ. На каждой итерации мы пытаемся увеличить кодируемый префикс, пока такой префикс есть в словаре. Кодовые слова такого алгоритма будут состоять из двух частей — номера в словаре самого длинного найденного префикса (
) и символа, который идет за этим префиксом ( ). При этом после кодирования такой пары префикс с приписанным символом добавляется в словарь, а алгоритм продолжает кодирование со следующего символа.Реализация
Будем хранить результат кодирования в такой структуре:
struct Node: int pos // номер слова в словаре char next
В качестве словаря будем использовать обычный map
:
list<Node> encodeLZ78(string s): string buffer = "" // текущий префикс map<string, int> dict = {} // словарь list<Node> ans = [] // ответ for i = 0 .. s.length - 1: if dict.hasKey(buffer + s[i]): // можем ли мы увеличить префикс buffer += s[i] else: ans.push({dict[buffer], s[i]}) // добавляем пару в ответ dict[buffer + s[i]] = dict.length + 1 // добавляем слово в словарь buffer = "" // сбрасываем буфер if not (buffer is empty): // если буффер не пуст - этот код уже был, нужно его добавить в конец словаря last_ch = buffer.peek() // берем последний символ буффера, как "новый" символ buffer.pop() // удаляем последний символ из буфера ans.push({dict[buffer], last_ch}) // добавляем пару в ответ return ans
Пример
В этот раз для примера возьмем строку
.Словарь | Осталось обработать | Найденный префикс | Код | Примечание |
---|---|---|---|---|
— | В словаре ничего не нашлось, вместо номера в словаре указываем | |||
— | ||||
, | Нашелся префикс | (слова в словаре нумеруются с ), добавляем|||
, , | ||||
, , , | ||||
, , , , | — | |||
, , , , , |
Результатом кодирования является список пар:
Декодирование
Декодирование происходит аналогично кодированию, на основе декодируемой информации строим словарь и берем из него значения.
string decodeLZ78(list<Node> encoded): list<string> dict = [""] // словарь, слово с номером 0 — пустая строка string ans = "" // ответ for node in encoded: word = dict[node.pos] + node.next // составляем слово из уже известного из словаря и новой буквы ans += word // приписываем к ответу dict.push(word) // добавляем в словарь return ans
Модификации
Одна из главных проблем
— размер словаря. Он ничем не ограничен и может достигать огромных размеров на больших объемах входных данных. В различных модификациях установлены лимиты на размер словаря: при достижении определенного лимита словарь может «замораживаться» (в него перестают добавляться новые слова), из словаря могут удалять менее используемые или самые старые слова и так далее.Самой известной модификацией LZW. В нём есть две важных особенности. Первая — словарь изначально инициализирован всеми символами алфавита. Вторая — после нахождения максимального префикса поиск следующего префикса начинается не с символа после неподходящего, а с самого неподходящего символа. Это позволяет не выводить неподходящий символ, а выяснять его прямо из словаря. Эта модификация используется в том числе в изображениях в формате [7].
является