ListとLinkedListのどちらを使用するのが良いですか?
ListとLinkedListのどちらを使用するのが良いですか?
回答:
この回答へのコメントを読んでください。人々は私が適切なテストをしなかったと主張します。これは受け入れられる答えではないことに同意します。学んでいる間、私はいくつかのテストを行い、それらを共有したいと思いました。
興味深い結果が見つかりました:
// Temporary class to show the example
class Temp
{
public decimal A, B, C, D;
public Temp(decimal a, decimal b, decimal c, decimal d)
{
A = a; B = b; C = c; D = d;
}
}
LinkedList<Temp> list = new LinkedList<Temp>();
for (var i = 0; i < 12345678; i++)
{
var a = new Temp(i, i, i, i);
list.AddLast(a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
List<Temp> list = new List<Temp>(); // 2.4 seconds
for (var i = 0; i < 12345678; i++)
{
var a = new Temp(i, i, i, i);
list.Add(a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
本質的にデータにアクセスするだけでも、かなり遅くなります!! linkedListを使用しないでください。
これは、多数の挿入を実行する別の比較です(リストの中央にアイテムを挿入する予定です)。
LinkedList<Temp> list = new LinkedList<Temp>();
for (var i = 0; i < 123456; i++)
{
var a = new Temp(i, i, i, i);
list.AddLast(a);
var curNode = list.First;
for (var k = 0; k < i/2; k++) // In order to insert a node at the middle of the list we need to find it
curNode = curNode.Next;
list.AddAfter(curNode, a); // Insert it after
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
List<Temp> list = new List<Temp>();
for (var i = 0; i < 123456; i++)
{
var a = new Temp(i, i, i, i);
list.Insert(i / 2, a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
list.AddLast(new Temp(1,1,1,1));
var referenceNode = list.First;
for (var i = 0; i < 123456; i++)
{
var a = new Temp(i, i, i, i);
list.AddLast(a);
list.AddBefore(referenceNode, a);
}
decimal sum = 0;
foreach (var item in list)
sum += item.A;
したがって、複数のアイテムを挿入する予定があり、どこかにアイテムを挿入する予定の参照がある場合にのみ、リンクリストを使用します。たくさんのアイテムを挿入する必要があるからといって、挿入したい場所を探すのに時間がかかるので、速くはなりません。
list.AddLast(a);
最後の2つのLinkedListの例のループ内なのですか?list.AddLast(new Temp(1,1,1,1));
最後から2番目のLinkedListの場合と同様に、ループの前に一度実行しますが、ループ自体にTempオブジェクトを2倍追加しているように見えます(私には)。(そして、私がテストアプリで自分自身を再確認するとき、確かに、LinkedListの2倍です。)
I say never use a linkedList.
後の投稿で明らかになるように、あなたの一般的なアドバイスに欠陥があります。あなたはそれを編集したいかもしれません。2)タイミングは?インスタンス化、追加、および列挙をすべて1つのステップで実行しますか?ほとんどの場合、インスタンス化と列挙は、PPLが心配することではなく、それらは1つのタイムステップです。具体的には、挿入と追加のタイミングを調整すると、より良いアイデアが得られます。3)最も重要なのは、必要以上にリンクリストに追加することです。これは間違った比較です。リンクリストに関する誤った考えを広めます。
ほとんどの場合、List<T>
より便利です。LinkedList<T>
リストの中央でアイテムを追加/削除する場合のコストは低くなりますが、リストList<T>
の最後で追加/削除できるのは安価です。
LinkedList<T>
順方向または逆方向のシーケンシャルデータにアクセスしている場合にのみ、最も効率的です。ランダムアクセスは、チェーンを毎回ウォークする必要があるため(インデクサーがないため)、比較的コストがかかります。ただし、a List<T>
は本質的に単なる配列(ラッパー付き)であるため、ランダムアクセスで問題ありません。
List<T>
また、支援方法の多くを提供しています- Find
、ToArray
など。ただし、これらはLinkedList<T>
.NET 3.5 / C#3.0でも拡張メソッドを介して利用できるため、それほど重要ではありません。
List<T>
そして、T[]
(すべて1スラブ)あまりにも分厚いもののために失敗し、LinkedList<T>
(要素ごとスラブ)あまりにも粒状であるために泣き叫ぶます。
リンクされたリストをリストと考えるのは少し誤解を招く可能性があります。チェーンのようなものです。実際、.NETでは、LinkedList<T>
も実装していませんIList<T>
。リンクされたリストには実際のインデックスの概念はありませんが、実際にはありません。確かに、クラスで提供されるメソッドはどれもインデックスを受け入れません。
リンクされたリストは、単一にリンクされていても、二重にリンクされていてもかまいません。これは、チェーン内の各要素が次の要素のみにリンクされているか(単一リンク)、前/次の要素の両方にリンクされているか(二重リンク)を示します。 LinkedList<T>
二重にリンクされています。
内部的にList<T>
は、配列によってサポートされています。これにより、メモリ内で非常にコンパクトな表現が提供されます。逆に、LinkedList<T>
連続する要素間の双方向リンクを格納するために追加のメモリが必要です。そのため、aのメモリフットプリントは、LinkedList<T>
一般的にの場合よりも大きくなります(追加操作中のパフォーマンスを向上させるために、未使用の内部配列要素をList<T>
含めることList<T>
ができるという警告があります)。
また、パフォーマンス特性も異なります。
LinkedList<T>.AddLast(item)
一定の時間List<T>.Add(item)
償却一定時間、線形ワーストケースLinkedList<T>.AddFirst(item)
一定の時間List<T>.Insert(0, item)
線形時間LinkedList<T>.AddBefore(node, item)
一定の時間LinkedList<T>.AddAfter(node, item)
一定の時間List<T>.Insert(index, item)
線形時間LinkedList<T>.Remove(item)
線形時間LinkedList<T>.Remove(node)
一定の時間List<T>.Remove(item)
線形時間List<T>.RemoveAt(index)
線形時間LinkedList<T>.Count
一定の時間List<T>.Count
一定の時間LinkedList<T>.Contains(item)
線形時間List<T>.Contains(item)
線形時間LinkedList<T>.Clear()
線形時間List<T>.Clear()
線形時間ご覧のとおり、これらはほとんど同じです。実際には、のAPI LinkedList<T>
を使用するのはより面倒であり、その内部ニーズの詳細がコードに流出します。
ただし、リスト内から多くの挿入/削除を行う必要がある場合は、一定の時間が提供されます。 List<T>
リスト内の余分なアイテムは挿入/削除後にシャッフルする必要があるため、線形時間を提供します。
リンクされたリストは、リストメンバーの非常に高速な挿入または削除を提供します。リンクリストの各メンバーには、リスト内の次のメンバーへのポインターが含まれているため、メンバーを位置iに挿入します。
リンクリストの欠点は、ランダムアクセスができないことです。メンバーにアクセスするには、目的のメンバーが見つかるまでリストを走査する必要があります。
私の以前の答えは十分に正確ではありませんでした。本当にそれは恐ろしいことでした:Dしかし、今では私ははるかに有用で正しい答えを投稿することができます。
追加のテストをいくつか行いました。次のリンクでソースを見つけ、ご自分の環境で再確認できます。https://github.com/ukushu/DataStructuresTestsAndOther.git
短い結果:
配列を使用する必要があります:
リストは使用する必要があります:
LinkedListを使用する必要があります:
詳細:
LinkedList<T>
内部的には.NETのリストではありません。それも実装していませんIList<T>
。そしてそれが、インデックスに関連するインデックスとメソッドが存在しない理由です。
LinkedList<T>
ノードポインタベースのコレクションです。.NETでは、二重にリンクされた実装にあります。これは、前/次の要素が現在の要素にリンクしていることを意味します。そして、データは断片化されています-RAMの異なる場所に異なるリストオブジェクトを配置できます。またLinkedList<T>
、List<T>
または配列よりも多くのメモリが使用されます。
List<T>
.NetはJavaのの代替ですArrayList<T>
。これは、これが配列ラッパーであることを意味します。つまり、1つの連続したデータブロックとしてメモリに割り当てられます。割り当てられたデータサイズが85000バイトを超える場合、ラージオブジェクトヒープに移動されます。サイズによっては、ヒープの断片化(メモリリークの穏やかな形式)が発生する可能性があります。しかし同時に、サイズが85000バイト未満の場合、これはメモリ内で非常にコンパクトで高速なアクセス表現を提供します。
ランダムアクセスパフォーマンスとメモリ消費には単一の連続ブロックが推奨されますが、定期的にサイズを変更する必要があるコレクションでは、配列などの構造を新しい場所にコピーする必要がありますが、リンクリストは新しく挿入されたメモリのみを管理する必要があります/ deletedノード。
ListとLinkedListの違いは、基礎となる実装にあります。リストは配列ベースのコレクション(ArrayList)です。LinkedListはノードポインターベースのコレクション(LinkedListNode)です。APIレベルの使用では、どちらもICollection、IEnumerableなどの同じインターフェイスのセットを実装しているため、どちらもほとんど同じです。
重要な違いは、パフォーマンスが重要な場合です。たとえば、 "INSERT"操作の多いリストを実装している場合、LinkedListはListよりも優れています。LinkedListはO(1)時間でそれを行うことができるので、Listは基礎となる配列のサイズを拡張する必要があるかもしれません。詳細/詳細については、LinkedListと配列データ構造のアルゴリズムの違いを確認することをお勧めします。http://en.wikipedia.org/wiki/Linked_list and Array
この助けを願っています、
Add
は常に既存の配列の最後にある場合について考えています。List
O(1)でなくても、「十分」です。終わりでない多くAdd
のが必要な場合、深刻な問題が発生します。Marcは、(サイズ変更が必要なときだけでなく)挿入するたびに既存のデータを移動する必要があると、のパフォーマンスコストが大幅に増加することを指摘しています。List
配列よりもリンクリストの主な利点は、リンクによってアイテムを効率的に再配置できることです。セジウィック、p。91
LinkedListを使用する一般的な状況は次のとおりです。
たとえば、100,000という大きなサイズの文字列のリストから、特定の多くの文字列を削除するとします。削除する文字列はHashSet dicで検索でき、文字列のリストには、削除する30,000〜60,000の文字列が含まれていると考えられています。
それでは、100,000個の文字列を格納するのに最適なタイプのリストは何でしょうか。答えはLinkedListです。それらがArrayListに格納されている場合、それを繰り返し、一致する文字列を削除すると、何十億もの操作が必要になりますが、イテレータとremove()メソッドを使用すると、約100,000操作しかかかりません。
LinkedList<String> strings = readStrings();
HashSet<String> dic = readDic();
Iterator<String> iterator = strings.iterator();
while (iterator.hasNext()){
String string = iterator.next();
if (dic.contains(string))
iterator.remove();
}
RemoveAll
してList
、多くの項目を移動せずにから項目を削除するかWhere
、LINQからを使用して2番目のリストを作成できます。LinkedList
ただし、here を使用すると、他のタイプのコレクションよりも劇的に多くのメモリを消費することになり、メモリの局所性が失われると、反復処理が著しく遅くなり、aよりもかなり悪くなりますList
。
RemoveAll
Javaに同等のものがあるかどうかはわかりません。
RemoveAll
使用できない場合はList
、トムのループのように見える「圧縮」アルゴリズムを実行できますが、2つのインデックスがあり、リストの内部配列で一度に1つずつ保持されるように項目を移動する必要があります。効率はO(n)であり、のトムのアルゴリズムと同じですLinkedList
。どちらのバージョンでも、文字列のHashSetキーを計算する時間が支配的です。これは、を使用する良い例ではありませんLinkedList
。
基本的に、List<>
.NETのは配列のラッパーです。A LinkedList<>
はリンクされたリストです。したがって、問題は、配列とリンクリストの違いは何か、そしてリンクリストの代わりに配列を使用する必要がある場合です。おそらく、どちらを使用するかを決定する上で最も重要な2つの要素は次のようになります。
これは、遠野ナムが受け入れたいくつかの誤った測定値を修正した答えを改作したものです。
テスト:
static void Main()
{
LinkedListPerformance.AddFirst_List(); // 12028 ms
LinkedListPerformance.AddFirst_LinkedList(); // 33 ms
LinkedListPerformance.AddLast_List(); // 33 ms
LinkedListPerformance.AddLast_LinkedList(); // 32 ms
LinkedListPerformance.Enumerate_List(); // 1.08 ms
LinkedListPerformance.Enumerate_LinkedList(); // 3.4 ms
//I tried below as fun exercise - not very meaningful, see code
//sort of equivalent to insertion when having the reference to middle node
LinkedListPerformance.AddMiddle_List(); // 5724 ms
LinkedListPerformance.AddMiddle_LinkedList1(); // 36 ms
LinkedListPerformance.AddMiddle_LinkedList2(); // 32 ms
LinkedListPerformance.AddMiddle_LinkedList3(); // 454 ms
Environment.Exit(-1);
}
そしてコード:
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace stackoverflow
{
static class LinkedListPerformance
{
class Temp
{
public decimal A, B, C, D;
public Temp(decimal a, decimal b, decimal c, decimal d)
{
A = a; B = b; C = c; D = d;
}
}
static readonly int start = 0;
static readonly int end = 123456;
static readonly IEnumerable<Temp> query = Enumerable.Range(start, end - start).Select(temp);
static Temp temp(int i)
{
return new Temp(i, i, i, i);
}
static void StopAndPrint(this Stopwatch watch)
{
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);
}
public static void AddFirst_List()
{
var list = new List<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
list.Insert(0, temp(i));
watch.StopAndPrint();
}
public static void AddFirst_LinkedList()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (int i = start; i < end; i++)
list.AddFirst(temp(i));
watch.StopAndPrint();
}
public static void AddLast_List()
{
var list = new List<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
list.Add(temp(i));
watch.StopAndPrint();
}
public static void AddLast_LinkedList()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (int i = start; i < end; i++)
list.AddLast(temp(i));
watch.StopAndPrint();
}
public static void Enumerate_List()
{
var list = new List<Temp>(query);
var watch = Stopwatch.StartNew();
foreach (var item in list)
{
}
watch.StopAndPrint();
}
public static void Enumerate_LinkedList()
{
var list = new LinkedList<Temp>(query);
var watch = Stopwatch.StartNew();
foreach (var item in list)
{
}
watch.StopAndPrint();
}
//for the fun of it, I tried to time inserting to the middle of
//linked list - this is by no means a realistic scenario! or may be
//these make sense if you assume you have the reference to middle node
//insertion to the middle of list
public static void AddMiddle_List()
{
var list = new List<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
list.Insert(list.Count / 2, temp(i));
watch.StopAndPrint();
}
//insertion in linked list in such a fashion that
//it has the same effect as inserting into the middle of list
public static void AddMiddle_LinkedList1()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
LinkedListNode<Temp> evenNode = null, oddNode = null;
for (int i = start; i < end; i++)
{
if (list.Count == 0)
oddNode = evenNode = list.AddLast(temp(i));
else
if (list.Count % 2 == 1)
oddNode = list.AddBefore(evenNode, temp(i));
else
evenNode = list.AddAfter(oddNode, temp(i));
}
watch.StopAndPrint();
}
//another hacky way
public static void AddMiddle_LinkedList2()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start + 1; i < end; i += 2)
list.AddLast(temp(i));
for (int i = end - 2; i >= 0; i -= 2)
list.AddLast(temp(i));
watch.StopAndPrint();
}
//OP's original more sensible approach, but I tried to filter out
//the intermediate iteration cost in finding the middle node.
public static void AddMiddle_LinkedList3()
{
var list = new LinkedList<Temp>();
var watch = Stopwatch.StartNew();
for (var i = start; i < end; i++)
{
if (list.Count == 0)
list.AddLast(temp(i));
else
{
watch.Stop();
var curNode = list.First;
for (var j = 0; j < list.Count / 2; j++)
curNode = curNode.Next;
watch.Start();
list.AddBefore(curNode, temp(i));
}
}
watch.StopAndPrint();
}
}
}
結果は、他の人がここで文書化した理論的なパフォーマンスに従っていることがわかります。かなり明確- LinkedList<T>
挿入の場合に大きな時間を得る。リストの中央からの削除についてはテストしていませんが、結果は同じになるはずです。もちろんList<T>
、O(1)ランダムアクセスのようにパフォーマンスが向上する他の領域があります。
LinkedList<>
いつ使用するか
Token Stream
。それ以外の場合は、を使用することをお勧めしますList<>
。
LinkedListNode<T>
コード内のオブジェクトを追跡している場合は、挿入/削除のリストをスキャンする必要はありません。それができる場合List<T>
、特に挿入/削除が頻繁に行われる非常に長いリストの場合は、を使用するよりもはるかに優れています。
node.Value
上記の点のほとんどに同意します。また、ほとんどの場合、リストがより明白な選択肢のように見えることにも同意します。
ただし、LinkedListの方がListよりも効率が良いため、はるかに優れている場合が多いことを付け加えておきます。
誰かがこれらのコメントが役に立てば幸いです。
ここに非常に多くの平均的な答え...
一部のリンクリストの実装では、事前に割り当てられたノードの基本ブロックを使用します。彼らがこれを一定時間/線形時間よりも実行しない場合、メモリのパフォーマンスが低下し、キャッシュのパフォーマンスがさらに悪化するため、関連性は低くなります。
リンクリストを使用する場合
1)スレッドセーフが必要です。より優れたスレッドセーフアルゴを構築できます。ロックコストは、並行スタイルリストを支配します。
2)構造のような大きなキューがあり、常に最後以外の場所で削除または追加したい場合。10万を超えるリストは存在しますが、それほど一般的ではありません。
LinkedListコレクションのパフォーマンスに関連する同様の質問をしたところ、DequeのSteven ClearyのC#実装が解決策であることを発見しました。Queueコレクションとは異なり、Dequeではアイテムを前後に移動できます。リンクリストに似ていますが、パフォーマンスが向上しています。
Deque
いる」という記述を再確認してください。そのステートメントを修飾してください:特定のコードでDeque
はLinkedList
、はより優れたパフォーマンスです。リンクをたどると、2日後にIvan Stoevから、これはLinkedListの非効率ではなく、コードの非効率であることがわかりました。(そして、それがLinkedListの非効率であったとしても、それはDequeがより効率的であるという一般的なステートメントを正当化しません;特定の場合にのみ。)