整数序列的最佳压缩算法

时间:2008-11-12 08:19:07

标签: algorithm compression

我有一个大数组,其中的整数范围大多是连续的,例如1-100,110-160等。所有整数都是正数。 压缩它的最佳算法是什么?

我尝试了deflate算法,但这只给我50%的压缩。 请注意,该算法不能有损。

所有数字都是独一无二的并逐渐增加。

另外,如果你能指出这种算法的java实现,那就太好了。

15 个答案:

答案 0 :(得分:61)

我们写了最近的研究论文,调查了这个问题的最佳方案。请参阅:

Daniel Lemire和Leonid Boytsov,通过矢量化解码每秒数十亿的整数,软件:实践&经验45(1),2015。 http://arxiv.org/abs/1209.2137

Daniel Lemire,Nathan Kurz,Leonid Boytsov,SIMD Compression和排序整数的交集,软件:实践和经验(出现)http://arxiv.org/abs/1401.6399

它们包括广泛的实验评估。

您可以在线找到C ++ 11中所有技术的完整实现: https://github.com/lemire/FastPForhttps://github.com/lemire/SIMDCompressionAndIntersection

还有C库:https://github.com/lemire/simdcomphttps://github.com/lemire/MaskedVByte

如果您更喜欢Java,请参阅https://github.com/lemire/JavaFastPFOR

答案 1 :(得分:32)

首先,通过获取每个值与前一个值之间的差值来预处理您的值列表(对于第一个值,假设前一个值为零)。在你的情况下,这应该主要是一系列的,大多数压缩算法可以更容易地压缩它们。

这就是PNG格式如何改进其压缩(它采用了几种不同的方法之一,后跟gzip使用的相同压缩算法)。

答案 2 :(得分:17)

好吧,我投票支持更聪明的方式。所有你必须存储的是[int:startnumber] [int / byte / whatever:迭代次数]在这种情况下,你将把你的示例数组转换为4xInt值。之后,您可以根据需要进行压缩:)

答案 3 :(得分:14)

虽然您可以设计特定于您的数据流的自定义算法,但使用现成的编码算法可能更容易。我运行了几个tests of compression algorithms available in Java并找到了一百万个连续整数序列的以下压缩率:

None        1.0
Deflate     0.50
Filtered    0.34
BZip2       0.11
Lzma        0.06

答案 4 :(得分:11)

数字的大小是多少?除了其他答案之外,您还可以考虑base-128变长编码,它允许您在单个字节中存储较小的数字,同时仍然允许更大的数字。 MSB表示“还有另一个字节” - 这里是described

将此与其他技术相结合,以便存储“跳过大小”,“占用大小”,“跳过大小”,“占用大小” - 但注意“跳过”或“取出”都不会为零,所以我们将从每个中减去一个(这样可以为少量值保存一个额外的字节)

所以:

1-100, 110-160

是“跳过1”(假设从零开始,因为它使事情变得更容易),“取100”,“跳过9”,“取51”;从每个中减去1,给出(作为小数)

0,99,8,50

编码为(hex):

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

因此我们可以轻松编码大数字,但是较小的数字(对于skip / take来说是典型的声音)占用的空间更少。

你可以尝试通过“deflate”运行它,但它可能没有多大帮助......


如果你不想处理所有那些混乱的编码cruff你自己...如果你可以创建值的整数数组(0,99,8,60) - 你可以使用protocol buffers with a packed repeated 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;
    }
}

答案 5 :(得分:3)

压缩字符串“1-100,110-160”或将字符串存储在某个二进制表示中并解析它以恢复数组

答案 6 :(得分:3)

除了其他解决方案:

您可以找到“密集”区域并使用位图来存储它们。

例如:

如果在1000-3000之间的400范围内有1000个数字,则可以使用单个位来表示存在数字和两个整数来表示范围。此范围的总存储量为2000位+ 2个整数,因此您可以将该信息存储为254字节,这非常棒,因为即使短整数也会占用两个字节,因此对于此示例,您可以节省7倍。

这个区域越密集,这个算法就越好,但在某些时候只是存储开始和结束会更便宜。

答案 7 :(得分:2)

我结合了CesarB和FernandoMiguélez给出的答案。

首先,存储每个值与前一个值之间的差异。正如CesarB指出的那样,这将给你一系列主要是一些。

然后,对此序列使用运行长度编码压缩算法。由于大量的重复值,它会非常好地压缩。

答案 8 :(得分:1)

我建议您查看Huffman Coding Arithmetic Coding的特例。在这两种情况下,您都要分析起始顺序以确定不同值的相对频率。更频繁出现的值使用比不常出现的值更少的位进行编码。

答案 9 :(得分:1)

您应该使用的基本思想是,对于每个连续整数范围(我将称之为这些范围),存储起始编号和范围的大小。例如,如果您有一个1000个整数的列表,但只有10个单独的范围,则可以存储仅20个整数(每个范围1个起始编号和1个大小)来表示此数据,其压缩率为98 %。幸运的是,您可以进行一些更优化,这将有助于范围数量更大的情况。

  1. 存储相对于上一个起始编号的起始编号的偏移量,而不是起始编号本身。这里的优点是您存储的数字通常需要较少的位(这可能在以后的优化建议中派上用场)。此外,如果您只存储了起始编号,这些编号都是唯一的,而存储偏移则有可能使数字接近甚至重复,这可能允许在之后应用另一种方法进一步压缩。

  2. 对两种类型的整数使用可能的最小位数。您可以迭代数字以获得起始整数的最大偏移量以及最大范围的大小。然后,您可以使用最有效地存储这些整数的数据类型,并简单地指定压缩数据开头的数据类型或位数。例如,如果起始整数的最大偏移量仅为12,000,最大范围为9,000长,则可以对所有这些使用2字节无符号整数。然后,您可以在压缩数据的开头填充对2,2,以显示两个整数使用2个字节。当然,您可以使用一点位操作将此信息放入单个字节中。如果您习惯于进行大量的大量操作,则可以将每个数字存储为最小可能的位数,而不是符合1,2,4或8字节的表示。

  3. 通过这两个优化,我们来看几个例子(每个都是4,000个字节):

    1. 1,000个整数,最大偏移量为500,10个范围
    2. 1,000个整数,最大偏移量为100,50个范围
    3. 1,000个整数,最大偏移量为50,100个范围
    4. 没有优化

      1. 20个整数,每个4个字节= 80个字节。 COMPRESSION = 98%
      2. 100个整数,每个4个字节= 400个字节。 COMPRESSION = 90%
      3. 200个整数,每个4个字节= 800个字节。压缩= 80%
      4. 优化

        1. 1个字节的标题+ 20个数字,每个1个字节= 21个字节。压缩= 99.475%
        2. 1个字节的标题+ 100个数字,每个1个字节= 101个字节。 COMPRESSION = 97.475%
        3. 1个字节的标题+ 200个数字,每个1个字节= 201个字节。压缩= 94.975%

答案 10 :(得分:1)

您的情况与搜索引擎中索引的压缩非常相似。使用的流行压缩算法是PForDelta算法和Simple16算法。您可以使用kamikaze库来满足您的压缩需求。

答案 11 :(得分:1)

我知道这是一个旧的消息线程,但我包括我在这里找到的SKIP / TAKE想法的个人PHP测试。我叫我的STEP(+)/ SPAN( - )。也许有人会发现它有用。

注意:我实现了允许重复整数和负整数的功能,即使原始问题涉及正整数,非重复整数。如果你想尝试削减一两个字节,请随意调整它。

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>';

输出:

--------------------------------------------------------------------------------
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
--------------------------------------------------------------------------------

答案 12 :(得分:1)

TurboPFor: Fastest Integer Compression

  • for C / C ++,包括Java Critical Natives / JNI Interface
  • SIMD加速整数压缩
  • 用于排序/未排序整数列表的标量+集成(SIMD)差分/ Zigzag编码/解码
  • 全范围8/16/32/64位整数列表
  • 直接访问
  • 基准应用

答案 13 :(得分:1)

我无法让我的压缩比.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字节,python生成的数据为496字节。这给出了0.10887096774193548作为压缩比。我认为有更多的时间可以缩小程序,或者你可以压缩压缩文件(即haskell文件)。

另一种方法可能是保存4个字节的数据。每个序列的最小值和最大值,然后将它们赋予生成函数。尽管如此,加载文件会为解压缩程序添加更多字符,从而为解压缩程序增加更多复杂性和更多字节。同样,我通过程序表示这个非常具体的序列,并没有概括,它是一种特定于数据的压缩。此外,增加通用性使分解器更大。

另一个问题是必须有Haskell解释器来运行它。当我编译程序时,它使它变得更大。我真的不知道为什么。 python存在同样的问题,所以最好的方法是给出范围,这样某个程序就可以解压缩文件。

答案 14 :(得分:0)

如果你有一系列重复值,RLE是最容易实现的,可以给你一个好的结果。尽管如此,其他更先进的算法考虑到了诸如LZW(现在没有专利)的委托,通常可以实现更好的压缩。

您可以查看这些和其他无损算法here