Лучший алгоритм сжатия для последовательности целых чисел - PullRequest
49 голосов
/ 12 ноября 2008

У меня большой массив с диапазоном целых чисел, которые в основном непрерывны, например, 1-100, 110-160 и т. Д. Все целые числа положительны. Какой будет лучший алгоритм для сжатия этого?

Я попробовал алгоритм дефляции, но это дает мне только 50% сжатия. Обратите внимание, что алгоритм не может быть с потерями.

Все числа уникальны и постепенно увеличиваются.

Также, если вы можете указать мне на реализацию Java такого алгоритма, это было бы здорово.

Ответы [ 15 ]

61 голосов
/ 13 февраля 2013

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

Даниэль Лемир и Леонид Бойцов, Декодирование миллиардов целых чисел в секунду с помощью векторизации, Software: Practice & Experience 45 (1), 2015. http://arxiv.org/abs/1209.2137

Даниэль Лемир, Натан Курц, Леонид Бойцов, SIMD-сжатие и пересечение отсортированных целых чисел, программное обеспечение: практика и опыт (в свет) http://arxiv.org/abs/1401.6399

Они включают обширную экспериментальную оценку.

Вы можете найти полную реализацию всех техник в C ++ 11 онлайн: https://github.com/lemire/FastPFor и https://github.com/lemire/SIMDCompressionAndIntersection

Есть также библиотеки C: https://github.com/lemire/simdcomp и https://github.com/lemire/MaskedVByte

Если вы предпочитаете Java, см. https://github.com/lemire/JavaFastPFOR

32 голосов
/ 12 ноября 2008

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

Вот как формат PNG улучшает сжатие (он использует один из нескольких разностных методов, за которыми следует тот же алгоритм сжатия, который используется gzip).

17 голосов
/ 12 ноября 2008

Ну, я голосую за разумный путь. Все, что вам нужно сохранить, это [int: startnumber] [int / byte / what: число итераций], в этом случае вы превратите свой примерный массив в значение 4xInt. После этого вы можете сжать как хотите:)

14 голосов
/ 04 июля 2009

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

None        1.0
Deflate     0.50
Filtered    0.34
BZip2       0.11
Lzma        0.06
11 голосов
/ 04 июля 2009

Какого размера цифры? В дополнение к другим ответам, вы могли бы рассмотреть кодирование по варианту длины base-128, которое позволяет хранить меньшие числа в отдельных байтах, в то же время допуская большие числа. MSB означает «есть еще один байт» - это описано здесь.

Объедините это с другими приемами, чтобы вы сохранили «размер пропуска», «размер взятия», «размер пропуска», «размер взятия», но отметив, что ни «пропуск», ни «взятие» никогда не будут нулевыми мы вычтем по одному из каждого (что позволяет сэкономить дополнительный байт для нескольких значений)

Итак:

1-100, 110-160

означает «пропустить 1» (предположим, что начинать с нуля, поскольку это облегчает задачу), «взять 100», «пропустить 9», «взять 51»; вычтите 1 из каждого, давая (как десятичные дроби)

0,99,8,50

, который кодируется как (шестнадцатеричное):

00 63 08 32

Если мы хотим пропустить / взять большее число - например, 300; мы вычитаем 1, давая 299 - но это превышает 7 бит; начиная с маленького конца, мы кодируем блоки из 7 бит и MSB, чтобы указать продолжение:

299 = 100101100 = (in blocks of 7): 0000010 0101100

поэтому начнем с малого конца:

1 0101100 (leading one since continuation)
0 0000010 (leading zero as no more)

дает:

AC 02

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

Вы можете попробовать , запустив это через "deflate", но это может не помочь намного больше ...


Если вы не хотите самостоятельно разбираться со всей этой грязной кодировкой ... если вы можете создать целочисленный массив значений (0,99,8,60) - вы можете использовать буферы протокола с упакованным повторяющимся uint32 / uint64 - и он сделает всю работу за вас; -p

Я не "делаю" Java, но вот полная реализация C # (заимствуя некоторые биты кодирования из моего protobuf-net проекта):

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
static class Program
{
    static void Main()
    {
        var data = new List<int>();
        data.AddRange(Enumerable.Range(1, 100));
        data.AddRange(Enumerable.Range(110, 51));
        int[] arr = data.ToArray(), arr2;

        using (MemoryStream ms = new MemoryStream())
        {
            Encode(ms, arr);
            ShowRaw(ms.GetBuffer(), (int)ms.Length);
            ms.Position = 0; // rewind to read it...
            arr2 = Decode(ms);
        }
    }
    static void ShowRaw(byte[] buffer, int len)
    {
        for (int i = 0; i < len; i++)
        {
            Console.Write(buffer[i].ToString("X2"));
        }
        Console.WriteLine();
    }
    static int[] Decode(Stream stream)
    {
        var list = new List<int>();
        uint skip, take;
        int last = 0;
        while (TryDecodeUInt32(stream, out skip)
            && TryDecodeUInt32(stream, out take))
        {
            last += (int)skip+1;
            for(uint i = 0 ; i <= take ; i++) {
                list.Add(last++);
            }
        }
        return list.ToArray();
    }
    static int Encode(Stream stream, int[] data)
    {
        if (data.Length == 0) return 0;
        byte[] buffer = new byte[10];
        int last = -1, len = 0;
        for (int i = 0; i < data.Length; )
        {
            int gap = data[i] - 2 - last, size = 0;
            while (++i < data.Length && data[i] == data[i - 1] + 1) size++;
            last = data[i - 1];
            len += EncodeUInt32((uint)gap, buffer, stream)
                + EncodeUInt32((uint)size, buffer, stream);
        }
        return len;
    }
    public static int EncodeUInt32(uint value, byte[] buffer, Stream stream)
    {
        int count = 0, index = 0;
        do
        {
            buffer[index++] = (byte)((value & 0x7F) | 0x80);
            value >>= 7;
            count++;
        } while (value != 0);
        buffer[index - 1] &= 0x7F;
        stream.Write(buffer, 0, count);
        return count;
    }
    public static bool TryDecodeUInt32(Stream source, out uint value)
    {
        int b = source.ReadByte();
        if (b < 0)
        {
            value = 0;
            return false;
        }

        if ((b & 0x80) == 0)
        {
            // single-byte
            value = (uint)b;
            return true;
        }

        int shift = 7;

        value = (uint)(b & 0x7F);
        bool keepGoing;
        int i = 0;
        do
        {
            b = source.ReadByte();
            if (b < 0) throw new EndOfStreamException();
            i++;
            keepGoing = (b & 0x80) != 0;
            value |= ((uint)(b & 0x7F)) << shift;
            shift += 7;
        } while (keepGoing && i < 4);
        if (keepGoing && i == 4)
        {
            throw new OverflowException();
        }
        return true;
    }
}
3 голосов
/ 04 июля 2009

В дополнение к другим решениям:

Вы можете найти «плотные» области и использовать растровое изображение для их хранения.

Так, например:

Если у вас есть 1000 чисел в 400 диапазонах между 1000-3000, вы можете использовать один бит для обозначения существования числа и два целых числа для обозначения диапазона. Общий объем памяти для этого диапазона составляет 2000 бит + 2 дюйма, поэтому вы можете хранить эту информацию в 254 байта, что довольно здорово, поскольку даже короткие целые числа занимают два байта каждый, поэтому в этом примере вы получаете 7-кратную экономию.

Чем плотнее области, тем лучше будет работать этот алгоритм, но в какой-то момент простое сохранение начала и конца будет дешевле.

3 голосов
/ 12 ноября 2008

сжать строку "1-100, 110-160" или сохранить строку в некотором двоичном представлении и проанализировать ее для восстановления массива

2 голосов
/ 12 ноября 2008

Я бы объединил ответы, данные CesarB и Фернандо Мигелесом.

Сначала сохраните различия между каждым значением и предыдущим. Как указал CesarB, это даст вам последовательность, в основном, из них.

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

1 голос
/ 09 ноября 2017

Я не мог добиться, чтобы моя компрессия была намного лучше, чем около .11 для этого. Я сгенерировал свои тестовые данные через интерпретатор Python, и это список целых чисел с разделителями новой строки от 1-100 и 110-160. Я использую реальную программу как сжатое представление данных. Мой сжатый файл выглядит следующим образом:

main=mapM_ print [x|x<-[1..160],x`notElem`[101..109]]

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

$ runhaskell generator.hs >> data

Размер файла g.hs составляет 54 байта, а сгенерированные питоном данные - 496 байтов. Это дает 0.10887096774193548 в качестве степени сжатия. Я думаю, что со временем можно сжать программу или сжать сжатый файл (то есть файл haskell).

Еще один подход - сохранить 4 байта данных. Мин и макс каждой последовательности, затем передайте их производящей функции. Хотя загрузка файлов добавит больше символов в декомпрессор, добавляя больше сложности и больше байтов декомпрессору. Опять же, я представил эту очень специфическую последовательность через программу, и она не обобщает, это сжатие, специфичное для данных. Кроме того, добавление универсальности делает декомпрессор больше.

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

1 голос
/ 08 июля 2016

TurboPFor: быстрое целочисленное сжатие

  • для C / C ++, включая Java Critical Natives / JNI Interface
  • SIMD ускоренное целочисленное сжатие
  • Scalar + Integrated (SIMD) дифференциальное / зигзагообразное кодирование / декодирование для отсортированных / несортированных целочисленных списков
  • Полный список 8/16/32/64 битных интергерских списков
  • Прямой доступ
  • Тестовое приложение
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...