この問題に対する適切な解決策がない可能性があることを知っています。値のセット(整数としましょう)が値のセットに存在するかどうかを判断する高速アルゴリズムを探していますが、値がセットにない場合もアルゴリズムを高速にする必要があります。つまり、値がリストにあるかどうかを判断する現在考えられるほとんどのメソッドは、値が早期に終了しないため、そうでないことを判断するのに時間がかかります。
私は順序付けられていないセットのキーを使用して実験してきましたが、実際の検証はかなり遅いことがわかりました。このデータ型の裏側で何が起こっているのか本当にわかりません。配列を直接検索する方がキーを検索するよりも速いかもしれませんが、配列のサイズはまだ適切にわかっていません。非常に大きくなるだけです。
プロパティと仮定:
既知の最大Nがある場合は、 ビット配列 を使用すると、ルックアップ時間が非常に高速になります。
サイズN/8(切り上げ)の配列を保持し、各ビットは数値に対応します。セット内にある場合は1、そうでない場合は0です。関連するビットを検索して、数値がセットに含まれているかどうかを確認します。
これが遅すぎてメガバイトがある場合(「百万」はそれほど大きく聞こえない)、サイズNのブール配列を保持します。
編集:Nの値がはるかに大きい場合、これはメモリに収まりません。おそらく何らかのハッシュが必要です。ただし、後続の呼び出しが互いに近い可能性が高いという事実を使用することは、それでもいいでしょう。
したがって、アイデアのスケッチ:キーとして256で除算した数を使用し、値として256ビットのビット配列を格納します(このキーに分割するすべての数について)。最後に取得したキーとビット配列を保存します。次の呼び出しが同じキーに分かれる場合、キャッシュビット配列ですぐにそれを検索できます。もちろん、256という数字はかなり調整できます。これが実際に余分なギミックなしのマップを使用するよりも速いかどうかはわかりません。プロファイルを作成してください。
Nの値が大きい場合、データのスパース性に大きく依存します。明らかに最大N 10 ^ 12があり、多くの値がセットに含まれている場合、それはとにかくメモリに収まりません。
バイナリツリー( 基数ツリーaka trie )を生成できます。検索を準備するには、配列内の各数値について、最下位ビットから始めて、0の場合、左側のノードを調べます。 1の場合は、正しいノードを探します。ノードがなくなるまでツリーをたどり、そこから一度に1つのノードでツリーを作成します。
検索するには、各ビットを取り、最下位ビットから始め、ビットが0の場合は左に移動します。ビットが1の場合、右に進みます。ノードが見つかりませんか?あなたの番号はツリーにありません、あなたは終わりました。それ以外の場合は、そのノードの次のビットを続行します。ビットが足りなくなり、ノード上にいる場合は、自分の番号がわかります。
O(nlog n)を使用してバイナリツリーを構築し、O(log n)を使用して数値が存在するかどうかを判断します。従来のハッシュマップよりも高速である必要がありますが、制限が厳しいため、この実装は数値でのみ機能します。
最下位ビットから始める理由は、テーブルのバランスをいくらか保つためです(そうしないと、多くの数値が0から始まる可能性があります)。お役に立てば幸いです。
私もビットフィールドに移動しようとしますが、データがまばらで範囲が広すぎてこの機能を果たせない場合は、 splay tree を使用するとよいでしょう。
同じ要素への繰り返しアクセスを最適化するために、スプレイツリーはアクセスごとに変更されます(そのため、スレッドセーフではないため、除外の理由になる可能性があります)。よく使用される要素が上に向かって泡立ちます。アクセスパターンがしばらくの間同じ領域に実際にとどまる場合、これはあなたのケースで高速なルックアップになる可能性があります。
ただし、これは使用パターンによって異なります。存在しない要素については、ツリーの高さ全体をトラバースする必要があります。
数値は最大で約10 ^ 12になる可能性があり、数値の約20%が使用されると言われました。これは、実際に使用されている2 * 10 ^ 11の数値です。これらの数値を使用して実行できる最善の方法は、約125 GBのビットマップです。RAMを購入する余裕がない場合は、SSDドライブと仮想メモリを使用する必要があります。 「通常の」ファイルアクセスが仮想メモリに依存するよりも速いかどうかを測定する必要があります。明らかに、物理メモリを超えるビットマップはsooooooowになります。それでも、あなたができる最速です。
マップの密度が低い場合は、より速く行うことができます。 0から10 ^ 12までの10億の数値があるとします。したがって、平均して1000の数値の範囲に含まれる数値は1つだけです。 「128kから128k + 127の範囲には少なくとも1つの数値が含まれている」という事実を表すビットマップを持つことができます。これには80億ビットまたは1 GBが必要であり、RAMに適合します。このマップを最初に確認すると、87%で数値がないことがわかります。したがって、使用したい低速アルゴリズムは8つの数値のうちの1つにしか使用されません。
明らかにあなたのアクセスパターンは非常に重要になります。 99%の確率で実際に存在する数値を検索する場合、上記の方法は役に立ちません。
私がこの問題のために思いついた最も効率的で汎用的なソリューションであり、それは特に整数に対して機能します(ただし、補助データ構造を使用して検索を高速化するときに、インデックスによってほとんどすべてのものを参照するため、私の場合でも広く適用できます) 、私は「スパースビットツリー」と呼んでいます。このデータ構造に正式な名前があるかどうかはわかりません。これは、セットのビット(1
)を見つけただけで100万個の要素をスキップできるため、両方のセットのすべての要素(ビット)を検査する必要がなく、線形時間よりも優れた時間でセット演算を実行できます。両方のセットのルートレベルと、両方のセットに単一ビットのテストと共通の100万個の要素があることがすぐにわかります。
その結果、Log(N)/ Log(64)のようなベストケースのパフォーマンスが得られます。この場合、3〜4回の反復で100万個の要素を含む2つの密なセット間のセットの交差を見つけることができます。 1ビットをチェックします。最悪のシナリオはN * Log(N)/ Log(64)で、両方のセット間のセットの交差を決定するために約300万から400万回の反復を実行する必要があるかもしれませんが、それでも線形時間であり、発生しませんデータが非常にぎこちなくまばらで、連続するインデックスがない場合(セット内のすべての整数が偶数の場合など)を除きます。
他の誰かが私よりずっと前に同じ考えを思いついたとしても、私は驚かないでしょう(もし誰かがその名前を知っているなら、私はそれを感謝します)。これは私が作成して古くから使用してきたものです(元のバージョンは16ビットの単語を使用し、64ビットに進化しました)。
これは実装するのに最も簡単なデータ構造ではありませんが、非常に多くのプロジェクトのバックボーンとして役立ちました。ダイアグラムは、関心のある人のために実装するのに十分なアイデアを提供する必要があります。セット演算で整数/インデックスセットとして使用できます。ほとんどのセットとは異なり、 ffs命令 を使用して、未使用の整数/インデックスをすばやく見つける(セット内の最初の整数notをすばやく見つける)にも使用できます。補数を使用して、「穴」のある配列に要素を挿入するインデックスを特定し、迅速な削除を可能にし、他の要素へのインデックスの無効化を回避します。
私は実際にそれが非常に便利であると思ったので、中央配列に格納されているすべてのインデックスを格納し、可能な場合はポインターを避けたいところまで、コードベースの設計方法を形作ったと思います。ハンマーを振るうとき、すべてを釘として見る傾向がありますが、この場合、パフォーマンスの高いソリューションを簡単に入手できるのは非常に効率的なハンマーです。常にセットの交差を実行したい(「X、Y、Zコンポーネントを実装するすべてのエンティティを見つける」)エンティティコンポーネントシステムに取り組んだ後、私はこれまでよりもさらに多くの用途を見つけました。
ループアクセラレーション
右下に示されているような高速線形トラバーサルは私にとって非常に大きなものです。私が作成した多くのプロジェクトには、線形(for each element in the set, do some light work
タイプのループ)よりもうまくできないループがたくさんあるからです。階層の最上位に向かって設定された単一ビットが範囲内のインデックス[8388608,16777216)
が存在することを示すことができる場合、ほとんどのメモリにアクセスしながらループを実行できるため、ループはインデックスの配列を反復するよりも安くなります。たとえば、800万ビット以上、さらには800万以上の整数ではなく、1ビットを検査するだけのセット。
その結果、たとえば整数の配列(int[]
)やvector<int>
の代わりにセットを必要としない場合でも、この構造を使用することがよくあります。結果は何回も。インデックスが主に隣接する範囲で構成されている場合、ループアクセラレータとして機能します。これらの場合、整数の配列よりも反復処理がはるかに高速だからです。それを無視しても、ループは元の要素にソート順にアクセスすることを意味します。これは通常、ソートされていないインデックスの配列をループするだけで得られる散発的なランダムアクセスとは対照的に、最もキャッシュフレンドリーなアクセスパターンです。
スパースデータ
そして、それはまばらなデータにかなり適しているので、インデックス/整数が非常にまばらであり、非常に広い範囲(それが大きすぎない限り、範囲)を持っている場合、メモリのボートロードを大量に消費することについて心配する必要はありません何百万というのはまったく問題ありませんが、10億または1兆は非常にまばらな表現に適したものを必要とするかもしれません。
時間的局所性
おもしろいことに、単一の整数の存在をテストするのに十分ですが、アルゴリズムの複雑度は、単純な巨大なビット配列(ビットセット)よりも、log(N)/ log(64)反復(またはビットごとの演算)のような最悪のケースになります。そのため、特定のビットを単一のビット演算といくつかの算術演算(またはビットシフトとビット単位のAND)で一定時間でテストできます。ただし、特にインデックスが適度に密である場合(スーパーの場合)、リーフにドリルダウンせずにnth
ビットが設定されているかどうかを頻繁に判断できることに関連する時間的局所性のため、それでもなお高速になる傾向があります。まばらなケースでは、葉ノードに直接行き、これをツリーではなくビットの巨大な配列のように扱うことができます)。