7

一意のIDでキー設定された要素のシーケンスを論理的に表すデータ構造を探します(簡単にするために、それらを文字列、または少なくともハッシュ可能なオブジェクトと見なします)。各要素は1回だけ表示でき、ギャップはなく、最初の位置は0です。

次の操作をサポートする必要があります(1文字の文字列で示されます)。

  • insert(id, position)-キー入力さidれた要素をオフセットのシーケンスに追加しpositionます。当然、シーケンスの後半にある各要素の位置は1ずつ増加します。例:[S E L F].insert(H, 1) -> [S H E L F]
  • remove(position)-オフセットの要素を削除しpositionます。シーケンスの後半にある各要素の位置を1つ減らします。例:[S H E L F].remove(2) -> [S H L F]
  • lookup(id)-でキー設定された要素の位置を見つけますid[S H L F].lookup(H) -> 1

ナイーブな実装は、リンクリストまたは配列のいずれかになります。lookupどちらもO(n) 、、、removeおよびを与えinsertます。

実際にlookupは、が最も使用される可能性が高く、insert線形removeではない方がよいほど頻繁に発生します(ハッシュマップ+配列/リストの単純な組み合わせで取得できます)。

完璧な世界では、それはO(1)lookup、O(log n)insert/removeですが、実際には、純粋に情報理論の観点からは機能しないと思います(私は試していませんが)。したがって、O(log n )lookupそれでもいいでしょう。

4

3 に答える 3

4

トライハッシュマップの組み合わせにより、O(log n)ルックアップ/挿入/削除が可能になります。

trieの各ノードには、このノードと最大2つの子ポインターをルートとする有効な要素のIDとカウンターが含まれます。トライをルートから特定のノードに移動するときに左(0)または右(1)に曲がることによって決定されるビット文字列は、値の一部であり、対応するidのハッシュマップに格納されます。

削除操作は、トライノードを無効としてマークし、削除されたノードからルートへのパス上の有効な要素のすべてのカウンターを更新します。また、対応するハッシュマップエントリを削除します。

挿入操作では、各トライノードの有効な要素の位置パラメーターとカウンターを使用して、新しいノードの先行ノードと後続ノードを検索する必要があります。先行ノードから後続ノードへの順序どおりの走査に削除されたノードが含まれている場合は、ランクが最も低いノードを選択して再利用します。それ以外の場合は、先行または後続のいずれかを選択し、それに新しい子ノードを追加します(先行の場合は右の子、後続の場合は左の子)。次に、このノードからルートへのパス上の有効な要素のすべてのカウンターを更新し、対応するハッシュマップエントリを追加します。

ルックアップ操作は、ハッシュマップからビット文字列を取得し、それを使用して、このパスの左側にある有効な要素のすべてのカウンターを合計しながら、トライルートから対応するノードに移動します。

これにより、挿入/削除のシーケンスが十分にランダムである場合、各操作のO(log n)予想時間が可能になります。そうでない場合、各操作の最悪の場合の複雑さはO(n)です。O(log n)の償却済み複雑度に戻すには、ツリーのスパース性とバランス係数を監視し、削除されたノードが多すぎる場合は、完全にバランスの取れた密な新しいツリーを再作成します。ツリーの不均衡が大きすぎる場合は、最も不均衡なサブツリーを再構築します。


ハッシュマップの代わりに、バイナリ検索ツリーまたは任意の辞書データ構造を使用することができます。トライ内のパスを識別するために使用されるビット文字列の代わりに、ハッシュマップはトライ内の対応するノードへのポインタを格納する場合があります。

このデータ構造でtrieを使用する他の代替手段は、インデックス可能なスキップリストです。


各操作のO(log N)時間は許容範囲ですが、完全ではありません。Kevinによって説明されているように、他の操作のより複雑なO(sqrt(N))と引き換えに、O(1)ルックアップの複雑さを持つアルゴリズムを使用することが可能です。しかし、これは改善することができます。

ルックアップ操作ごとにいくつかのメモリアクセス(M)を選択した場合、他の操作はO(M * N 1 / M)時間で実行される可能性があります。このようなアルゴリズムのアイデアは、関連する質問へのこの回答に示されています。そこに記述されているトライ構造により、位置を配列インデックスに簡単に変換したり、元に戻したりすることができます。この配列の空でない各要素にはidが含まれ、ハッシュマップの各要素はこのIDを配列インデックスにマップします。

このデータ構造に要素を挿入できるようにするには、連続する配列要素の各ブロックを空のスペースでインターリーブする必要があります。ブロックの1つが使用可能なすべての空きスペースを使い果たしたら、50%を超える空きスペースがある、トライの一部の要素に関連するブロックの最小グループを再構築する必要があります。空きスペースの総数が50%未満または75%を超える場合は、構造全体を再構築する必要があります。

このリバランススキームは、ランダムで均等に分散された挿入/削除に対してのみ、 O(M N 1 / M )の償却された複雑さを提供します。最悪の場合の複雑さ(たとえば、常に左端の位置に挿入する場合)は、M> 2の場合にはるかに大きくなります。O(M N 1 / M)の最悪の場合を保証するには、より多くのメモリを予約し、リバランススキームを変更して維持する必要があります。このように不変:構造全体に少なくとも50%の空きスペースを予約し、上位のトライノードに関連するすべてのデータに少なくとも75%の空きスペースを予約し、次のレベルのトライノード(87.5%)に予約します。

M = 2の場合、ルックアップにはO(1)時間、その他の操作にはO(sqrt(N))時間があります。

M = log(N)の場合、すべての操作に対してO(log(N))時間があります。

ただし、実際には、Mの値を小さくする(2 .. 5など)ことが望ましいです。これはO(1)ルックアップ時間として扱われ、この構造(通常の挿入/削除操作を実行している間)がキャッシュに適した方法で最大5つの比較的小さな連続したメモリブロックを処理し、ベクトル化の可能性が高くなります。また、最悪の場合の複雑さが必要な場合は、これによりメモリ要件が制限されます。

于 2012-08-18T13:30:10.137 に答える
3

O(sqrt(n))時間ですべてを達成できますが、多少の作業が必要になることを警告します。

ThriftyListに書いたブログ投稿を見てみましょう。ThriftyListは、最適な時間と空間のサイズ変更可能な配列で説明されているデータ構造の実装であり、それぞれのサイズがO(sqrt(n))のO(sqrt(n))循環サブリストを維持するためのカスタマイズが含まれています。循環サブリストを使用すると、包含サブリストでの標準の挿入/削除後シフトと、それに続く循環サブリスト自体での一連のプッシュ/ポップ操作によって、O(sqrt(n))時間の挿入/削除を実現できます。

ここで、クエリ値が該当するインデックスを取得するには、値からサブリスト/絶対インデックスへのマップを維持する必要があります。つまり、特定の値は、その値を含むサブリストに加えて、値が該当する絶対インデックスにマップされます(アイテムが該当するインデックスは非円形のリストでした)。これらのデータから、循環サブリストの先頭からオフセットを取得し、含まれているサブリストの後ろにある要素の数と合計することにより、値の相対インデックスを計算できます。このマップを維持するには、挿入/削除ごとにO(sqrt(n))操作が必要です。

于 2012-08-18T11:59:59.750 に答える
-1

Clojureの永続的なベクトルとほぼ同じように聞こえます。これらは、ルックアップと更新にO(log32 n)のコストを提供します。nの値が小さい場合、O(log32 n)は定数と同じくらい良好です。

基本的に、それらは配列マップされた試行です。

削除と挿入の時間計算量についてはよくわかりませんが、O(log n)の削除と挿入を使用して、このデータ構造のバリアントを取得できると確信しています。

このプレゼンテーション/ビデオを参照してください:http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey

ソースコード(Java):https ://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/PersistentVector.java

于 2012-08-18T11:17:10.233 に答える