私の理解...
利点:
- 最後に挿入すると、O(N)ではなくO(1)になります。
- リストが二重リンクリストの場合、末尾から削除することもO(N)ではなくO(1)です。
不利益:
これらの長所と短所を見ると、リンクリストがテールポインターの使用を避ける理由がわかりません。私が欠けているものはありますか?
私の理解...
利点:
不利益:
これらの長所と短所を見ると、リンクリストがテールポインターの使用を避ける理由がわかりません。私が欠けているものはありますか?
回答:
あなたは正しいです、テールポインターは決して痛くなく、助けることができるだけです。ただし、テールポインターがまったく必要ない場合があります。
リンクリストを使用してスタックを実装する場合、すべてのアクセス、挿入、および削除が先頭で行われることを保証できるため、テールポインターは不要です。とにかく、それはライブラリまたはプラットフォームでの標準的な実装であり、メモリは安価であるため、テールポインタを使用して二重リンクリストを使用する可能性がありますが、必要ではありません。
リンクリストは非常に一般的に永続的で不変です。実際、関数型プログラミング言語では、この使用法はどこにでもあります。テールポインターは、これらのプロパティの両方を破壊します。ただし、不変性や永続性を気にしない場合は、テールポインターを含めることによるマイナス面はほとんどありません。
リンクリストにテールポインターを使用することはめったになく、スタックのようなプッシュ/ポップパターンの挿入と削除(または単に中央からの線形時間の削除)で十分な場合、単一リンクリストをより頻繁に使用する傾向があります。私の一般的なユースケースでは、テールリンクが実際に高価であるのは、単一リンクリストを二重リンクリストにするのが高価だからです。
多くの場合、単一リンクリストの一般的な使用例では、それぞれ数個のリストノードのみを含む数十万のリンクリストが格納されます。また、通常、リンクリストにポインターを使用しません。インデックスは32ビットにすることができるため、代わりに配列にインデックスを使用します。たとえば、64ビットポインターの半分のスペースを使用します。また、通常、リストノードを1つずつ割り当てず、代わりに、すべてのノードを格納するために大きな配列を使用し、32ビットインデックスを使用してノードをリンクします。
一例として、400x400のグリッドを使用して、衝突検出を加速するために互いに動き回ったり跳ね返ったりする100万個の粒子を分割するビデオゲームを想像してください。その場合、非常に効率的な保存方法は、160,000の片方向リンクリストを保存することです。これは、私の場合、160,000の32ビット整数(約640キロバイト)とパーティクルごとの32ビット整数オーバーヘッドに変換されます。パーティクルが画面上を動き回ると、次のように、いくつかの32ビット整数を更新して、あるセルから別のセルにパーティクルを移動するだけです。
... next
セル内の次のパーティクルへのインデックスまたはパーティクルが消滅した場合に回収する次のフリーパーティクルのインデックスとして機能するパーティクルノードのインデックス(「ポインタ」)を使用します(基本的にインデックスを使用したフリーリストアロケータの実装)。
セル内のパーティクルを反復処理することでパーティクルロジックを処理しているため、セルからの線形時間の削除は実際にはオーバーヘッドではありません。したがって、二重にリンクされたリストは、私の場合はすべて、尾も私にはまったく役に立たないのと同じです。
テールポインターは、グリッドのメモリ使用量を2倍にし、キャッシュミスの数を増やします。また、リストがブランチレスではなく空であるかどうかをチェックするためにブランチを要求するには、挿入が必要です。二重リンクリストにすると、各パーティクルのリストオーバーヘッドが2倍になります。私がリンクリストを使用する時間の90%は、このような場合のためです。そのため、実際にはテールポインターを格納するのは比較的かなり高価です。
したがって、最初にリンクリストを使用するほとんどのコンテキストでは、実際には4〜8バイトは些細なことではありません。データ構造を使用して要素のボートロードを格納している場合、4〜8バイトが必ずしもそれほど無視できない場合があるため、そこにチップを入れたいと思っただけです。実際にリンクリストを使用して、メモリ割り当ての数と必要なメモリの量を削減します。たとえば、爆発的なメモリ使用を伴うグリッド用に成長する160,000の動的配列(通常、少なくともグリッドセルごとに1つのポインタと2つの整数)セルごとに1つの整数とゼロのヒープ割り当てとは対照的に、グリッドセルごとのヒープ割り当てと共に)。
LLが連続性が一般的に不足しているためにLLが適切でない場合、フロント/ミドルの削除とフロント/ミドルの挿入に関連する一定の時間の複雑さのために、多くの人がリンクリストに手を伸ばすことがよくあります。パフォーマンスの観点からLLが私にとって美しいのは、いくつかのポインタを操作するだけで1つの要素を1つのリストから別の要素に移動する機能であり、可変サイズのメモリアロケータなしで可変サイズのデータ構造を実現できることです各ノードのサイズは均一です。たとえば、フリーリストを使用できます。各リストノードが汎用アロケーターに対して個別に割り当てられている場合、それは通常、リンクリストが他の選択肢に比べてはるかに悪い場合です。
私は代わりに、リンクされたリストが単純な代替案に対して非常に効果的な最適化として機能するほとんどの場合、最も有用なフォームは一般に単一リンクであり、ヘッドポインターのみが必要であり、汎用メモリ割り当てを必要としないことをお勧めしますノードではなく、ノードごとに既に割り当てられているメモリをプールすることができます(たとえば、事前に割り当てられた大きな配列から)。また、通常、各SLLは、グラフノードに接続されたエッジ(1つの巨大なリンクリストではなく、多くの小さなリンクリスト)のような非常に少数の要素を格納します。
また、最近ではDRAMが大量に使用されていますが、これは利用可能なメモリの中で2番目に遅いタイプです。64バイトのキャッシュラインを備えたL1キャッシュに関しては、コアあたり64 KBのようです。結果として、これらの小さなバイトの節約は、上記のパーティクルシムのようなパフォーマンスが重要な領域で、キャッシュラインに2倍のノードを格納するかどうかの違いを意味する場合、数百万回掛けると本当に重要になります。