Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Сжатие данных, звука и изображений

.pdf
Скачиваний:
1692
Добавлен:
01.05.2014
Размер:
7.92 Mб
Скачать

Глава 2. Словарные методы

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

for i:=0 to 255 do

добавить i как 1-символьную строку в словарь; добавить Л в словарь;

di:=словарный индекс Л; repeat

read(ch);

if <<di,ch>> есть в словаре then

di: =словарныи индекс

<<di,ch>>;

else

 

 

output(di);

 

 

добавить < < d i , c h > >

в словарь;

di:^словарный

индекс

ch;

endif;

 

 

iintil конец входного

файла;

 

Рис. 2.8. Алгоритм LZW.

Пример: Применим алгоритм LZW для кодирования строки символов «alf _eats_alf alf а». Последовательность шагов отображе­ на в табл. 2.9. Кодер выдает на выход файл:

97 (а), 108 (1), 102 (f), 32 (_), 101 (е), 97 (а), 116 (t), 115 (s), 32 (_), 256 (al), 102 (f), 265 (alf), 97 (a),

a в словаре появляются следующие новые записи:

(256: al), (257: If), (258: f_), (259: _е), (260: еа), (261: at), (262: ts), (263: s_), (264: _a), (265: alf), (266: fa), (267: alfa).

Пример: Проанализируем сжатие строки «аааа...» алгорит­ мом LZW. Кодер заносит первую «а» в I, ищет и находит а в слова­ ре. Добавляет в I следующую а, находит, что строки 1х (=аа) нет в словаре. Тогда он добавляет запись 256: аа в словарь и выводит мет­ ку 97 (а) в выходной файл. Переменная I инициализируется второй «а», вводится третья «а», 1х вновь равна аа, которая теперь имеет­ ся в словаре. I становится аа, появляется четвертая «а». Строка

2.4- LZW lOJ^

равна aaa, которых нет в словаре. Словарь пополняется этой стро­ кой, а на выход идет метка 256 (аа). I инициализируется третьей «а», и т.д. и т.п. Дальнейший процесс вполне ясен.

Врезультате строки а, аа, ааа, аааа. . . добавляются в словарь

впозиции 256, 257, 258, ..., а на выход подается последовательность

97 (а), 256 (аа), 257 (ааа), 258 (аааа), . . . .

Значит, выходной файл состоит из указателей на все более и более длинные последовательности символов а, и А; указателей обозначают строку, длина которой равна 1 -\-2Л' - -- -\- к — {к ~\- А;^)/2.

 

В

Новая

 

 

В

Новая

 

 

I

словаре ?

запись

Выход

I

словаре?

запись

Выход

а

да

 

 

s_

нет

263-s_

115 (s)

a l

нет

25б-а1

9 7 ( a )

_

да

264-_а

32(_)

1

да

 

 

_a

нет

If

нет

257-lf

108 (1)

a

да

 

 

 

f

да

 

 

a l

да

 

 

 

f-

нет

258-f_

102 (f)

a l f

нет

265-alf

256

(al)

_

да

259-_е

32 (w)

f

да

266-fa

102

(f)

_e

нет

fa

нет

e

да

 

 

a

да

 

 

 

ea

нет

2б0-еа

101 (е)

a l

да

 

 

 

a

да

 

 

a l f

да

 

 

 

at

нет

261-at

9 7 ( a )

a l f a

нет

267-alfa

265

(alf)

t

да

 

 

a

да

 

 

 

t s

нет

262-ts

116 (t)

a,eof

нет

 

9 7 ( a )

s

да

 

 

 

 

 

 

 

Табл. 2.9. Кодирование LZW для «alf _eats_alf alf a»

Предположим, что входной файл состоит из 1 миллиона симво­ лов а. Мы можем найти длину сжатого файла, решив квадратное

уравнение -{- к^)/2 = 1000000

относительно неизвестной к. Ре­

шение будет к

« 1414. Выходит,

что файл длиной

8 миллионов

бит будет сжат

в 1414 указателей

длины не менее 9

бит (а на са­

мом ^\еле^ 16 бит). Фактор сжатия или 8М/(1414 х 9) ^ 628.6 или 8М/(1414 X 16) ^ 353.6.

Результат потрясающий, но такие файлы попадаются весьма редко (заметим, что этот конкретный файл можно сжать, просто записав в выходной файл «1000000 а» без использования LZW).

2.4-i- Декодирование LZW

Для того, чтобы понять как работает декодер метода LZW, прежде всего еще раз напомним основные три шага, которые выполняет

Глава 2. Словарные методы

кодер каждый раз, делая очередную запись в выходной файл: (1) он заносит туда словарный указатель на строку I, (2) сохраняет строку 1х в следующей свободной позиции словаря и (3) инициализирует строку I символом X.

Декодер начинает с заполнения словаря первыми символами ал­ фавита (их, обычно, 256). Затем он читает входной файл, который состоит из указателей в словаре, использует каждый указатель для того, чтобы восстановить несжатые символы из словаря и записать их в выходной файл. Кроме того, он строит словарь тем же мето­ дом, что и кодер (этот факт, обычно, отражается фразой: кодер и декодер работают синхронно или шаг в шаг).

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

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

В нашем примере «sir . sid . . .» первым символом входного файла декодера является указатель 115. Он соответствует строке s, кото­ рая извлекается из словаря, сохраняется в I и становится первой строкой, записанной в выходной (разжатый) файл. Следующий ука­ затель - 105, поэтому в J заносится строка i, а содержимое J за­ писывается в выходной файл. Первый символ строки J добавляется к переменной I, образуя строку s i , которой нет в словаре, поэтому она добавляется в словарь на позиции 256. Содержимое переменной J переносится в переменную I; теперь I равно i. Следующий ука­ затель - 114, поэтому, из словаря извлекается строка г, заносится

вJ и пишется в выходной файл. Переменная I дополняется до зна­ чения ir; такой строки нет в словаре, поэтому она туда заносится под номером 257. Переменная J переписывается в I, теперь I равно г. На следующем шаге декодер читает указатель 32, записывает «_»

вфайл и сохраняет в словаре строку г_.

Пример: Строка «alf-eats_alfalfa» будет декодироваться с помопцэю сжатого файла из примера на стр. 100 следующим образом: 1. Читаем указатель 97. Из словаря заносим 1=«а» и выводим в файл

24LZW 103J

«a». Строку Ix надо записать в словарь, но х неизвестно.

2.Читаем указатель 108. Из словаря заносим J=«l» и выводим в файл «1». Сохраняем «al» в словаре под номером 256. Заносим 1=«1».

3.Читаем указатель 102. Из словаря заносим J=«f» и выводим в файл «f». Сохраняем «If» в словаре под номером 257. Заносим I=«f».

4.Читаем указатель 32. Из словаря заносим J=«_» и выводим в файл

«_». Сохраняем «f _» в словаре под номером 258. Заносим 1=«_».

5.Читаем указатель 101. Из словаря заносим J=«e» и выводим в файл «е». Сохраняем «_е» в словаре под номером 259. Заносим 1=«е».

6.Читаем указатель 97. Из словаря заносим J=«a» и выводим в файл «а». Сохраняем «еа» в словаре под номером 260. Заносим 1=«а».

7.Читаем указатель 116. Из словаря заносим J=«t» и выводим в файл «t». Сохраняем «at» в словаре под номером 261. Заносим I=«t».

8.Читаем указатель 115. Из словаря заносим J=«s» и выводим в файл «S». Сохраняем «ts» в словаре под номером 262. Заносим I=«s».

9.Читаем указатель 32. Из словаря заносим J=«_» и выводим в файл «_». Сохраняем «s_» в словаре под номером 263. Заносим 1=«_».

10. Читаем указатель 256. Из словаря заносим J=«al» и выводим в файл «al». Сохраняем «_а» в словаре под номером 264. Заносим 1=«а1».

11. Читаем указатель 102. Из словаря заносим J=«f» и выводим в файл «f». Сохраняем «alf» в словаре под номером 265. Заносим I=«f». 12. Читаем указатель 265. Из словаря заносим J=«alf» и выводим в файл «alf». Сохраняем «fa» в словаре под номером 266. Заносим I=«alf».

13.Читаем указатель 97. Из словаря заносим J=«a» и выводим в файл «а». Сохраняем «alfa» в словаре под номером 267 (хотя она никогда не будет использована). Заносим 1=«а».

14.Читаем eof. Конец.

Пример: Для алфавита из двух символов а и b покажем первые несколько шагов кодирования и декодирования последовательности «ababab. . .». Предполагается, что словарь инициализируется всего двумя строками (1: а) и (2: Ь). Кодер выдает на выход последова­ тельность

1 (а), 2 (Ь), 3 (аЬ), 5 (aba), 4 (ba), 7 (bab), б (abab), 9 (ababa), 8 (baba),. . .

И добавляет в словарь новые строки: (3: аЬ), (4: Ьа), (5: aba), (6: abab), (7: bab), (8: baba), (9: ababa), (10: ababab), (11: babab). Процесс впол­ не регулярный, его легко проанализировать и предсказать, что бу­ дет заноситься на А;-ом шаге в файл и словарь. Результат не стоит затраченных усилий.

Глава 2. Словарные методы

2.4-2. Структура словаря LZW

До этого момента считалось, что словарем LZW служит массив из строк переменной длины. Чтобы понять, почему специальное дерево будет являться лучшей структурой для словаря, следует напомнить работу кодера. Он считывает символы и добавляет их в строку I до тех пор, пока I находится в словаре. В некоторый момент строка 1х в словаре не обнаруживается, и тогда строка 1х помеш;ается в словарь. Значит, при добавлении новых строк в словарь поступа­ ет всего один новый символ х. Это предложение можно перефрази­ ровать еще так: для каждой словарной строки в словаре найдется «родительская» строка, которая короче ровно на один символ.

Поэтому для наших целей хорошим выбором будет структура дерева, которая уже использовалось в LZ78, при построении кото­ рого добавление новых строк 1х делается добавлением всего одного нового символа х. Основная проблема состоит в том, что каждый узел дерева LZW может иметь много потомков (дерево не двоич­ ное, а q-ичное, где q - это объем алфавита). Представим себе узел буквы а с номером 97. В начале у него нет потомков, но если к дереву добавится строка аЬ, то у а появится один потомок. Если позже добавится новая строка, скажем, ае, то потомков станет два, и так далее. Значит, дерево следует сконструировать так, чтобы каждый узел в нем мог бы иметь любое число потомков, причем без резервирования дополнительной памяти для них заранее.

Один путь конструирования такой структуры данных - это по­ строение дерева в виде массива узлов, в котором каждый узел явля­ ется структурой из двух полей: символа и указателя на узел роди­ теля. В самом узле нет указателей на его потомков. Перемещение вниз по дереву от родителя к потомку делается с помощью про­ цесса хеширования (перемешивания или рандомизации), в котором указатели на узлы и символы потомков перемешиваются некоторой функцией для создания новых указателей.

Предположим, что строка аЬс уже была на входе, символ за сим­ волом, и ее сохранили в трех узлах с адресами 97, 266, 284. Пусть далее за ними следует символ d. Теперь кодер ищет строку abed, а точнее, узел, содержащий символ d, чей родитель находится по адресу 284. Кодер хеширует адреса 284 (указатель на строку аЬс) и 100 (ASCH код d) и создает указатель на некоторый узел, скажем, 299. Потом кодер проверяет узел 299. Возможны три случая:

1. Этот узел не используется. Это означает строки abed еще нет в словаре и его следует туда добавить. Кодер делает это путем со­ хранения родительского указателя и ASCH кода 100 в этом узле.

2.4- LZW 105^

Получаем следующий результат:

Узел

Адрес

97

266

284

299

Содержимое

(-: «а»)

(97: «Ь»)

(266: «с»)

(284: «d»)

Строки

«а»

«аЬ»

«abc»

«abed»

2.Узел содержит родительский указатель 284 и ASCII код симво­ ла d. Это означает, что строка abed уже есть на дереве. Тогда кодер вводит следующий символ, скажем, е и ищет на дереве строку abede.

3.В узле хранится что-то другое. Это означает, что хеширование другой пары указателя и кода ASCII дало адрес 299, и узел 299 уже содержит в себе информацию про другие строки. Такая ситуация называется коллизией^ и ее можно разрешить несколькими путями. Простейший путь разрешения коллизии - это увеличивать адрес до 300, 301,... до тех пор, пока не будет найден пустой узел или узел с содержимым (284: «d»).

На практике узлы строятся состоящими из трех полей: указа­ теля на родительский узел, указателя (или индекса), созданного в процессе хеширования, и кода (обычно, ASCII) символа, лежащего в этом узле. Второе поле необходимо для разрешения коллизий. Таким образом, узел имеет вид триплета

parent index symbol

Пример: Проиллюстрируем эту структуру данных с помощью строки «ababab. . .» из примера на стр. 103). Словарем будет слу­ жить массив diet, элементами которого являются структуры с тремя полями parent, index, symbol. Мы будем обращаться к этим полям с помощью выражения вида diet [pointer] .parent, где pointer - индекс в массиве. Словарь инициализируется двумя символами а и Ь. (Для простоты мы не будем использовать коды ASCII, но будем считать, что а и b имеют коды 1 и 2.) Первые несколько шагов кодера будут следующими:

Шаг 0: Отметить все позиции, начиная с третьей, как неиспользуе­ мые.

/ / / / /

12

аЬ

Глава 2. Словарные методы

Шаг 1: Первый входной символ а помещается в переменную I. Точ­ нее, переменной I присваивается значение кода 1. Поскольку это первый символ входного файла, кодер его не ищет в словаре.

Шаг 2: Второй символ b помещается в переменную J, то есть, J=2. Кодер должен искать в словаре строку аЬ. Он выполняет операцию p o i n t e r : = h a s h ( I , J ) . Здесь hash(x,y) обозначает некоторую функ­ цию двух аргументов х и у. Предположим, что результатом этой операции будет 5. Поле diet [pointer] . index содержит «пусто», так как строки аЬ нет в словаре. Она туда заносится с помощью следу­ ющих действий

d i e t [ p o i n t e r ] . p a r e n t : = 1 ;

d i e t [ p o i n t e r ] . i n d e x : = p o i n t e r ; diet[pointer] . symbol:=J;

причем pointer=5 . После чего J помещается в I, то есть 1=2.

/

/

/

/

1

1

2

 

 

5

а

b

 

 

b

Шаг 3: Третий символ а поступает в J, то есть, J=l. Кодер дол­ жен искать в словаре строку Ьа. Выполняется p o i n t e r : = h a s h ( I , J ) . Пусть результат равен 8. Поле diet [pointer] . index содержит «пу­ сто», то есть, строки Ьа нет в словаре. Добавляем ее в словарь с помощью операций

diet [ p o i n t e r ] . p a r e n t : = 1 ;

d i e t [ p o i n t e r ] . i n d e x : = p o i n t e r ; diet[pointer] . symbol:=J;

причем pointer=8 . После чего J помещается в I, то есть 1=1.

/

/

/

/

1

/

/

2

/

1

2

 

 

5

 

 

8

 

а

b

 

 

b

 

 

а

 

Шаг 4' Четвертый символ b переносится в J, теперь 3=2. Кодер дол­ жен искать в словаре строку аЬ. Выполняется p o i n t e r : = h a s h ( I , J ) . Мы знаем из шага 2, что результат - 5. Поле d i e t [pointer] . index содержит 5, то есть строка аЬ имеется в словаре. Значение перемен­ ной pointer заносится в I, 1=5.

Шаг 5: Пятый символ а попадает в J, теперь J=l. Кодер должен искать в словаре строку aba. Как обычно, выполняется оператор p o i n t e r : = h a s h ( I , J ) . Предположим, что результатом будет 8 (кол­ лизия). Поле diet [pointer] . index содержит 8, что хорошо, но поле

2.4. LZW

diet [pointer] .parent содержит 2 вместо ожидавшего указателя 5. Произошла коллизия, это означает, что строки aba нет в словаре по адресу 8. Тогда pointer последовательно увеличивается на 1 до тех пор, пока не найдет узел, для которого index=8 и parent=5 или, который пуст. В первом случае aba имеется в словаре, и pointer записывается в I. Во втором случае aba нет в словаре, кодер сохра­ няет его в узле pointer и записывает в I содержимое J.

1

2

1/11/ 5

l/j

/1 8

8

/

/1

 

111

|2|

|5|

а

{ь|

|b|

 

1 ^ \

 

 

 

 

 

 

1 ^ 1

Пример: Сделаем процедуру хеширование для кодирования стро­ ки символов «alf.eats.alfalfa». Сам процесс кодирования детально разобран в примере на стр. 100. Результаты хеширования выбраны произвольно. Они не порождены какой-либо реальной функцией хе­ ширования. Все 12 построенных узлов этого дерева показаны на рис. 2.10.

1.Hash(l,97)==:278. По адресу 278 заносится (97,278,1).

2.Hash(f ,108)=266. По адресу 266 заносится (108,266,f).

3.Hash(_,102)=269. По адресу 269 заносится (102,269,_).

4.Hash(e,32)=267. По адресу 267 заносится (32,267,е).

5.Hash(a,101)=265. По адресу 265 заносится (101,265,а).

6.Hash(t,97)=272. По адресу 272 заносится (97,272,t).

7.Hash(s,116)=265. Коллизия! Спускаемся в следующую свободную позицию 268 и заносим (116,265,s).

8.Hash(_,115)=270. По адресу 270 заносится (115,270,.).

9.Hash(a,32)=268. Коллизия! Спускаемся в следующую свободную позицию 271 и заносим (32,268,а).

10.Hash(l,97)=278. По адресу 278 в поле index записано 278, а поле symbol содержит 1. Значит, ничего не надо записывать или сохра­ нять на дереве.

11.Hash(f ,278)=276. По адресу 276 заносится (278,276,f).

12.Hash(a,102)=274. По адресу 274 заносится (102,274,а).

13.Hash(l,97)=278. По адресу 278 в поле index записано 278, а поле symbol содержит 1. Значит, ничего не надо делать.

14.Hash(f ,278)=276. По адресу 276 в поле index записано 276, а поле symbol содержит f. Значит, ничего не надо делать.

15.Hash(a,276)=274. Коллизия! Спускаемся в следующую свободную позицию 275, и заносим (276,274,а).

Читатель, который тщательно следил за нашими построениями до этого момента будет счастлив узнать, что декодер LZW исполь­ зует словарь очень простым способом, не применяя хеширование.

N

Ч

Ь

24LZW

Сначала декодер, как и кодер заполняет первые 256 позиций слова­ ря кодами ASCII. Затем он читает указатели из входного файла и использует их для нахождения символов в словаре.

На первом шаге декодирования, декодер вводит первый указа­ тель и использует его для обнаружения в словаре первой строки I. Этот символ записывается в выходной (разжатый) файл. Теперь не­ обходимо записать в словарь строку 1х, но символ х еще не известен; им будет первый символ следующей строки извлекаемой из словаря.

На каждом шаге декодирования после первого декодер читает следующий указатель из файла, использует его для извлечения сле­ дующей строки J из словаря и записывает эту строку в выходной файл. Если указатель был равен, скажем, 8, то декодер изучает по­ ле diet [8] . index. Если это поле равно 8, то это правильный узел. В противном случае декодер изучает последующие элементы масси­ ва до тех пор, пока не найдет правильный узел.

После того, как правильный узел найден, его поле parent исполь­ зуется при проходе назад по дереву р^ля извлечения всех символов строки в обратном порядке. Затем символы помещаются в J в пра­ вильном порядке (см. пункт 2 после следующего примера), декодер извлекает последний символ х из J и записывает в словарь строку Хх в следующую свободную позицию. (Строка I была найдена на предыдущем шаге, поэтому на дерево необходимо добавить толь­ ко один узел с символом х.) После чего декодер перемещает J в I. Теперь он готов для следующего шага.

Для извлечения полной строки из дерева LZW декодер следует по указателям полей parent. Это эквивалентно продвижению вверх по дереву. Вот почему хеширование здесь не нужно.

Пример: В предыдущем примере описана процедура хеширова­ ния строки «alf_eats_alfalfa». На последнем шаге в позицию 275 массива был записан узел (276,274,а), а в выходной сжатый файл был записан указатель 275. Когда этот файл читается декодером, указа­ тель 275 является последним обработанным им элементом входного файла. Декодер находит символ а в поле symbol узла с адресом 275, а в поле pernt читает указатель 276. Тогда декодер проверяет адрес 276 и находит в нем символ f и указатель на родительский узел 278. В позиции 278 находится символ 1 и указатель 97. Наконец, в по­ зиции 97 декодер находит символ а и нулевой указатель. Значит, восстановленной строкой будет alf а. У декодера не возникает не­ обходимости делать хеширование и применять поле index.

Нам осталось обсудить обращение строки. Возможны два вари­ анта решения.