web-dev-qa-db-ja.com

一連の範囲内で数値が属する範囲をすばやく見つけるための高速アルゴリズム?

シナリオ

いくつかの番号範囲があります。これらの範囲は重複していません。重複していないため、論理的な結果として、一度に複数の範囲に数値を含めることはできません。各範囲は連続しています(単一の範囲内に穴がないため、8〜16の範囲には実際には8〜16のすべての数値が含まれます)が、2つの範囲の間に穴が存在する可能性があります(たとえば、範囲は64から始まり128になります。次の範囲は256から始まり、384になります)。したがって、一部の番号はどの範囲にもまったく属していない可能性があります(この例では、129から255の番号はどの範囲にも属していません)。

問題

番号を取得しているので、その番号がどの範囲に属しているかを知る必要があります...それがいずれかの範囲に属しているかどうか。そうでなければ、私はそれがどの範囲にも属していないことを知る必要があります。もちろん、速度は重要です。何千もの範囲があるかもしれないので、O(n)になるすべての範囲を単純にチェックすることはできません。

シンプルなソリューション

簡単な解決策は、すべての数値を並べ替えられた配列に保持し、その配列でバイナリ検索を実行することでした。それは私に少なくともO(log n)を与えるでしょう。もちろん、バイナリ検索は、範囲の最小数と最大数を常にチェックする必要があるため、多少変更する必要があります。検索する番号が間にある場合は、正しい範囲が見つかりました。それ以外の場合は、現在の範囲より下または上の範囲を検索する必要があります。最後に残っている範囲が1つだけで、その数がその範囲内にない場合、その数はまったく範囲内になく、「見つかりません」という結果を返すことができます。

範囲は、ある種のツリー構造でチェーン化することもできます。これは基本的に、バイナリ検索を使用したソート済みリストのようなものです。利点は、並べ替えられた配列(範囲の追加/削除)よりもツリーの変更が高速になることですが、ツリーのバランスを保つために余分な時間を浪費するのとは異なり、ツリーは時間の経過とともに非常に不均衡になる可能性があり、その結果、ソートされた配列のバイナリ検索よりもはるかに遅い検索。

実際には、検索と変更操作の数はほぼバランスが取れているため、どちらのソリューションが良いか悪いかを議論することができます(1秒あたりに実行される検索と追加/削除操作の数は同じになります)。

質問

この種の問題に対して、ソートされたリストやツリーよりも優れたデータ構造があるのでしょうか。おそらく、最良の場合はO(log n)、最悪の場合はO(log n)よりも優れている可能性がありますか?

ここで役立つ可能性のある追加情報は次のとおりです。すべての範囲は常に2の倍数で開始および終了します。それらは常にすべて2の同じ累乗で開始および終了します(たとえば、すべて4の倍数または8の倍数または16の倍数で開始/終了します)。 2の累乗は、実行時に変更できません。最初の範囲を追加する前に、2の累乗を設定する必要があり、これまでに追加したすべての範囲は、アプリケーションが終了するまで、この値の倍数で開始/終了する必要があります。これは、すべてが次の倍数で始まるかのように、最適化に使用できると思います。 8、すべての比較操作の最初の3ビットは無視できますが、他のビットだけで範囲がわかります。

セクションと範囲の木について読みました。これらの問題に対する最適な解決策はありますか?おそらくより良い解決策はありますか?問題はmalloc実装が実行する必要があることと似ているように聞こえます(たとえば、解放されたすべてのメモリブロックは使用可能なメモリの範囲に属し、malloc実装はどのメモリに属する​​かを見つける必要があります)、それらは一般的にどのように問題を解決しますか?

38
Mecki

さまざまなベンチマークを実行した後、ここではツリーのような構造しか機能しないという結論に達しました。ソートされたリストは、もちろん良好なルックアップパフォーマンス(O(log n))を示していますが、更新パフォーマンスはひどく示されています(挿入と削除は、ツリーと比較して10倍以上遅くなります!)。

平衡二分木もO(log n)ルックアップパフォーマンスを備えていますが、O(log n)付近でも更新がはるかに高速であり、ソートされたリストはO(n)更新の場合(挿入する位置または削除する要素を見つけるためのO(log n)ですが、リスト内で最大n個の要素を移動する必要があります。これはO(n)です)。

私はAVLツリー、赤黒木、Treap、AAツリー、およびさまざまなバリエーションのBツリーを実装しました(Bはここではバイエルツリーを意味し、バイナリではありません)。結果:バイエルの木はほとんど勝ちません。それらのルックアップは良好ですが、更新パフォーマンスは不良です(Bツリーの各ノード内にソートされたリストが再びあります!)。バイエルツリーは、ノードの読み取り/書き込みが非常に遅い操作である場合にのみ優れています(たとえば、ノードがハードディスクから直接読み取りまたは書き込みされる場合)-Bツリーは他のどのノードよりもはるかに少ないノードを読み取り/書き込みする必要があるためツリーなので、そのような場合は勝ちます。ただし、ツリーをメモリに保持している場合は、他のツリーに対抗するチャンスはありません。Bツリーファンの皆さんには申し訳ありません。

Treapは実装が最も簡単で(他のバランスの取れたツリーに必要なコードの半分未満、アンバランスのツリーに必要なコードの2倍のみ)、ルックアップと更新の平均パフォーマンスが良好です...しかし、それ。

AAツリーは驚くほど優れたルックアップパフォーマンスを示しています-理由はわかりません。彼らは時々他のすべての木を打ち負かします(遠くはありませんが、それでも偶然ではないほど十分です)...そして除去性能は大丈夫です、しかし私がそれらを正しく実装するのに愚かでない限り、挿入性能は本当に悪いです(それは実行します他のどのツリーよりもすべての挿入ではるかに多くのツリー回転-Bツリーでさえより速い挿入パフォーマンスを持っています)。

これにより、AVLとRB-Treeの2つのクラシックが残ります。どちらも非常に似ていますが、何時間ものベンチマークを行った後、1つ明らかなことがあります。それは、AVLツリーのルックアップパフォーマンスがRBツリーよりも確実に優れていることです。違いはそれほど大きくはありませんが、すべてのベンチマークの2/3で、ルックアップテストに勝ちます。当然のことながら、結局のところ、AVLツリーはRBツリーよりも厳密にバランスが取れているため、ほとんどの場合、最適なバイナリツリーに近くなります。ここでは大きな違いについて話しているのではなく、常に緊密な競争です。

一方、RBツリーはほとんどすべてのテストランで挿入に関してAVLツリーを上回っており、それはそれほど緊密な競争ではありません。以前のように、それは予想されます。厳密にバランスが取れていないRBツリーは、AVLツリーと比較してインサートでのツリー回転がはるかに少なくなります。

ノードの削除はどうですか?ここでは、ノードの数に大きく依存しているようです。ノード数が少ない場合(50万未満すべて)、RBツリーは再びAVLツリーを所有します。違いはインサートの場合よりもさらに大きくなります。むしろ予想外のことは、ノード数が100万ノードを超えると、AVLツリーが追いついてきて、RBツリーとの差がほぼ同じくらい速くなるまで縮小することです。ただし、これはシステムの影響である可能性があります。プロセスのメモリ使用量やCPUキャッシングなどに関係している可能性があります。 AVLツリーよりもRBツリーに悪影響を及ぼし、AVLツリーが追いつく可能性があるもの。ルックアップ(ノードの数に関係なく、AVLは通常高速)と挿入(ノードの数に関係なく、RBは通常高速)では同じ効果は観察されません。

結論:
ルックアップの数は挿入と削除の数よりもいくらか多いだけであり、ルックアップでのAVLの速度に関係なく、全体的なパフォーマンスはRB-Treeを使用する場合に最も速く得られると思います。挿入/削除のパフォーマンスが低下します。

つまり、ここの誰かがRBツリーを大いに所有するはるかに優れたデータ構造を思い付く可能性がない限り;-)

23
Mecki

並べ替えられたリストを作成し、下マージン/開始で並べ替えます。数百万の範囲がない限り(そしておそらくそれでも)、これは実装が最も簡単で、十分に高速です。

範囲を探すときは、start <= positionの範囲を見つけてください。リストはソートされているため、ここでバイナリ検索を使用できます。 position <= endの場合、数値は範囲内です。

範囲の終わりは次の範囲の始まりよりも小さいことが保証されているため、位置が含まれている可能性のある範囲が見つかるまで、終わりを気にする必要はありません。

他のすべてのデータ構造は、交差点を取得したり、範囲が非常に多い場合や、構造体を構築して頻繁にクエリを実行する場合に興味深いものになります。

8
Aaron Digulla

各ノードに範囲を持つバランスの取れたソート済みツリーが答えのようです。それが最適であることを証明することはできませんが、私があなただったら、これ以上探すことはありません。

7
reinierpost

数値の合計範囲が狭く、十分なメモリがある場合は、すべての数値を含む巨大なテーブルを作成できます。

たとえば、100万の数値がある場合、範囲オブジェクトを参照するテーブルを作成できます。

1
Javier

O(log n)平衡二分探索木(BST)の代わりに、ビット単位の(圧縮された)トライを作成することを検討できます。つまり保存している数値のビットのプレフィックスツリー。

これにより、O(w)-検索、挿入、および削除のパフォーマンスが得られます。ここで、w =ビット数(たとえば、32または64から範囲の基になっている2の累乗を引いたもの)。

パフォーマンスが良くも悪くもなるとは言えませんが、BSTとは異なるという意味では真の代替手段のようですが、理論上のパフォーマンスは良好で、BSTと同じように先行クエリを実行できます。

0
Bartel