O(1)の削除と置換を取得できるように、どのデータ構造を使用できますか?または、前述の構造が必要な状況をどのように回避できますか?
ST
Haskell のモナドはこれをうまく行います。
O(1)の削除と置換を取得できるように、どのデータ構造を使用できますか?または、前述の構造が必要な状況をどのように回避できますか?
ST
Haskell のモナドはこれをうまく行います。
回答:
怠inessやその他のトリックを活用して、償却された一定時間、または(キューなどの一部の限られたケースでは)さまざまな種類の問題に対する一定時間の更新を実現する膨大なデータ構造があります。クリス・オカサキの博士論文「Purely Functional Data Structures」と同名の本は(おそらく最初の主要なものの)代表的な例ですが、それ以来この分野は進歩しています。これらのデータ構造は通常、インターフェースで純粋に機能するだけでなく、純粋なHaskellおよび同様の言語で実装することもでき、完全に永続的です。
これらの高度なツールがなくても、単純なバランスの取れたバイナリ検索ツリーは対数時間の更新を提供するため、可変メモリは最悪の場合は対数スローダウンでシミュレートできます。
不正行為と見なされる可能性のある他のオプションもありますが、実装作業と実際のパフォーマンスに関しては非常に効果的です。たとえば、線形型または一意性型は、プログラムが前の値(変更されるメモリ)を保持しないようにすることで、概念的に純粋な言語の実装戦略としてインプレース更新を許可します。これは、永続的なデータ構造ほど一般的ではありません。たとえば、以前のバージョンの状態をすべて保存して、アンドゥログを簡単に作成することはできません。AFAIKはまだ主要な機能言語では利用できませんが、それはまだ強力なツールです。
可変状態を機能設定に安全に導入するためのもう1つのオプションは、ST
Haskell のモナドです。ミューテーションなしで実装でき、unsafe*
機能がなければ、永続的なデータ構造を暗黙的に渡すことの単なる派手なラッパーのように動作します(cf. State
)。しかし、評価の順序を強制し、エスケープを防ぐタイプシステムトリックにより、インプレースミューテーションを使用して安全に実装でき、すべてのパフォーマンス上の利点があります。
1つの安価な可変構造は、引数スタックです。
典型的なSICPスタイルの階乗計算を見てください。
(defn fac (n accum)
(if (= n 1)
accum
(fac (- n 1) (* accum n)))
(defn factorial (n) (fac n 1))
ご覧のとおり、への2番目の引数fac
は、急速に変化する積を含む可変アキュムレーターとして使用されますn * (n-1) * (n-2) * ...
。ただし、変更可能な変数はありません。また、アキュムレータを別のスレッドなどから誤って変更する方法はありません。
もちろんこれは限定的な例です。
ヘッドノードを安価に交換することで、不変のリンクリストを取得できます(さらに、ヘッドから始まる部分を拡張することで)。古いヘッドと同じ次のノードを新しいヘッドポイントに設定するだけです。これは、多くのリスト処理アルゴリズム(何でもfold
ベース)でうまく機能します。
たとえばHAMTに基づいた連想配列からかなり良いパフォーマンスを得ることができます。論理的に、いくつかのキーと値のペアが変更された新しい連想配列を受け取ります。実装は、古いオブジェクトと新しく作成されたオブジェクトの間でほとんどの共通データを共有できます。ただし、これはO(1)ではありません。通常、少なくとも最悪の場合、対数的なものが得られます。一方、不変ツリーは、通常、可変ツリーと比較してパフォーマンスが低下することはありません。もちろん、これにはある程度のメモリオーバーヘッドが必要です。
別のアプローチは、木が森に落ちて誰もそれを聞いていない場合、音を出す必要がないという考えに基づいています。つまり、少し変化した状態が何らかのローカルスコープを決して離れないことを証明できれば、その中のデータを安全に変更できます。
Clojureには、ローカルスコープの外部にリークしない不変のデータ構造の可変の「シャドウ」であるトランジェントがあります。CleanはUniquesを使用して同様のことを実現します(正しく覚えている場合)。Rustは、静的にチェックされた一意のポインターで同様のことを行うのに役立ちます。
ref
し、それらを特定のスコープ内にバインドする方法があります。IORef
またはを参照してくださいSTRef
。そしてもちろん、似たようなTVar
sとMVar
sがありますが、コンカレントセマンティクスは同じです(TVar
sのstmとsのmutexベースMVar
)
あなたが求めているのは少し広すぎる。O(1)どの位置からの取り外しと交換?シーケンスの頭?しっぽ?任意の位置?使用するデータ構造は、それらの詳細に依存します。ただし、2〜3本のフィンガーツリーは、最も汎用性の高い永続的なデータ構造の1つと思われます。
2〜3本のフィンガーツリーを提供します。これは、償却された一定の時間での端へのアクセスをサポートする永続的なシーケンスの機能的表現と、小さなピースのサイズでの時間の対数の連結と分割をサポートします。
(...)
さらに、分割操作を一般的な形式で定義することにより、シーケンス、優先度キュー、検索ツリー、優先度検索キューなどとして機能できる汎用データ構造を取得します。
一般に、永続的なデータ構造は、任意の位置を変更するときに対数パフォーマンスを発揮します。O(1)アルゴリズムの定数は高い可能性があり、対数のスローダウンはより遅いアルゴリズム全体に「吸収」される可能性があるため、これは問題になる場合もあれば、そうでない場合もあります。
さらに重要なことは、永続的なデータ構造により、プログラムに関する推論が容易になることであり、これが常にデフォルトの操作モードである必要があります。永続的なデータ構造を可能な限り優先し、永続的なデータ構造がパフォーマンスのボトルネックであることをプロファイリングして決定した後にのみ、可変のデータ構造を使用する必要があります。それ以外は早すぎる最適化です。