1〜100、110〜160など、ほとんど連続した整数の範囲を持つ大きな配列があります。すべての整数は正です。これを圧縮するのに最適なアルゴリズムは何ですか?
deflateアルゴリズムを試しましたが、50%しか圧縮できません。アルゴリズムは不可逆的ではないことに注意してください。
すべての数値は一意であり、徐々に増加しています。
また、このようなアルゴリズムのJava実装が素晴らしいと思います。
この問題に対する最善のスキームを調査する最近の研究論文を執筆しました。見てください:
ダニエル・レミアとレオニード・ボイツォフ、ベクトル化による毎秒数十億の整数のデコード、ソフトウェア:実践と経験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(to表示) 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 をご覧ください。
最初に、各値と前の値との差を取ることによって値のリストを前処理します(最初の値については、前の値がゼロであると仮定します)。これは、ほとんどの場合、ほとんどの圧縮アルゴリズムではるかに簡単に圧縮できる一連のシーケンスを提供します。
これは、PNG形式が圧縮を改善する方法です(gzipで使用されるのと同じ圧縮アルゴリズムが後に続くいくつかの異なる方法の1つを行います)。
まあ、私はよりスマートな方法に投票しています。この場合、保存しなければならないのは[int:startnumber] [int/byte/whatever:number of iterations]です。この場合、サンプルの配列を4xInt値に変換します。その後、必要に応じて圧縮できます:)
データストリームに固有のカスタムアルゴリズムを設計することもできますが、市販のエンコードアルゴリズムを使用する方がおそらく簡単です。私はいくつかの Javaで利用可能な圧縮アルゴリズムのテスト を実行し、100万の連続した整数のシーケンスに対して次の圧縮率を見つけました。
None 1.0
Deflate 0.50
Filtered 0.34
BZip2 0.11
Lzma 0.06
数字の大きさは?他の回答に加えて、ベース128の可変長エンコードを検討することもできます。これにより、より小さな数字を1バイトで格納しながら、より大きな数字を使用できます。 MSBは「別のバイトがある」ことを意味します-これは 記述 ここです。
これを他の手法と組み合わせて、「スキップサイズ」、「テイクサイズ」、「スキップサイズ」、「テイクサイズ」を保存します。ただし、「スキップ」も「テイク」もゼロではないことに注意してください。それぞれから1を減算します(これにより、いくつかの値に対して余分なバイトを保存できます)
そう:
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
そのため、大きな数値を簡単にエンコードできますが、小さな数値(スキップ/テイクでよく聞こえる)は、スペースを取りません。
あなたはこれを「deflate」で実行することができますtryが、それ以上の助けにはならないかもしれません...
厄介なエンコード処理をすべて自分で処理したくない場合...値の整数配列(0,99,8,60)を作成できる場合- protocol buffers with aパックされた繰り返し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;
}
}
文字列「1-100、110-160」を圧縮するか、文字列をバイナリ表現で保存し、解析して配列を復元します
他のソリューションに加えて:
「密な」領域を見つけ、ビットマップを使用してそれらを保存できます。
たとえば、次のとおりです。
1000〜3000の400の範囲に1000の数値がある場合、単一のビットを使用して数値の存在を示し、2つの整数を使用して範囲を示すことができます。この範囲の合計ストレージは2000ビット+ 2 intであるため、その情報を254バイトで格納できます。これは、短い整数でもそれぞれ2バイトを占めるため、この例では7倍の節約になります。
領域が密であればあるほど、このアルゴリズムの性能は向上しますが、ある時点で開始と終了を保存するだけで安くなります。
私は、CesarBとFernandoMiguélezの回答を組み合わせます。
まず、各値と前の値との差を保存します。 CesarBが指摘したように、これはほとんどのシーケンスを提供します。
次に、このシーケンスでRun Length Encoding圧縮アルゴリズムを使用します。多数の値が繰り返されるため、非常にうまく圧縮されます。
私はこれが古いメッセージスレッドであることを知っていますが、個人的なPHPここで見つけたSKIP/TAKEアイデアのテストを含めています。STEP(+)/ SPAN(-)と呼んでいます。おそらく誰かが役に立つと思うかもしれません。
注:元の質問に正の重複していない整数が含まれていたとしても、負の整数だけでなく整数の重複も許可する機能を実装しました。 1つか2バイト削りたい場合は、自由に調整してください。
コード:
// $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
--------------------------------------------------------------------------------
あなたのケースは、検索エンジンでのインデックスの圧縮に非常に似ています。使用される一般的な圧縮アルゴリズムは、PForDeltaアルゴリズムとSimple16アルゴリズムです。圧縮のニーズに合わせてkamikazeライブラリを使用できます。
この場合、圧縮率を約.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ファイルなど)を圧縮できます。
もう1つの方法は、4バイトのデータを保存することです。次に、各シーケンスの最小値と最大値を生成関数に渡します。ただし、ファイルをロードすると、圧縮解除プログラムにより多くの文字が追加され、圧縮解除プログラムにより多くの複雑さとバイトが追加されます。繰り返しますが、私はこの非常に特定のシーケンスをプログラムで表現しましたが、一般化はしていません。データに固有の圧縮です。さらに、一般性を追加すると、解凍プログラムが大きくなります。
もう1つの懸念は、これを実行するにはHaskellインタープリターが必要だということです。プログラムをコンパイルしたとき、それははるかに大きくなりました。理由は本当にわかりません。 pythonにも同じ問題があります。そのため、いくつかのプログラムがファイルを解凍できるように、範囲を指定するのが最善の方法かもしれません。
おそらく使用すべき基本的な考え方は、連続する整数の各範囲(これらの範囲と呼びます)に対して、開始番号と範囲のサイズを格納することです。たとえば、1000個の整数のリストがあり、10個の個別の範囲しかない場合、圧縮率98になるこのデータを表すために、わずか20個の整数(各範囲に1つの開始番号と1つのサイズ)を格納できます%。幸いなことに、範囲の数が多い場合に役立つ最適化がいくつかあります。
開始番号自体ではなく、前の開始番号に対する開始番号のオフセットを保存する。ここでの利点は、保存する数値が一般に必要なビット数が少なくなることです(これは後の最適化の提案で役立ちます)。さらに、開始番号のみを保存した場合、これらの番号はすべて一意になりますが、オフセットを保存すると、番号が近くなるか繰り返される可能性があり、その後に別の方法でさらに圧縮することができます。
両方のタイプの整数に可能な最小ビット数を使用します。数値を反復処理して、開始整数の最大オフセットと最大範囲のサイズを取得できます。次に、これらの整数を最も効率的に格納するデータ型を使用し、圧縮データの開始時にデータ型またはビット数を指定するだけです。たとえば、開始整数の最大オフセットが12,000のみで、最大範囲が9,000の場合、これらすべてに2バイトの符号なし整数を使用できます。次に、圧縮データの開始時にペア2、2を詰め込み、両方の整数に2バイトが使用されていることを示します。もちろん、少しのビット操作を使用して、この情報を1バイトに収めることができます。大量のビット操作を行うことに慣れている場合は、1、2、4、または8バイト表現に準拠するのではなく、可能な限り最小のビット量として各数値を格納できます。
これらの2つの最適化で、いくつかの例を見てみましょう(それぞれ4,000バイト):
最適化なし
最適化あり
ハフマンコーディング 、 算術コーディング の特殊なケースを見てみることをお勧めします。どちらの場合も、開始シーケンスを分析して、異なる値の相対頻度を決定します。頻度の高い値は、頻度の低い値よりも少ないビットでエンコードされます。
一連の繰り返し値がある場合、RLEを実装するのが最も簡単で、良い結果が得られます。それにもかかわらず、LZWなどのエントロフィを考慮に入れた他のより高度なアルゴリズムは、現在では特許がなく、通常ははるかに優れた圧縮を実現できます。
これらおよびその他のロスレスアルゴリズム こちら をご覧ください。