少ないメモリフットプリントでセット実装を探す


9

セットデータタイプの実装を探しています。つまり、

  • サイズuのユニバースU = \ {0、1、2、3、\ dots、u – 1 \}からの動的サブセットS(サイズn)を維持します。U={0,1,2,3,,u1}u
  • 操作insert(x)(要素xSに追加S)およびfind(x)(要素xSのメンバーかどうかをチェックS

他の操作は気にしません。オリエンテーションについては、私が使用しているアプリケーションでu1010ます。

時間O(1)で両方の操作を提供する実装を知っているので、主にデータ構造のサイズを心配します。何十億ものエントリを期待していますが、できるだけスワッピングを避けたいです。

必要に応じて、ランタイムを犠牲にしてもかまいません。O(\ log n)の償却実行時間O(logn)は、私が認めることができるものです。予想されるランタイムまたは\ omega(\ log n)のランタイムω(logn)は許可されません。

私の考えの1つは、Sを範囲の和集合として表すことができれば[xmin, xmax]、パフォーマンスがいくらか低下する代わりに、ストレージサイズを節約できるということです。また、など、他のいくつかのデータパターンも可能[0, 2, 4, 6]です。

そのようなことを行うことができるデータ構造を私に教えてもらえますか?



要素の数はどのようにして画像に入りますか?つまり、要素が挿入され、すでにがある場合はどうなりますか?nnn
フォンブランド2014年

@vonbrand- nは、セットSのサイズです。これは、ごとinsertに増加する可能性があります。または、要素xがすでにセットにある場合は、そのままにすることができます。
HEKTO 2014年

3
誤検知の可能性はわずかですか?その場合、ブルームフィルターが理想的です。en.wikipedia.org
Joe

1
@AlekseyYakovlev、ブルームフィルターの偽陽性率は、ユニバースサイズ(ハッシュ関数の数、データ構造体サイズ、およびアイテムのみ)とは関係ありませんが、実際に近いと(小さな定数場合はと言います)、スペースの総ビット数がであると私が考える単純なビットベクトルよりも上手くやるのは難しいでしょう。M nはN U U = N C C C Nkmnnuu=ncccn
Joe

回答:


8

ジョーの答えは非常に優れており、重要なキーワードをすべて提供します。

簡潔なデータ構造の研究はまだ初期段階であり、結果の多くは大部分が理論的なものであることを認識しておく必要があります。提案されたデータ構造の多くは実装が非常に複雑ですが、ほとんどの複雑さは、ユニバースサイズと格納されている要素の数の両方で漸近的な複雑さを維持する必要があるという事実によるものです。これらのいずれかが比較的一定であれば、複雑さの多くはなくなります。

コレクションが半静的である場合(つまり、挿入がまれであるか、少なくともボリュームが少ない場合)、更新と組み合わせて、実装が簡単な静的データ構造(Sadakaneのsdarrayが最適です)を検討する価値があります。キャッシュ。基本的に、更新を従来のデータ構造(Bツリー、トライ、ハッシュテーブルなど)に記録し、定期的に「メイン」のデータ構造を一括更新します。転置インデックスは検索に多くの利点がありますが、インプレースで更新することが難しいため、これは情報検索で非常に人気のある手法です。このような場合は、コメントでお知らせください。この回答を修正して、いくつかの指針を示します。

挿入の頻度が高い場合は、簡潔なハッシュをお勧めします。基本的な考え方はここで説明するのに十分単純なので、説明します。

したがって、基本的な情報理論上の結果は、アイテムのユニバースから要素を格納し、他の情報がない場合(たとえば、要素間に相関関係がない場合)、それを格納するビット。(特に指定のない限り、すべての対数は底が2です。)これだけ多くのビットが必要です。それを回避する方法はありません。のunulog(un)+O(1)

今いくつかの用語:

  • データを格納でき、ビットのスペースでの操作をサポートできるデータ構造がある場合、これを暗黙的なデータ構造と呼びます。log(un)+O(1)
  • あなたがにデータを格納し、あなたの業務を支援することができるデータ構造を持っている場合はビットのスペースを。これをコンパクトデータ構造と呼びます。実際には、これは相対的なオーバーヘッド(理論上の最小値に対する)が一定の範囲内にあることを意味します。5%のオーバーヘッド、10%のオーバーヘッド、または10倍のオーバーヘッドの可能性があります。log(un)+O(log(un))=(1+O(1))log(un)
  • あなたがあなたの業務をデータを格納し、サポートすることができ、データ構造がある場合はビットのスペースを。これを簡潔なデータ構造と呼びます。log(un)+o(log(un))=(1+o(1))log(un)

簡潔とコンパクトの違いは、リトルオーとビッグオーの違いです。絶対値のものをしばらく無視しています...

  • g(n)=O(f(n))は、すべての、ような定数と数値が存在することを意味します。cn0n>n0g(n)<cf(n)
  • g(n)=o(f(n))は、すべての定数に対して、すべてのに対してようなが存在することを意味します。cn0n>n0g(n)<cf(n)

非公式には、big-ohとlittle-ohはどちらも「定数係数内」ですが、big-ohでは定数が(アルゴリズムデザイナー、CPUメーカー、物理法則などによって)選択されますが、 -ああ、定数を自分で選ぶと、好きなだけ小さくすることができます。言い換えると、簡潔なデータ構造では、問題のサイズが大きくなると、相対的なオーバーヘッドが任意に小さくなります。

もちろん、問題のサイズは、必要な相対的なオーバーヘッドを実現するために巨大化する必要があるかもしれませんが、すべてを持つことはできません。

わかりました、それを私たちのベルトの下で、問題にいくつかの数字を付けましょう。キーがビットの整数(つまり、ユニバースサイズが)であり、これらの整数のを格納するとします。完全に占有され、無駄のない理想化されたハッシュテーブルを魔法のように配置できるため、正確にハッシュスロットが必要だとします。n2n2m2m

ルックアップ操作では、ビットのキーをハッシュし、ビットをマスクしてハッシュスロットを見つけ、テーブルの値がキーと一致したかどうかを確認します。ここまでは順調ですね。nm

このようなハッシュテーブルは、ビットを使用します。これより上手くできますか?n2m

ハッシュ関数が可逆であると仮定します。その後、各ハッシュスロットにキー全体を格納する必要はありません。ハッシュスロットの場所からビットのハッシュ値が得られるので、残りのビットのみを保存した場合は、これらの2つの情報(ハッシュスロットの場所とそこに保存されている値)からキーを再構築できます。したがって、必要なのはビットのストレージだけです。hmnm(nm)2m

場合はに比べて小さく、、スターリングの近似と少しの算術(証明は運動です!)ことを明らかに:2m2n

(nm)2m=log(2n2m)+o(log(2n2m))

したがって、このデータ構造は簡潔です。

ただし、2つの問題があります。

最初の問題は、「良い」可逆ハッシュ関数を構築することです。幸い、これは見た目よりもはるかに簡単です。暗号学者はいつでも可逆関数を作成し、それらを「暗号」と呼びます。たとえば、ハッシュ関数をFeistelネットワークに基づくことができます。これは、非可逆ハッシュ関数から可逆ハッシュ関数を構築する簡単な方法です。

2番目の問題は、誕生日のパラドックスのおかげで、実際のハッシュテーブルは理想的ではないということです。したがって、こぼれることなく完全な占有率に近づける、より洗練されたタイプのハッシュテーブルを使用する必要があります。カッコウハッシュは、理論的には理想に近づき、実際には非常に近いため、これに最適です。

カッコウハッシュには複数のハッシュ関数が必要であり、ハッシュスロットの値には、使用されたハッシュ関数でタグ付けする必要があります。したがって、たとえば4つのハッシュ関数を使用する場合、各ハッシュスロットにさらに2ビットを格納する必要があります。これは、が大きくなってもまだ簡潔であるため、実際には問題ではなく、キー全体を格納するよりも優れています。m

ああ、あなたはファン・エムデ・ボアスの木を見ることもできます。

もっと考える

がどこかにある場合、はおおよそなので、値の間に相関関係がないと仮定すると、基本的には何もできませんビットベクトルよりも優れています。上記のハッシュソリューションは事実上その場合に縮退します(ハッシュスロットごとに1ビットを格納することになります)が、ハッシュ関数を使用するよりも、キーをアドレスとして使用する方が安上がりです。nu2log(un)u

がに非常に近い場合、すべての簡潔なデータ構造の文献では、辞書の意味を逆にするように勧めています。セットに出現しない値を格納します。ただし、今は事実上、削除操作をサポートする必要があり、簡潔な動作を維持するには、さらに要素が「追加」されるときにデータ構造を縮小できる必要があります。ハッシュテーブルの拡張はよく理解されている操作ですが、縮小はそうではありません。nu


こんにちは。回答の2番目の段落についてと同様に、へのすべての呼び出しにinsertfind、同じ引数でへの呼び出しが伴うことを期待しています。したがって、findリターンが返された場合はtrue、スキップしinsertます。だから、の頻度find呼び出しはもっとしての周波数であるinsertときにも、通話n近くになるとu、その後insertの呼び出しは非常にまれになります。
HEKTO 2014

しかし、あなたはが最終的にに近づくことを期待していますか?un
仮名

現実の世界では、nはuに達するまで成長しますが、それが起こるかどうかは予測できません。データ構造はすべての場合にうまく機能するはずですn <= u
HEKTO

正しい。次に、(上記の意味で)簡潔で、範囲全体でこれを実現する単一のデータ構造については知らないと言っても差し支えありません。ときにスパースデータ構造が必要になると思いますが周りにあるときに密なデータ構造(ビットベクトルなど)に切り替えてから、反転したスパースデータ構造を使用します。が近いときの感覚。nun<unu2nu
仮名

5

動的なメンバーシップの問題には、簡潔なデータ構造が必要なようです。

簡潔なデータ構造は、スペース要件が情報理論の下限に「近い」ものですが、圧縮されたデータ構造とは異なり、効率的なクエリが可能であることを思い出してください。

メンバーシップの問題は、あなたの質問に記述するまさに次のとおりです。

サイズのユニバースからの(サイズ)サブセット維持します。SnU={0,1,2,3,,u1}u

  • find(x)(要素xがメンバーかどうかをチェックします)。S
  • insert(x)(要素xを追加)S
  • delete(x)xから要素を削除)S

find操作のみがサポートされている場合、これは静的メンバーシップの問題です。insertまたはのいずれかdeleteがサポートされているが両方はサポートされていない場合、セミダイナミックと呼ばれ、3つの操作すべてがサポートされている場合は、ダイナミックメンバーシップ問題と呼ばれます。

厳密に言えば、半動的メンバーシップ問題のデータ構造のみを要求したと思いますが、この制約を利用して他の要件も満たすデータ構造は知りません。ただし、次の参照があります。

一定時間およびほぼ最小の空間におけるメンバーシップの定理5.1 では、ブロドニクとムンロは次の結果を与えます。

一定の時間での検索と一定の予想償却時間での挿入と削除をサポートするビットを必要とするデータ構造が存在します。O(B)

ここで、は、必要な情報理論上の最小ビット数です。B=log(un)

基本的な考えは、慎重に選択されたサイズの範囲に宇宙を再帰的に分割するということです。そのため、これはテクニックがあなたが考えている線に沿っているかもしれません。

ただし、実際に実装できるものを探している場合、これが最善の策になるかどうかはわかりません。私は紙をざっと目を通しただけであり、詳細を説明することはこの回答の範囲をはるかに超えています。彼らは、と相対的なサイズに応じて異なる戦略を使用して、ソリューションをパラメーター化します。また、データ構造の動的バージョンは、紙にスケッチされているだけです。Nun


1
Brodnik&Munroの論文の要約では、挿入について何も述べられていません。しかし、彼らの結果は私たちが期待できるものですよね?の場合n = u/2、必要なスペースは最大です。
HEKTO 2014年

@AlekseyYakovlev彼らは、動的なケースを抽象的に実際に言及していませんが、動的なケースを扱う定理は私の回答に引用されています(セクション5から)。
Joe
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.