В цих задачах потрібно буде розібратись, як працює lossless компресія даних.
Екстра плюс, якщо будуть також написані unit-тести до алгоритмів!
(illustration by Daniel Hertzberg)
Мабуть, найперша задача з теорії інформації, це перетворити список байт у список біт.
Завдання
Перетворити рядок байт у рядок біт (по кодуванню ASCII). Не забути, що один байт перетворюється у 8 біт (не менше!).
І схожа задача, перетворити рядок біт у рядок байт:
Завдання
Перетворити рядок біт у рядок байт.
Тепер, коли ви легко перетворюється біти <-> байти, можна приступити до більш адаптивного перетворення.
В біт-рядку закодовано символи. Є розділювачі, і є символи. Закодовані вони таким чином:
Наприклад, біт рядок:
перетвориться у
Якщо розділювачі писати бітами, то виглядатиме так:
Завдання
Написати декодер бітового рядка у символьний
Бінарне дерево –- це деревовидна структура даних, де у кожної ноди є рівно дві гілки. Наприклад,
є бінарним деревом, з трьома нодами (парами дужок) і чотирма листочками (кінцевими нодами). В цьому випадку, інформація кодується числами в листочках.
Потрібно закодувати дерево у біт рядок.
Для цього треба використати обхід дерева, де спочатку ми кодуємо ліву гілку, потім праву. Якщо нода, яку ми кодуємо, не листова, то записуємо її як 0
, якщо листова –- то записуємо як 1
та дописуємо число з листочка як 8 біт.
Тобто, дерево вище перетвориться у рядок 001\x01 1\x02 01\x03 1\x04
(звісно, пробіли я поставив щоб зручніше читалось, насправді їх бути не повинно) (а також це записано у форматі біт-розділювачів з попереднього завдання).
Інший приклад, дерево
буде закодовано як (без пробілів, звісно)
або у повному форматі:
Завдання
Написати енкодер дерева з байтами-листочками у біт рядок.
Можливо ви помітили, що формат кодування з попереднього завдання дуже схожий на формат цього дерева. Саме так! Це один і той же формат.
Завдання
Написати декодер дерева з біт рядка.
Нехай дано блок довжиною 410 байт:
Видно, що багато тексту повторюється. Це означає, що ентропія блоку низька (кількість інформації на символ маленька), а отже, можна спробувати зменшити розмір блоку за рахунок збільшення ентропії (збільшити кількість інформації на символ).
Для цього потрібно побудувати спеціальний словник, і цю таблицю потрібно буде передавати разом з повідомленням.
Якщо вважати що 1 символ == 1 ASCII символ, то можна порахувати, який символ як часто зустрічається:
І, відповідно, можна порахувати частоту зустрічання кожної букви, просто поділивши на кількість символів (таблиця X1
):
Але якщо вибирати в якості "символа" не 1 байт, а кілька то можна побудувати іншу таблицю (таблиця X2
):
Для того, щоб порівняти ці дві таблиці символів, потрібно порахувати ентропію. Ентропія рахується як сума добутків ймовірностей кожного символа в таблиці на їх двійкові логарифми (зі знаком мінус):
Наприклад, для першого випадку ми отримаємо:
А для другого –- H(X2) = 5.0
. Видно, що у другому випадку ентропія більша, а отже кодування в такому словнику буде більш ефективним.
Завдання
Потрібно написати функцію, яка отримує на вхід блок даних, і для нього рахує, при якому фіксованому розмірі символа ми отримуємо максимальну ентропію. Повернути на вихід ентропію, розмір символа та таблицю частот.
Тепер, коли відома ентропія символів, можна приблизно оцінити, скільки потрібно біт на кодування оригінального блоку. Формула –- кількість символів у повідомленні помножити на ентропію:
У випадку NbitsX2
ми ділимо на 2, бо нам потрібно 205 двобайтних символів для кодування оригінального блоку.
Власне "як саме" буде відбуватись це кодування ми розглянемо трохи пізніше. Там все не зовсім просто, формула вище вірна тільки в ідеальних випадках.
Завдання
Визначити, для якого розміру символа кількість біт для кодування блоку буде мінімальною.
Але, окрім біт на кодування, нам потрібно закодувати саму таблицю. В подальшому, для кодування цієї таблиці буде потрібно 2 біти на кожен елемент таблиці + сума довжин всіх ключів таблиці у байтах.
Наприклад, в таблиці X1
є 24 ключа по одному байту кожний, тобто, 2 біти * 24 + 24 байти = 240 біт = 30 байт.
В таблиці X2
є 65 ключів по два байти кожний, тобто, 2 біти * 65 + 65 * 2 байти = 1170 біт = 147 байт.
Деталі кодування таблиці також розглянемо окремо пізніше. Там все також не просто :)
Завдання
Враховуючи, що закодований блок = кодування таблиці символів + кодування блоку, визначити, при якій довжині символа довжина закодованого блоку буде мінімальна. Відношення довжини закодованого блоку до оригінальної довжини і буде коефіцієнтом компресії.
RLE (run-length encoding) –- це спеціальний метод представлення даних. Фактично, це узагальнення ідеї, що повтори символів ми можемо представити меншими зусиллями. Наприклад, рядок
можна представити як
Але це якось просто, тому я опишу більш складний RLE. Вам потрібно буде зробити декомпресію послідовності RLE блоків.
Отже, формат блоку RLE виглядає так (одна буква –- один байт):
L
–- довжина літералу. Літерал –- це нестиснені дані, які копіюються при декомпресії без змін. Літерал починається після другого байту блоку і має довжину L
байт. Може бути нулем, це означає що літералу в блоці немає взагаліX
–- це число означає, яка довжина послідовності, яка має повторитись. Початок послідовності для повторення дивись у F
. Якщо 0, це означає немає повторуF
–- кількість байт, оффсет (зсув) назад, з якого починається повтор послідовності. Цей оффсет рахується не відносно блоку, а відносно результату декомпресіїМабуть, найкраще пояснити як це все працює, буде на прикладах.
b'\x05\x00HELLO'
–- L=5, X=0, значить читаємо 5 байт і копіюємо їх на вихід. Оскільки X=0, то жодних копіювань по оффсету. Результат: HELLO
b'\x05\x00HELLO\x01\x04 \x06'
–- тут два блоки, перший такий же як в прикладі вище, другий \x01\x04 \x06
.HELLO
(в кінці пробіл), а потім считуємо F=6. Якщо відкатити індекс на 6 назад, то ми будемо бачити букву H
, а довжина X=4 означає що треба взяти 4 символа, тобто, HELL
. І це треба додати назад до результату, щоб вийшло HELLO HELL
.Rhabarberbar
Rhabarberbarbar
Rhabarberbarbarbarbar
Rhabarberbarbarbarbarbarenbarts
Це RLE кодування –- це спрощена версія формату LZ4, найшвидшого методу декомпресії.
Завдання
Написати декодер RLE.
Основна ідея ентропійного кодування –- це ті символи, які зустрічаються частіше, закодувати меншою кількістю біт, а ті символи, які зустрічаються рідше –- більшою кількістю біт.
Девід Хафман, будучи студентом, випадково знайшов оптимальний спосіб визначення, які саме повинні бути ці кодування.
Його алгоритм в загальному виглядає так:
Пункт 1 тут ключовий, але спершу розглянемо наступні пункти.
Наприклад, у дерева ( (1, 2), (3, 4) )
будуть такі відповідності символів до біт рядків:
Не дивно, що всі символи мають однакову довжину, бо всі вони знаходяться на одному рівні в дереві. Але як щодо такого дерева: ( ( (b'A', b'B'), b'C'), b'D' )
?
В такому випадку, різні символи кодуються різними довжинами біт рядків. І, що головне, при декодування символів не буде двозначностей!
Завдання
Маючи бінарне дерево, отримати таблицю біт-кодувань для кожного листового символа дерева.
Чудово, тепер маючи набір символів виду DAADDBDADDD
можна буде перетворити його у набір біт 1 000 000 1 1 001 1 000 1 1 1
(пробіли розставлені тільки для зручності читання). Але як його декодувати назад?
Щоб декодувати цей біт рядок назад, потрібно читати один біт, і
Якщо ми потрапляємо на лист дерева, то BINGO! Ми знайшли наш символ, можна додавати його до результату і починати зчитування наступного символу.
А якщо потрапляємо на ноду дерева, то читаємо наступний біт, і так поки не дійдемо до листка.
Завдання
Зробити зчитування наступного символа з біт рядка, використовуючи бінарне дерево для декодування.
Тепер, коли всі ці алгоритми реалізовано, цікаво яке ж дерево потрібно вибрати для кодування конкретного повідомлення. І тут якраз потрібен нам алгоритм Хафмана.
Дерево будується так:
Як приклад, можна взяти дерево з Вікіпедії.
Завдання
Маючи таблицю частот символів, побудувати відповідне бінарне дерево Хафмана.
TODO
TODO