ほとんどの場合、人々がリンクリストを使用しようとするのを見ると、貧しい(または非常に貧しい)選択のように思えます。おそらく、リンクされたリストがデータ構造の適切な選択であるか、そうでない状況を調査することは有用でしょう。
理想的には、データ構造を選択する際に使用する基準、および特定の状況下でどのデータ構造が最適に機能する可能性が高いかについての答えを説明します。
編集:私は言わなければならない、私は数だけでなく、答えの質に非常に感銘を受けています。私は1つしか受け入れることができませんが、もう少し良いものがなかったら、受け入れる価値があったと言わなければならない2つまたは3つがあります。リンクされたリストが本当の利点を提供する状況を指摘したのは、カップル(特に私が受け入れたもの)だけでした。スティーブジェソップは、1つだけでなく3つの異なる答えを思い付くと、ある種の名誉ある言及に値すると思います。もちろん、回答ではなくコメントとしてのみ投稿されていたとしても、Neilのブログエントリは読む価値があると思います-有益であるだけでなく、非常に面白いです。
同時データ構造に役立ちます。 (現在、以下の非同時実世界の使用例があります- @ Neil hadn ' tはFORTRANに言及しました;-)
たとえば、.NET 4.0 RCのConcurrentDictionary<TKey, TValue>
は、リンクリストを使用して、同じバケットにハッシュするアイテムをチェーンします。
ConcurrentStack<T>
の基になるデータ構造もリンクリストです。
ConcurrentStack<T>
は、 新しいスレッドプール の基盤として機能するデータ構造の1つです(基本的にスタックとして実装されるローカル「キュー」を使用)。 (他の主要なサポート構造はConcurrentQueue<T>
です。)
新しいスレッドプールは、新しい Task Parallel Library の作業スケジューリングの基礎を提供します。
したがって、それらは確かに役立つ可能性があります。リンクリストは、現在、少なくとも1つの優れた新技術の主要なサポート構造の1つとして機能しています。
(一方向にリンクされたリストは、魅力的な lock-free になります-ただし、待機なしではありません-主な操作は単一で実行できるため、これらの場合に選択してください- [〜#〜] cas [〜#〜] (+ retries)。Java and .NETなど)のような最新のGC-d環境では ABAの問題 は簡単に回避できます。新しく作成したノードに追加したアイテムをラップし、それらのノードを再利用しないでください-GCに作業を任せます。ABAの問題に関するページでは、ロックフリースタックの実装も提供されます- (GC-ed)Nodeアイテムを保持する)を使用して.Net(&Java)で実際に動作します。
Edit:@Neil:実際、あなたがFORTRANについて言及したことは、同じ種類のリンクリストがおそらく最もよく使われ、悪用されたデータで見つかることを思い出させてくれました.NETの構造:プレーンな.NETジェネリックDictionary<TKey, TValue>
。
1つではなく、多くのリンクリストが配列に格納されます。
基本的に、多くのリンクリストは配列に保存されます。 (使用されるバケットごとに1つ。)再利用可能なノードの無料リストは、それらの間に「織り込まれている」(削除があった場合)。配列はstart/on rehashで割り当てられ、チェーンのノードはその中に保持されます。また、削除に続くfreeポインター(配列へのインデックス)もあります。 ;-)それで-それを信じるかどうか-FORTRAN技術はまだ生きています。 (...そして、最も一般的に使用される.NETデータ構造の1つにある他の場所はありません;-)。
リンクリストは、任意の長さ(コンパイル時は不明)のリストに対して多くの挿入と削除を行う必要があるが、あまり検索する必要がない場合に非常に役立ちます。
リストの分割と結合(双方向リンク)は非常に効率的です。
リンクされたリストを結合することもできます-例えばツリー構造は、水平リンクリスト(兄弟)を接続する「垂直」リンクリスト(親/子関係)として実装できます。
これらの目的で配列ベースのリストを使用することには、厳しい制限があります。
リンクリストは非常に柔軟です。1つのポインターを変更するだけで、配列リストで同じ操作を行うと非常に非効率的な大きな変更を加えることができます。
配列は、リンクリストが通常比較されるデータ構造です。
通常、リンクリストは、リスト自体を大幅に変更する必要がある場合に役立ちますが、配列は要素への直接アクセスのリストよりも優れています。
相対操作コスト(n =リスト/配列の長さ)と比較した、リストと配列に対して実行できる操作のリストを次に示します。
これは、これら2つの一般的で基本的なデータ構造の非常に低レベルの比較であり、リスト自体に多くの変更(要素の削除または追加)を行う必要がある状況で、リストのパフォーマンスが向上することがわかります。一方、配列の要素に直接アクセスする必要がある場合、配列はリストよりもパフォーマンスが向上します。
メモリ割り当ての観点からすると、すべての要素を隣り合わせにする必要がないため、リストの方が優れています。一方、次の(または前の)要素へのポインタを格納する(わずかな)オーバーヘッドがあります。
開発者が実装でリストと配列を選択するためには、これらの違いを知ることが重要です。
これはリストと配列の比較であることに注意してください。ここで報告されている問題に対する優れた解決策があります(例:SkipLists、Dynamic Arraysなど)。この回答では、すべてのプログラマーが知っておくべき基本的なデータ構造を考慮しました。
単一リンクリストは、セルアロケーターまたはオブジェクトプールの空きリストに適しています。
二重リンクリストは、特に最後のアクセスで順序付けられている場合、要素(JavaのLinkedHashMap)の順序も定義するハッシュマップの順序を定義するのに適しています。
確かに、より洗練された調整可能なものと比較して、そもそもLRUキャッシュが良いアイデアであるかどうかについて議論することができますが、もし持っているなら、これはかなりまともな実装です。読み取りアクセスのたびに、ベクターまたは両端キューに対して中間から削除および最後まで追加を実行する必要はありませんが、通常、ノードを末尾に移動することで問題ありません。
高速なプッシュ、ポップ、回転が必要なときに便利です。O(n)インデックス作成を気にしないでください。
リンクリストは、データの保存場所を制御できないが、何らかの方法でオブジェクト間を移動する必要がある場合の自然な選択肢の1つです。
たとえば、C++(新規/削除置換)でメモリトラッキングを実装する場合、解放されたポインタを追跡する制御データ構造が必要です。これは完全に実装する必要があります。別の方法は、全体リストを作成し、リンクリストを各データチャンクの先頭に追加することです。
削除が呼び出されたときにリスト内のどこにいるかを常に知っているため、O(1)でメモリを簡単に放棄できます。また、mallocされたばかりの新しいチャンクの追加はO(1)にあります。この場合、リストを歩くことはほとんど必要ないので、O(n)コストはここでは問題になりません(構造を歩くことはO(n)とにかく)。
単一リンクリストは、関数型プログラミング言語の一般的な「リスト」データ型の明らかな実装です。
(append (list x) (L))
および(append (list y) (L))
は、ほぼすべてのデータを共有できます。書き込みのない言語でコピーオンライトの必要はありません。機能的なプログラマは、これを活用する方法を知っています。それに比べて、ベクトルまたは両端キューは通常、どちらかの端に追加するのが遅く、(少なくとも2つの別個の追加の例では)リスト(ベクトル)全体、またはインデックスブロックとデータブロックのコピーを取得する必要があります(deque)に追加されます。実際には、何らかの理由で末尾に追加する必要がある大きなリストの両端キューに対して何か言われることがあります。私は判断するための関数型プログラミングについて十分に知らされていません。
私の経験から、スパース行列とフィボナッチヒープの実装。リンクリストを使用すると、このようなデータ構造の全体的な構造をより詳細に制御できます。スパース行列がリンクリストを使用して実装するのが最適かどうかはわかりませんが、おそらくより良い方法がありますが、それはアンダーグレードCSでリンクリストを使用してスパース行列のインとアウトを学ぶのに本当に役立ちました:)
リンクリストの適切な使用例の1つは、リスト要素が非常に大きい場合です。同時に1つまたは2つだけがCPUキャッシュに収まる十分な大きさです。この時点で、反復用のベクトルや配列などの連続したブロックコンテナーの利点は多かれ少なかれ無効になり、多くの挿入と削除がリアルタイムで発生する場合、パフォーマンス上の利点が得られる可能性があります。
リンクリストは、繰り返しに連動する部分を含むシステムのドメインドリブンデザインスタイルの実装で非常に役立つ場合があることを考慮してください。
頭に浮かぶ例は、吊り鎖をモデル化する場合です。特定のリンクの緊張状態を知りたい場合は、インターフェイスに「見かけの」重みのゲッターを含めることができます。その実装には、次のリンクに見かけの重みを要求するリンクが含まれ、結果に独自の重みが追加されます。この方法では、チェーンのクライアントからの1回の呼び出しで、一番下までの全長が評価されます。
自然言語のように読み取るコードの提唱者である私は、これがプログラマーがチェーンリンクにどのくらいの重みを運ぶかを尋ねさせる方法が好きです。また、リンク実装の境界内でこれらのプロパティの子供を計算する懸念を保持し、チェーンの重量計算サービスの必要性を排除します。
メッシュや画像処理、物理エンジン、レイトレーシングなどのパフォーマンスが重要なフィールドで動作するリンクリストで最も便利なケースの1つは、リンクリストを使用すると、実際に参照の局所性が向上し、ヒープ割り当てが削減され、場合によってはメモリ使用量が削減されることもあります簡単な代替案。
これは、リンクされたリストがしばしば反対のことを行うことで悪名高いので、リンクされたリストがすべてを行うことができる完全な矛盾のように見えますが、各リストノードには、許可するために活用できる固定サイズとアライメントの要件があるというユニークなプロパティがありますそれらは連続して保存され、可変サイズのものでは不可能な方法で一定時間で削除されます。
その結果、100万個のネストされた可変長サブシーケンスを含む可変長シーケンスを格納するのと同じようなことをしたい場合を考えてみましょう。具体的な例は、100万個のポリゴン(三角形、四角形、五角形、六角形など)を格納するインデックス付きメッシュであり、メッシュはメッシュ内のどこからでも削除され、既存のポリゴンに頂点を挿入するためにポリゴンが再構築されたり、削除してください。その場合、100万個の小さなstd::vectors
、その後、すべての単一ベクトルのヒープ割り当てと爆発的なメモリ使用に直面することになります。 100万個の小さなSmallVectors
は、一般的なケースではこの問題をそれほど受けないかもしれませんが、別々にヒープに割り当てられていない事前に割り当てられたバッファは、爆発的なメモリ使用を引き起こす可能性があります。
ここでの問題は、100万std::vector
インスタンスは、100万個の可変長のものを保存しようとしています。可変長のものは、コンテンツをヒープ上の他の場所に保存しなかった場合、非常に効果的に連続して保存し、一定の時間で(少なくとも非常に複雑なアロケーターなしで簡単に)削除できないため、ヒープ割り当てが必要になる傾向があります。
代わりに、これを行う場合:
struct FaceVertex
{
// Points to next vertex in polygon or -1
// if we're at the end of the polygon.
int next;
...
};
struct Polygon
{
// Points to first vertex in polygon.
int first_vertex;
...
};
struct Mesh
{
// Stores all the face vertices for all polygons.
std::vector<FaceVertex> fvs;
// Stores all the polygons.
std::vector<Polygon> polys;
};
...その後、ヒープ割り当てとキャッシュミスの数を劇的に削減しました。アクセスするすべてのポリゴンにヒープ割り当てと潜在的な強制キャッシュミスを要求する代わりに、メッシュ全体に格納された2つのベクトルのいずれかが容量(償却コスト)を超えたときにのみヒープ割り当てを要求するようになりました。また、ある頂点から次の頂点に到達するためのストライドによりキャッシュミスが発生する可能性がありますが、ノードが連続して格納され、隣接する頂点が格納される可能性があるため、すべてのポリゴンが個別の動的配列を格納する場合よりも少なくなることがよくあります立ち退きの前にアクセスする(特に、多くのポリゴンが頂点を一度にすべて追加するため、ポリゴン頂点の大部分が完全に連続することを考慮してください)。
別の例を次に示します。
...ここでは、グリッドセルを使用して、たとえば、1フレームごとに移動する1600万個の粒子の粒子間衝突を加速します。このパーティクルグリッドの例では、リンクリストを使用して、3つのインデックスを変更するだけで、あるグリッドセルから別のグリッドセルにパーティクルを移動できます。ベクトルから消去して別のベクトルにプッシュバックすると、かなり高価になり、ヒープ割り当てが増えます。リンクリストは、セルのメモリを32ビットに減らします。実装に応じて、ベクターは、空のベクターに32バイトを使用できるポイントに動的配列を事前に割り当てることができます。約100万個のグリッドセルがある場合、それはまったく違います。
...これは最近、リンクリストが最も役立つ場所であり、32ビットインデックスは64ビットマシン上のリンクのメモリ要件を半分にするため、「インデックス付きリンクリスト」の種類が特に便利であることがわかります。ノードは配列に連続して格納されます。
多くの場合、それらをインデックス付きの空きリストと組み合わせて、どこでも一定の時間の削除と挿入を可能にします。
その場合、next
インデックスは、ノードが削除されている場合は次の空きインデックスを、ノードが削除されていない場合は次に使用されるインデックスを指します。
そして、これは最近私がリンクリストで見つけた一番のユースケースです。たとえば、それぞれ4つの要素を平均する(ただしこれらのサブシーケンスの1つに要素が削除されて追加される)平均100万の可変長サブシーケンスを格納する場合、リンクリストを使用して400万を格納できますそれぞれ個別にヒープに割り当てられた100万のコンテナの代わりに、リンクされたリストノードが連続して:1つの巨大なベクトル、つまり100万の小さなものではありません。
リストには簡単にO(1)であり、他のデータ構造ではO(1)-削除および挿入要素の順序を維持する必要があると仮定して、任意の位置からの要素。
ハッシュマップは、明らかにO(1)で挿入と削除を行うことができますが、要素を順番に繰り返すことはできません。
上記の事実を考えると、ハッシュマップをリンクリストと組み合わせて、気の利いたLRUキャッシュを作成できます。一定数のキーと値のペアを格納し、アクセス頻度が最も低いキーをドロップして、新しいキーのためのスペースを作ります。
ハッシュマップのエントリには、リンクリストノードへのポインタが必要です。ハッシュマップにアクセスすると、リンクリストノードは現在の位置からリンク解除され、リストの先頭に移動します(O(1)、リンクリストはいや!)。使用頻度が最も低い要素を削除する必要がある場合は、リストの末尾から1つを削除する必要があります(再度O(1)末尾ノードへのポインタを保持すると仮定))関連付けられたハッシュマップエントリ(したがって、リストからハッシュマップへのバックリンクが必要です。)
過去にC/C++アプリケーションでリンクリスト(二重リンクリストを含む)を使用しました。これは.NETおよびstlの前でした。
必要なすべてのトラバーサルコードはLinq拡張メソッドを介して提供されるため、おそらく.NET言語ではリンクリストを使用しないでしょう。