Самый компактный способ кодирования последовательности двоичных кодов произвольной длины? - PullRequest
10 голосов
/ 29 января 2010

Допустим, у вас есть List<List<Boolean>>, и вы хотите закодировать его в двоичную форму максимально компактным способом.

Мне плевать на производительность чтения или записи. Я просто хочу использовать минимальное количество места. Кроме того, пример на Java, но мы не ограничены системой Java. Длина каждого «Списка» составляет неограниченно . Поэтому любое решение, которое кодирует длину каждого списка, должно само по себе кодировать тип данных переменной длины.

С этой проблемой связано кодирование целых чисел переменной длины. Вы можете думать о каждом List<Boolean> как о переменной длине unsigned integer.

Пожалуйста, внимательно прочитайте вопрос. Мы не ограничены системой Java.

EDIT

Я не понимаю, почему многие ответы говорят о сжатии. Я не пытаюсь выполнить сжатие как таковое, а просто кодирую random последовательность битов вниз. За исключением того, что каждая последовательность битов имеет разную длину и порядок должен быть сохранен.

Вы можете думать об этом вопросе по-другому. Допустим, у вас есть список произвольного списка случайных целых чисел без знака (неограниченный). Как вы кодируете этот список в двоичном файле?

Исследования

Я немного прочитал и нашел то, что действительно ищу, это Универсальный код

Результат

Я собираюсь использовать вариант Elias Omega Coding , описанный в статье Новый рекурсивный универсальный код натуральных чисел

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

Ответы [ 16 ]

9 голосов
/ 02 февраля 2010

Я думаю о кодировании битовой последовательности, подобной этой:

head  | value
------+------------------
00001 | 0110100111000011

Head имеет переменную длину. Его конец отмечен первым появлением 1. Подсчитайте количество нулей в голове. Длина поля value будет 2 ^ zeroes. Поскольку длина значения известна, это кодирование может повторяться. Поскольку размер head равен log value, при увеличении размера кодированного значения накладные расходы сходятся к 0%.

Добавление

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

head  | length | value
------+--------+-----------
00001 | 1001   | 011011001
7 голосов
/ 01 февраля 2010

Я не очень разбираюсь в Java, поэтому думаю, что мое решение должно быть общим:)

1. Сжать списки

Поскольку логические значения неэффективны, каждый List<Boolean> должен быть сжат до List<Byte>, это просто, просто захватите их по 8 за раз.

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

2. Сериализация списка элементов

У вас есть 2 способа продолжить: либо вы кодируете количество элементов в списке, либо вы используете шаблон, чтобы отметить конец. Я бы порекомендовал кодировать количество элементов, шаблонный подход требует экранирования, и это жутко, плюс это сложнее с упакованными битами.

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

0... .... > this byte encodes the number of items (7 bits of effective)
10.. .... / .... .... > 2 bytes
110. .... / .... .... / .... .... > 3 bytes

Это довольно экономно, и декодирование происходит на целых байтах, так что не слишком сложно. Можно заметить, что это очень похоже на схему UTF8:)

3. Применить рекурсивно

List< List< Boolean > > становится [Length Item ... Item], где каждый Item является представлением List<Boolean>

4. Zip

Полагаю, для Java доступна библиотека zlib или что-то еще, например deflate или lcw. Передайте его в буфер и убедитесь, что вы хотите максимально сжать, сколько бы времени это ни потребовало.

Если в вашем представлении есть какой-либо повторяющийся шаблон (даже тот, который вы не видели), он должен быть в состоянии сжать его. Не доверяйте этому тупо, хотя и проверяйте, что «сжатая» форма легче, чем «несжатая», это не всегда так.

5. Примеры

Где замечено, что отслеживание края списков занимает много места:)

// Tricky here, we indicate how many bits are used, but they are packed into bytes ;)
List<Boolean> list = [false,false,true,true,false,false,true,true]
encode(list) == [0x08, 0x33] // [00001000, 00110011]  (2 bytes)

// Easier: the length actually indicates the number of elements
List<List<Boolean>> super = [list,list]
encode(super) == [0x02, 0x08, 0x33, 0x08, 0x33] // [00000010, ...] (5 bytes)

6. Потребление пространства

Предположим, у нас есть List<Boolean> из n логических значений, пространство, используемое для его кодирования:

booleans = ceil( n / 8 )

Для кодирования количества бит (n) нам понадобится:

length = 1   for 0    <= n < 2^7   ~ 128
length = 2   for 2^7  <= n < 2^14  ~ 16384
length = 3   for 2^14 <= n < 2^21  ~ 2097152
...
length = ceil( log(n) / 7 )  # for n != 0 ;)

Таким образом, чтобы полностью закодировать список:

bytes =
 if n == 0: 1
 else     : ceil( log(n) / 7 ) + ceil( n / 8 )

7. Маленькие списки

Существует один угловой случай: нижний предел спектра (т. Е. Почти пустой список).

Для n == 1, bytes оценивается как 2, что действительно может показаться расточительным. Однако я не буду пытаться угадать, что произойдет, когда произойдет сжатие.

Возможно, вы захотите упаковать еще больше. Это возможно, если мы откажемся от идеи сохранения целых байтов ...

  1. Сохраняйте кодировку длины как есть (на целых байтах), но не добавляйте List<Boolean>. Список из одного элемента становится 0000 0001 x (9 бит)
  2. Попробуйте также "упаковать" кодировку длины

Второй пункт сложнее, мы фактически используем кодирование двойной длины:

  1. Указывает, сколько битов кодируют длину
  2. Фактически кодировать длину в этих битах

Например:

0  -> 0 0
1  -> 0 1
2  -> 10 10
3  -> 10 11
4  -> 110 100
5  -> 110 101
8  -> 1110 1000
16 -> 11110 10000 (=> 1 byte and 2 bits)

Работает очень хорошо для очень маленьких списков, но быстро вырождается:

# Original scheme
length = ceil( ( log(n) / 7)

# New scheme
length = 2 * ceil( log(n) )

Прорыв? 8

Да, вы правильно прочитали, это лучше только для списка с менее чем 8 элементами ... и только лучше "битами".

n         -> bits spared
[0,1]     ->  6
[2,3]     ->  4
[4,7]     ->  2
[8,15]    ->  0    # Turn point
[16,31]   -> -2
[32,63]   -> -4
[64,127]  -> -6
[128,255] ->  0    # Interesting eh ? That's the whole byte effect!

И, конечно же, как только компрессия начнет работать, скорее всего, это не будет иметь большого значения.

Я понимаю, что вы, возможно, оцените алгоритм recursive, но я бы все же посоветовал вычислить цифры фактического потребления пространства или, что еще лучше, проверить его с использованием архивирования, примененного к реальным тестовым наборам.

8. Рекурсивное / Переменное кодирование

Я с интересом прочитал ответ TheDon и ссылку, которую он отправил на Elias Omega Coding .

Это разумные ответы в теоретической области. К сожалению, они довольно непрактичны. Основная проблема заключается в том, что у них чрезвычайно интересное асимптотическое поведение, но когда нам действительно нужно кодировать гигабайтные данные? Редко, если вообще когда-либо.

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

Пример алгоритма TheDon. Скажем, у меня есть список [0,1,0,1,0,1,0,1]

len('01010101') = 8 -> 1000
len('1000')     = 4 -> 100
len('100')      = 3 -> 11
len('11')       = 2 -> 10

encode('01010101') = '10' '0' '11' '0' '100' '0' '1000' '1' '01010101'

len(encode('01010101')) = 2 + 1 + 2 + 1 + 3 + 1 + 4 + 1 + 8 = 23

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

threshold     2    3    4    5      My proposal
-----------------------------------------------
[0,3]    ->   3    4    5    6           8
[4,7]    ->   10   4    5    6           8
[8,15]   ->   15   9    5    6           8
[16,31]  ->   16   10   5    6           8
[32,63]  ->   17   11   12   6           8
[64,127] ->   18   12   13   14          8
[128,255]->   19   13   14   15         16

Если честно, я сконцентрировался на нижнем конце, и мое предложение подходит для этой задачи. Я хотел подчеркнуть, что это не так ясно, хотя. Тем более, что около 1 функция log является почти линейной, и поэтому рекурсия теряет свое очарование. Порог очень помогает, и 3 кажется хорошим кандидатом ...

Что касается Elias omega coding, это еще хуже. Из статьи в википедии:

17 -> '10 100 10001 0'

Вот и все, 11 бит.

Мораль: вы не можете выбрать схему кодирования без учета имеющихся данных.

Итак, если ваш List<Boolean> не имеет длины в сотни, не беспокойтесь и не придерживайтесь моего маленького предложения.

4 голосов
/ 02 февраля 2010

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

11000101 10010110 00100000

На самом деле будет означать:

   10001 01001011 00100000

Поскольку целое число продолжается 2 раза.

Эти целые числа переменной длины скажут, сколько бит нужно прочитать. И в начале будет еще один int переменной длины, который скажет, сколько битовых наборов нужно прочитать.

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


РЕДАКТИРОВАТЬ Я не думаю, что существует идеальный способ достичь всего, чего вы хотите, все сразу. Вы не можете создавать информацию из ничего, и если вам нужны целые числа переменной длины, вам, очевидно, придется также кодировать целую длину. обязательно компромисс между пространством и информацией, но есть также минимальная информация, которую вы не можете вырезать, чтобы использовать меньше места. Ни одна система, в которой факторы растут с разной скоростью, никогда не сможет идеально масштабироваться. Это все равно что пытаться подогнать прямую линию по логарифмической кривой. Вы не можете сделать это. (И кроме того, это именно то, что вы пытаетесь сделать здесь.)

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

Итак, вот еще одна моя идея: в целочисленном «заголовке» напишите по одному 1 на каждый байт, от которого требуется целое число переменной длины. Первый 0 обозначает конец «заголовка» и начало самого целого числа.

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


РЕДАКТИРОВАТЬ 2 Вот уравнения:

  • Решение один, 7 бит на бит кодирования (один полный байт за раз):
    y = 8 * ceil(log(x) / (7 * log(2)))
  • Решение одно, 3 бита на бит кодирования (по одному кусочку за раз):
    y = 4 * ceil(log(x) / (3 * log(2)))
  • Решение два, 1 байт на бит кодирования плюс разделитель:
    y = 9 * ceil(log(x) / (8 * log(2))) + 1
  • Решение второе, 1 бит на кодовый бит плюс разделитель:
    y = 5 * ceil(log(x) / (4 * log(2))) + 1

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

3 голосов
/ 02 февраля 2010

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

Как сказал один из других авторов, вам нужно знать НЕКОТОРЫЕ о наборе данных, чтобы представить его в более компактной форме, и / или вы должны принять некоторую потерю, чтобы превратить его в предсказуемую форму, которая может быть более компактно выраженный.

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

http://en.wikipedia.org/wiki/Information_theory

Вы могли бы всегда обманывать, я полагаю, и манипулировать оборудованием, чтобы закодировать дискретный диапазон значений на носителе, чтобы выявить еще несколько «бит на бит» (подумайте о мультиплексировании). Вы бы потратили больше времени на его кодирование и чтение.

Практически, вы всегда можете попробовать эффект «покачивания», когда вы кодируете данные несколько раз разными способами (попробуйте интерпретировать как аудио, видео, 3D, периодические, последовательные, на основе ключей, различия и т. Д.) И в несколько размеров страницы и выбрать лучший. Вы гарантированно получите лучшее РАЗУМНОЕ сжатие, и ваш худший случай будет не хуже, чем ваш исходный набор данных.

Не знаю, если бы это дало вам теоретическую оценку.

3 голосов
/ 01 февраля 2010

Я не вижу, как кодирование произвольного набора битов отличается от сжатия / кодирования любой другой формы данных. Обратите внимание, что вы накладываете только ограниченное ограничение на биты, которые вы кодируете, а именно, это списки списков битов. С этим небольшим ограничением этот список битов становится просто данными, произвольными данными, и это то, что сжимают «нормальные» алгоритмы сжатия.

Конечно, большинство алгоритмов сжатия работают в предположении, что ввод будет повторен каким-либо образом в будущем (или в прошлом), как в компрессоре семейства LZxx, или имеет заданное распределение частоты для символов.

Учитывая ваши предпосылки и как работают алгоритмы сжатия, я бы посоветовал сделать следующее:

  1. Упакуйте биты каждого списка, используя как можно меньшее количество байтов, используя байты в качестве битовых полей, кодируя длину и т. Д.
  2. Попробуйте использовать huffman, арифметику, LZxx и т. Д. Для полученного потока байтов.

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

ЕСЛИ вы не знаете что-то из ваших данных или какие-то преобразования в тех списках, которые заставляют их поднимать какую-то модель. Возьмем для примера кодирование коэффициентов DCT в кодировке JPEG. Способ перечисления этих коэффициентов (диагональный и зигзагообразный) сделан так, чтобы благоприятствовать шаблону при выводе различных коэффициентов для преобразования. Таким образом, традиционные сжатия могут быть применены к полученным данным. Если вы знаете что-то из этих списков битов, которые позволяют вам переупорядочивать их более сжимаемым способом (способ, который показывает некоторую дополнительную структуру), то вы получите сжатие.

3 голосов
/ 29 января 2010

Я предполагаю, что для "самого компактного возможного способа" вам понадобится сжатие, но кодирование Хаффмана может оказаться не лучшим решением, так как я думаю, что оно лучше всего работает с алфавитами, которые имеют статические частоты для каждого символа.

Извлечение Арифметическое кодирование - оно работает с битами и может адаптироваться к вероятностям динамического входа. Я также вижу, что есть BSD-лицензированная Java-библиотека , которая сделает это за вас, и, кажется, ожидает ввода одного бита.

Полагаю, для максимального сжатия вы могли бы объединить каждый внутренний список (с префиксом его длины) и снова запустить алгоритм кодирования для всего лота.

2 голосов
/ 01 февраля 2010

Теоретические пределы

На этот вопрос сложно ответить, не зная больше о данных, которые вы намереваетесь сжать; Ответ на ваш вопрос может отличаться для разных доменов.

Например, из раздела Ограничения статьи Википедии о сжатии без потерь :

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

В принципе, поскольку теоретически невозможно сжать все возможные входные данные без потерь, невозможно даже эффективно ответить на ваш вопрос.

Практический компромисс

Просто используйте Huffman, DEFLATE, 7Z или какой-либо ZIP-подобный готовый алгоритм сжатия и кодируйте биты как байтовые массивы переменной длины (или списки, или векторы, или как они называются в Java или на любом другом языке, который вы используете). лайк). Конечно, для считывания битов может потребоваться небольшая декомпрессия, но это может быть сделано за кулисами. Вы можете создать класс, который скрывает внутренние методы реализации, чтобы возвращать список или массив логических значений в некотором диапазоне индексов, несмотря на тот факт, что данные хранятся внутри в байтовых массивах пакета. Обновление логического значения при заданном индексе или индексах может быть проблемой, но отнюдь не невозможно.

1 голос
/ 02 февраля 2010

Кодировка списка списков элементов:

  • Когда вы приходите в начало списка, запишите биты для ASCII '['. Затем перейдите в список.

  • Когда вы приходите к любому произвольному двоичному числу, запишите биты, соответствующие десятичному представлению числа в ASCII. Например, число 100, напишите 0x31 0x30 0x30. Затем запишите биты, соответствующие ASCII ','.

  • Когда вы подходите к концу списка, запишите биты для ']'. Затем напишите ASCII ','.

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

0 голосов
/ 02 февраля 2010

Если я правильно понимаю, наша структура данных (1 2 (33483 7) 373404 9 (337652222 37333788))

Формат выглядит так:

byte 255 - escape code
byte 254 - begin block
byte 253 - list separator
byte 252 - end block

Итак, имеем:

 struct {
    int nmem; /* Won't overflow -- out of memory first */
    int kind; /* 0 = number, 1 = recurse */
    void *data; /* points to array of bytes for kind 0, array of bigdat for kind 1 */
 } bigdat;

 int serialize(FILE *f, struct bigdat *op) {
   int i;
   if (op->kind) {
      unsigned char *num = (char *)op->data;
      for (i = 0; i < op->nmem; i++) {
         if (num[i] >= 252)
            fputs(255, f);
         fputs(num[i], f);
      }
   } else {
      struct bigdat *blocks = (struct bigdat *)op->data
      fputs(254, f);
      for (i = 0; i < op->nmem; i++) {
          if (i) fputs(253, f);
          serialize(f, blocks[i]);
      }
      fputs(252, f);
 }

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

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

0 голосов
/ 02 февраля 2010

Этот вопрос имеет определенное чувство индукции к нему. Требуется функция: (список bool-списка) -> (список bool), чтобы обратная функция (список bool) -> (список bool-списка) генерировала ту же исходную структуру, а длина закодированного списка bool минимальна, без наложение ограничений на структуру ввода. Поскольку этот вопрос является настолько абстрактным, я думаю, что эти списки могут быть ошеломительно большими - 10 ^ 50, может быть, или 10 ^ 2000, или они могут быть очень маленькими, например, 10 ^ 0. Кроме того, может быть большое количество списков, опять же 10 ^ 50 или просто 1. Так что алгоритм должен адаптироваться к этим очень различным входам.

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

let encode2d(list1d::Bs) = encode1d(length(list1d), true) @ list1d @ encode2d(Bs)
    encode2d(nil)       = nil

let encode1d(1, nextIsValue) = true :: nextIsValue :: []
    encode1d(len, nextIsValue) = 
               let bitList = toBoolList(len) @ [nextIsValue] in
               encode1d(length(bitList), false) @ bitList

let decode2d(bits) = 
               let (list1d, rest) = decode1d(bits, 1) in
               list1d :: decode2d(rest)

let decode1d(bits, n) = 
               let length = fromBoolList(take(n, bits)) in
               let nextIsValue :: bits' = skip(n, bits) in
               if nextIsValue then bits' else decode1d(bits', length)
assumed library functions
-------------------------

toBoolList : int -> bool list
   this function takes an integer and produces the boolean list representation
   of the bits.  All leading zeroes are removed, except for input '0' 

fromBoolList : bool list -> int
   the inverse of toBoolList

take : int * a' list -> a' list
   returns the first count elements of the list

skip : int * a' list -> a' list
   returns the remainder of the list after removing the first count elements

Накладные расходы указаны для каждого отдельного списка. Для пустого списка накладные расходы составляют 2 дополнительных элемента списка. Для 10 ^ 2000 bools накладные расходы будут 6645 + 14 + 5 + 4 + 3 + 2 = 6673 дополнительных элемента списка.

...