web-dev-qa-db-fra.com

Meilleur algorithme de compression pour une séquence d'entiers

J'ai un grand tableau avec une gamme d'entiers qui sont principalement continus, par exemple 1-100, 110-160, etc. Tous les entiers sont positifs. Quel serait le meilleur algorithme pour compresser cela?

J'ai essayé l'algorithme de dégonflage mais cela ne me donne qu'une compression de 50%. Notez que l'algorithme ne peut pas être avec perte.

Tous les nombres sont uniques et augmentent progressivement.

Aussi, si vous pouvez me diriger vers l'implémentation Java d'un tel algorithme, ce serait formidable.

50
pdeva

Nous avons rédigé des articles de recherche récents qui recensent les meilleurs schémas pour résoudre ce problème. S'il te plait regarde:

Daniel Lemire et Leonid Boytsov, Décodage de milliards d'entiers par seconde grâce à la vectorisation, Software: Practice & Experience 45 (1), 2015. http://arxiv.org/abs/1209.2137

Daniel Lemire, Nathan Kurz, Leonid Boytsov, SIMD Compression and the Intersection of Sorted Integers, Software: Practice and Experience (à paraître) http://arxiv.org/abs/1401.6399

Ils comprennent une évaluation expérimentale approfondie.

Vous pouvez trouver une implémentation complète de toutes les techniques en C++ 11 en ligne: https://github.com/lemire/FastPFor et https://github.com/lemire/SIMDCompressionAndIntersection

Il existe également des bibliothèques C: https://github.com/lemire/simdcomp et https://github.com/lemire/MaskedVByte

Si vous préférez Java, veuillez consulter https://github.com/lemire/JavaFastPFOR

66
Daniel Lemire

Tout d'abord, prétraitez votre liste de valeurs en prenant la différence entre chaque valeur et la précédente (pour la première valeur, supposez que la précédente était nulle). Dans votre cas, cela devrait donner principalement une séquence d'unités, qui peuvent être compressées beaucoup plus facilement par la plupart des algorithmes de compression.

C'est ainsi que le format PNG améliore sa compression (il fait l'une des nombreuses méthodes de différence suivies du même algorithme de compression utilisé par gzip).

32
CesarB

Eh bien, je vote pour une façon plus intelligente. Tout ce que vous avez à stocker est [int: startnumber] [int/byte/que ce soit: nombre d'itérations] dans ce cas, vous transformerez votre exemple de tableau en valeur 4xInt. Après cela, vous pouvez compresser comme vous le souhaitez :)

17
Tamir

Bien que vous puissiez concevoir un algorithme personnalisé spécifique à votre flux de données, il est probablement plus facile d'utiliser un algorithme de codage standard. J'ai exécuté quelques tests d'algorithmes de compression disponibles en Java et trouvé les taux de compression suivants pour une séquence d'un million d'entiers consécutifs:

None        1.0
Deflate     0.50
Filtered    0.34
BZip2       0.11
Lzma        0.06
14
brianegge

De quelle taille sont les chiffres? En plus des autres réponses, vous pouvez envisager un codage de longueur variant en base 128, qui vous permet de stocker des nombres plus petits en octets uniques tout en autorisant des nombres plus importants. Le MSB signifie "il y a un autre octet" - c'est décrit ici.

Combinez cela avec les autres techniques afin que vous stockiez "skip size", "take size", "skip size", "take size" - mais en notant que ni "skip" ni "take" ne seront jamais nuls, donc nous allons soustrayez un de chaque (ce qui vous permet d'enregistrer un octet supplémentaire pour une poignée de valeurs)

Donc:

1-100, 110-160

est "sauter 1" (supposer commencer à zéro car cela facilite les choses), "prendre 100", "sauter 9", "prendre 51"; soustrayez 1 de chacun, en donnant (en décimales)

0,99,8,50

qui code comme (hex):

00 63 08 32

Si nous voulions sauter/prendre un plus grand nombre - 300, par exemple; nous soustrayons 1 en donnant 299 - mais cela va sur 7 bits; en commençant par la petite extrémité, nous encodons des blocs de 7 bits et un MSB pour indiquer la suite:

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

donc en commençant par la petite fin:

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

donnant:

AC 02

Nous pouvons donc encoder facilement de grands nombres, mais les petits nombres (qui sonnent généralement pour sauter/prendre) prennent moins de place.

Vous pouvez essayer d'exécuter ceci via "dégonfler", mais cela pourrait ne pas aider beaucoup plus ...


Si vous ne voulez pas vous débrouiller avec tous ces problèmes d'encodage ... si vous pouvez créer le tableau d'entiers des valeurs (0,99,8,60) - vous pouvez utiliser tampons de protocole avec un emballé répété uint32/uint64 - et il fera tout le travail pour vous ;-p

Je ne "fais" pas Java, mais voici une implémentation C # complète (empruntant certains des bits d'encodage à mon protobuf-net projet):

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;
    }
}
11
Marc Gravell

compresser la chaîne "1-100, 110-160" ou stocker la chaîne dans une représentation binaire et l'analyser pour restaurer le tableau

3
Ray Tayek

En plus des autres solutions:

Vous pouvez trouver des zones "denses" et utiliser une image bitmap pour les stocker.

Ainsi, par exemple:

Si vous avez 1 000 numéros dans 400 plages entre 1 000 et 3 000, vous pouvez utiliser un seul bit pour indiquer l'existence d'un nombre et deux entiers pour désigner la plage. Le stockage total pour cette plage est de 2000 bits + 2 pouces, vous pouvez donc stocker ces informations dans 254 octets, ce qui est assez génial car même les entiers courts prendront deux octets chacun, donc pour cet exemple, vous obtenez 7X d'économies.

Plus les zones sont denses, meilleur sera cet algorithme, mais à un moment donné, le stockage du début et de la fin sera moins cher.

3
Sam Saffron

Je combinerais les réponses données par CesarB et Fernando Miguélez.

Tout d'abord, stockez les différences entre chaque valeur et la précédente. Comme CesarB l'a souligné, cela vous donnera une séquence de la plupart d'entre eux.

Ensuite, utilisez un algorithme de compression Run Length Encoding sur cette séquence. Il se compressera très bien en raison du grand nombre de valeurs répétées.

2
Michael Dorfman

Je sais que c'est un vieux fil de message, mais j'inclus mon PHP test personnel de l'idée SKIP/TAKE que j'ai trouvé ici. J'appelle le mien STEP (+)/SPAN (-) Peut-être que quelqu'un pourrait trouver cela utile.

REMARQUE: j'ai implémenté la possibilité d'autoriser les entiers en double ainsi que les entiers négatifs même si la question d'origine impliquait des entiers positifs et non dupliqués. N'hésitez pas à le modifier si vous voulez essayer de vous raser un octet ou deux.

CODE:

  // $integers_array can contain any integers; no floating point, please. Duplicates okay.
  $integers_array = [118, 68, -9, 82, 67, -36, 15, 27, 26, 138, 45, 121, 72, 63, 73, -35,
                    68, 46, 37, -28, -12, 42, 101, 21, 35, 100, 44, 13, 125, 142, 36, 88,
                    113, -40, 40, -25, 116, -21, 123, -10, 43, 130, 7, 39, 69, 102, 24,
                    75, 64, 127, 109, 38, 41, -23, 21, -21, 101, 138, 51, 4, 93, -29, -13];

  // Order from least to greatest... This routine does NOT save original order of integers.
  sort($integers_array, SORT_NUMERIC); 

  // Start with the least value... NOTE: This removes the first value from the array.
  $start = $current = array_shift($integers_array);    

  // This caps the end of the array, so we can easily get the last step or span value.
  array_Push($integers_array, $start - 1);

  // Create the compressed array...
  $compressed_array = [$start];
  foreach ($integers_array as $next_value) {
    // Range of $current to $next_value is our "skip" range. I call it a "step" instead.
    $step = $next_value - $current;
    if ($step == 1) {
        // Took a single step, wait to find the end of a series of seqential numbers.
        $current = $next_value;
    } else {
        // Range of $start to $current is our "take" range. I call it a "span" instead.
        $span = $current - $start;
        // If $span is positive, use "negative" to identify these as sequential numbers. 
        if ($span > 0) array_Push($compressed_array, -$span);
        // If $step is positive, move forward. If $step is zero, the number is duplicate.
        if ($step >= 0) array_Push($compressed_array, $step);
        // In any case, we are resetting our start of potentialy sequential numbers.
        $start = $current = $next_value;
    }
  }

  // OPTIONAL: The following code attempts to compress things further in a variety of ways.

  // A quick check to see what pack size we can use.
  $largest_integer = max(max($compressed_array),-min($compressed_array));
  if ($largest_integer < pow(2,7)) $pack_size = 'c';
  elseif ($largest_integer < pow(2,15)) $pack_size = 's';
  elseif ($largest_integer < pow(2,31)) $pack_size = 'l';
  elseif ($largest_integer < pow(2,63)) $pack_size = 'q';
  else die('Too freaking large, try something else!');

  // NOTE: I did not implement the MSB feature mentioned by Marc Gravell.
  // I'm just pre-pending the $pack_size as the first byte, so I know how to unpack it.
  $packed_string = $pack_size;

  // Save compressed array to compressed string and binary packed string.
  $compressed_string = '';
  foreach ($compressed_array as $value) {
      $compressed_string .= ($value < 0) ? $value : '+'.$value;
      $packed_string .= pack($pack_size, $value);
  }

  // We can possibly compress it more with gzip if there are lots of similar values.      
  $gz_string = gzcompress($packed_string);

  // These were all just size tests I left in for you.
  $base64_string = base64_encode($packed_string);
  $gz64_string = base64_encode($gz_string);
  $compressed_string = trim($compressed_string,'+');  // Don't need leading '+'.
  echo "<hr>\nOriginal Array has "
    .count($integers_array)
    .' elements: {not showing, since I modified the original array directly}';
  echo "<br>\nCompressed Array has "
    .count($compressed_array).' elements: '
    .implode(', ',$compressed_array);
  echo "<br>\nCompressed String has "
    .strlen($compressed_string).' characters: '
    .$compressed_string;
  echo "<br>\nPacked String has "
    .strlen($packed_string).' (some probably not printable) characters: '
    .$packed_string;
  echo "<br>\nBase64 String has "
    .strlen($base64_string).' (all printable) characters: '
    .$base64_string;
  echo "<br>\nGZipped String has "
    .strlen($gz_string).' (some probably not printable) characters: '
    .$gz_string;
  echo "<br>\nBase64 of GZipped String has "
    .strlen($gz64_string).' (all printable) characters: '
    .$gz64_string;

  // NOTICE: The following code reverses the process, starting form the $compressed_array.

  // The first value is always the starting value.
  $current_value = array_shift($compressed_array);
  $uncompressed_array = [$current_value];
  foreach ($compressed_array as $val) {
    if ($val < -1) {
      // For ranges that span more than two values, we have to fill in the values.
      $range = range($current_value + 1, $current_value - $val - 1);
      $uncompressed_array = array_merge($uncompressed_array, $range);
    }
    // Add the step value to the $current_value
    $current_value += abs($val); 
    // Add the newly-determined $current_value to our list. If $val==0, it is a repeat!
    array_Push($uncompressed_array, $current_value);      
  }

  // Display the uncompressed array.
  echo "<hr>Reconstituted Array has "
    .count($uncompressed_array).' elements: '
    .implode(', ',$uncompressed_array).
    '<hr>';

PRODUCTION:

--------------------------------------------------------------------------------
Original Array has 63 elements: {not showing, since I modified the original array directly}
Compressed Array has 53 elements: -40, 4, -1, 6, -1, 3, 2, 2, 0, 8, -1, 2, -1, 13, 3, 6, 2, 6, 0, 3, 2, -1, 8, -11, 5, 12, -1, 3, -1, 0, -1, 3, -1, 2, 7, 6, 5, 7, -1, 0, -1, 7, 4, 3, 2, 3, 2, 2, 2, 3, 8, 0, 4
Compressed String has 110 characters: -40+4-1+6-1+3+2+2+0+8-1+2-1+13+3+6+2+6+0+3+2-1+8-11+5+12-1+3-1+0-1+3-1+2+7+6+5+7-1+0-1+7+4+3+2+3+2+2+2+3+8+0+4
Packed String has 54 (some probably not printable) characters: cØÿÿÿÿ ÿõ ÿÿÿÿÿÿ
Base64 String has 72 (all printable) characters: Y9gE/wb/AwICAAj/Av8NAwYCBgADAv8I9QUM/wP/AP8D/wIHBgUH/wD/BwQDAgMCAgIDCAAE
GZipped String has 53 (some probably not printable) characters: xœ Ê» ÑÈί€)YšE¨MŠ“^qçºR¬m&Òõ‹%Ê&TFʉùÀ6ÿÁÁ Æ
Base64 of GZipped String has 72 (all printable) characters: eJwNyrsNACAMA9HIzq+AKVmaRahNipNecee6UgSsBW0m0gj1iyXKJlRGjcqJ+cA2/8HBDcY=
--------------------------------------------------------------------------------
Reconstituted Array has 63 elements: -40, -36, -35, -29, -28, -25, -23, -21, -21, -13, -12, -10, -9, 4, 7, 13, 15, 21, 21, 24, 26, 27, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 51, 63, 64, 67, 68, 68, 69, 72, 73, 75, 82, 88, 93, 100, 101, 101, 102, 109, 113, 116, 118, 121, 123, 125, 127, 130, 138, 138, 142
--------------------------------------------------------------------------------
1
Deen Foxx

Votre cas est très similaire à la compression d'index dans les moteurs de recherche. L'algorithme de compression populaire utilisé est l'algorithme PForDelta et l'algorithme Simple16. Vous pouvez utiliser la bibliothèque kamikaze pour vos besoins de compression.

1
Arun Iyer

Je n'ai pas pu obtenir une compression bien meilleure qu'environ .11 pour cela. J'ai généré mes données de test via l'interpréteur python et c'est une liste délimitée par des sauts de 1 à 100 et 110 à 160. J'utilise le programme réel comme représentation compressée des données. Mon fichier compressé le fichier est le suivant:

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

C'est juste un script Haskell qui génère le fichier que vous pouvez exécuter via:

$ runhaskell generator.hs >> data

La taille du fichier g.hs est de 54 octets, et le python sont de 496 octets. Cela donne 0,10887096774193548 comme taux de compression. Je pense qu'avec plus de temps on pourrait réduire le programme, ou vous pouvez compresser le fichier compressé (c'est-à-dire le fichier haskell).

Une autre approche pourrait consister à enregistrer 4 octets de données. Les min et max de chaque séquence, puis donnez-les à une fonction génératrice. Quoique, le chargement des fichiers ajoutera plus de caractères au décompresseur ajoutant plus de complexité et plus d'octets au décompresseur. Encore une fois, j'ai représenté cette séquence très spécifique via un programme et elle ne se généralise pas, c'est une compression qui est spécifique aux données. De plus, l'ajout de généralité agrandit le décompresseur.

Une autre préoccupation est que l'on doit avoir l'interprète Haskell pour exécuter cela. Quand j'ai compilé le programme, il l'a rendu beaucoup plus grand. Je ne sais pas vraiment pourquoi. Il y a le même problème avec python, donc la meilleure approche est peut-être de donner les plages, afin qu'un programme puisse décompresser le fichier.

1
Antithesis

TurboPFor: Compression entière la plus rapide

  • pour C/C++, y compris Java Critical Natives/JNI Interface
  • Compression entière accélérée SIMD
  • Scalar + Integrated (SIMD) differentiel/encodage/décodage en zigzag pour les listes entières triées/non triées
  • Listes d'interger 8/16/32/64 bits pleine gamme
  • Accès direct
  • Application de référence
1
powturbo

L'idée de base que vous devriez probablement utiliser est, pour chaque plage d'entiers consécutifs (j'appellerai ces plages), de stocker le numéro de départ et la taille de la plage. Par exemple, si vous avez une liste de 1000 entiers, mais qu'il n'y a que 10 plages distinctes, vous pouvez stocker seulement 20 entiers (1 numéro de départ et 1 taille pour chaque plage) pour représenter ces données, ce qui correspondrait à un taux de compression de 98 %. Heureusement, vous pouvez effectuer d'autres optimisations qui vous aideront dans les cas où le nombre de plages est plus grand.

  1. Enregistrer le décalage du numéro de départ par rapport au numéro de départ précédent, plutôt que le numéro de départ lui-même. L'avantage ici est que les nombres que vous stockez nécessitent généralement moins de bits (cela peut être utile dans les suggestions d'optimisation ultérieures). De plus, si vous ne stockiez que les nombres de départ, ces nombres seraient tous uniques, tandis que le stockage du décalage donne une chance que les nombres soient proches ou même répétés, ce qui peut permettre une compression encore plus poussée avec une autre méthode appliquée après.

  2. Utilisez le nombre minimum de bits possible pour les deux types d'entiers. Vous pouvez parcourir les nombres pour obtenir le plus grand décalage d'un entier de départ ainsi que la taille de la plus grande plage. Vous pouvez ensuite utiliser un type de données qui stocke le plus efficacement ces entiers et spécifier simplement le type de données ou le nombre de bits au début des données compressées. Par exemple, si le plus grand décalage d'un entier de départ n'est que de 12 000 et que la plage la plus grande est de 9 000, vous pouvez utiliser un entier non signé de 2 octets pour tous ces éléments. Vous pouvez ensuite entasser la paire 2,2 au début des données compressées pour montrer que 2 octets sont utilisés pour les deux entiers. Bien sûr, vous pouvez intégrer ces informations dans un seul octet en utilisant un peu de manipulation de bits. Si vous êtes à l'aise avec beaucoup de manipulation de bits lourds, vous pouvez stocker chaque nombre comme la quantité minimale possible de bits plutôt que de se conformer aux représentations à 1, 2, 4 ou 8 octets.

Avec ces deux optimisations, regardons quelques exemples (chacun fait 4 000 octets):

  1. 1000 entiers, le plus grand décalage est de 500, 10 plages
  2. 1000 entiers, le plus grand décalage est de 100, 50 plages
  3. 1000 entiers, le plus grand décalage est de 50, 100 plages

SANS OPTIMISATION

  1. 20 entiers, 4 octets chacun = 80 octets. COMPRESSION = 98%
  2. 100 entiers, 4 octets chacun = 400 octets. COMPRESSION = 90%
  3. 200 entiers, 4 octets chacun = 800 octets. COMPRESSION = 80%

AVEC OPTIMISATIONS

  1. En-tête de 1 octet + 20 chiffres, 1 octet chacun = 21 octets. COMPRESSION = 99,475%
  2. En-tête de 1 octet + 100 chiffres, 1 octet chacun = 101 octets. COMPRESSION = 97,475%
  3. En-tête de 1 octet + 200 chiffres, 1 octet chacun = 201 octets. COMPRESSION = 94,975%
1
MahlerFive

Je suggère de jeter un coup d'œil à Huffman Coding , un cas spécial de Arithmetic Coding . Dans les deux cas, vous analysez votre séquence de départ pour déterminer les fréquences relatives de différentes valeurs. Les valeurs les plus fréquentes sont codées avec moins de bits que celles qui se produisent moins fréquemment.

1
Martin

Si vous avez des séries de valeurs répétées, RLE est le plus facile à implémenter et pourrait vous donner un bon résultat. Néanmoins, d'autres algorithmes plus avancés qui prennent en compte l'entrophie comme LZW, qui est maintenant sans brevet, peuvent généralement obtenir une bien meilleure compression.

Vous pouvez jeter un œil à ces algorithmes sans perte ici .

0
Fernando Miguélez