web-dev-qa-db-ja.com

リストとリンクリストをいつ使うべきか

List vs --- LinkedList を使用する方がよい場合はいつですか。

362
Jonathan Allen

編集する

この答えに対するコメントを読んでください。人々は私が適切なテストをしなかったと主張します。私はこれが受け入れられた答えであるべきではないことに同意します。私が学んでいたとき、私はいくつかのテストをして、それらを共有したい気がしました。

元の答え...

私は面白い結果を見つけました:

// 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;
    }
}

リンクリスト(3.9秒)

        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;

リスト(2.4秒)

        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は絶対に使用しません。




多くの挿入を実行するもう1つの比較です(リストの中央に項目を挿入する予定です)

リンクリスト(51秒)

        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;

リスト(7.26秒)

        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;

挿入する場所の参照を持つリンクリスト(0.04秒)

        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;

そのため、複数の項目を挿入する予定があり、かつのどこかに、項目を挿入する予定の場所の参照がある場合に限り、リンクリストを使用します。たくさんのアイテムを挿入する必要があるからといって、挿入したい場所を探すのに時間がかかるため、高速にはなりません。

96
Tono Nam

ほとんどの場合、List<T>がより便利です。 LinkedList<T>は、リストの途中で項目を追加/削除するときのコストが低くなりますが、List<T>は、リストのendで安価に追加/削除できるだけです。

LinkedList<T>は、シーケンシャルデータ(順方向または逆方向)にアクセスしている場合にのみ最も効率的です。ランダムアクセスは毎回チェーンをたどる必要があるため(比較的インデクサがないため)、比較的高価です。ただし、List<T>は基本的に単なる(ラッパー付きの)配列なので、ランダムアクセスは問題ありません。

List<T>FindToArrayなどの多くのサポート方法も提供します。しかし、これらは.NET 3.5/C#3.0のLinkedList<T>でも拡張メソッドを介して利用できます - それはそれほど要因ではありません。

259
Marc Gravell

リンクリストをリストとして考えることは少し誤解を招く可能性があります。鎖のようなものです。実際、.NETでは、LinkedList<T>IList<T>さえ実装していません。リンクリストには実際にはインデックスという概念がありません。クラスで提供されているどのメソッドもインデックスを受け入れません。

リンクリストは、単一リンクでも、二重リンクでもかまいません。これは、チェーン内の各要素が、次の要素へのリンク(1つだけリンクされている)、または前の/次の要素の両方(2重リンク)へのリンクを持っているかどうかを示します。 LinkedList<T>は二重にリンクされています。

内部的には、List<T>は配列によって支えられています。これはメモリ内で非常にコンパクトな表現を提供します。逆に、LinkedList<T>は連続する要素間の双方向リンクを格納するための追加のメモリを必要とします。そのため、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()線形時間

ご覧のとおり、これらはほぼ同等です。実際には、LinkedList<T>のAPIを使用するのは面倒であり、その内部ニーズの詳細はコードに漏れます。

ただし、リスト内から多数の挿入/削除を行う必要がある場合は、一定の時間がかかります。リスト内の余分な項目は挿入/削除後にシャッフルする必要があるため、List<T>は線形時間を提供します。

202
Drew Noakes

リンクリストは、リストメンバーの非常に速い挿入または削除を提供します。リンクリストの各メンバは、リストの次のメンバへのポインタを含んでいるので、位置iにメンバを挿入します。

  • 新しいメンバーを指すように、メンバーi-1のポインターを更新します。
  • 新しいメンバのポインタをメンバiを指すように設定します。

リンクリストの短所は、ランダムアクセスが不可能なことです。メンバーにアクセスするには、目的のメンバーが見つかるまでリストをトラバースする必要があります。

115
b3.

ListとLinkedListの違いは、基になる実装にあります。リストは配列ベースのコレクション(ArrayList)です。 LinkedListは、ノードポインタベースのコレクション(LinkedListNode)です。 APIレベルの使用法では、どちらもICollection、IEnumerableなどの同じインターフェイスセットを実装しているため、どちらもほぼ同じです。

パフォーマンスが重要な場合は、主な違いが生じます。たとえば、 "INSERT"操作が多いリストを実装している場合、LinkedListはListよりも優れています。 LinkedListはO(1)時間内にそれを行うことができるので、Listは基礎となる配列のサイズを拡張する必要があるかもしれません。詳細情報や詳細については、LinkedListと配列データ構造のアルゴリズムの違いについてお読みください。 http://ja.wikipedia.org/wiki/Linked_list and Array

この助けを願って、

17
user23117

私の前の答えは十分に正確ではありませんでした。本当に恐ろしいことでした。Dしかし、今ではもっと有用で正しい答えを投稿することができます。


私はいくつか追加のテストをしました。以下のリンクからソースを見つけて、あなた自身の環境でそれを再チェックすることができます。 https://github.com/ukushu/DataStructuresTestsAndOther.git

簡単な結果:

  • 配列は使用する必要があります:

    • できるだけ頻繁に。それは速く、同じ量の情報に対して最小のRAMの範囲を取ります。
    • 正確な細胞数がわかっている場合
    • データが85000 b未満の配列に保存されている場合
    • 必要に応じて高いランダムアクセス速度
  • リストは使用する必要があります:

    • セルをリストの末尾に追加する必要がある場合(多くの場合)
    • リストの先頭/中央にセルを追加する必要がある場合(NOT OFTEN)
    • データが85000 b未満の配列に保存されている場合
    • 必要に応じて高いランダムアクセス速度
  • LinkedListは使用する必要があります:

    • リストの先頭/中央/末尾にセルを追加する必要がある場合(多くの場合)
    • 順次アクセス(順方向/逆方向)のみが必要な場合
    • あなたがより大きなアイテムを保存する必要があるが、アイテム数が少ない場合。
    • リンクに追加のメモリを使用するため、大量のアイテムには使用しないでください。

詳細:

введите сюда описание изображения知っているとおもしろい:

  1. 内部的にはLinkedList<T>は.NETのリストではありません。 IList<T>も実装されていません。そしてそれが、欠けているインデックスとインデックスに関連するメソッドがある理由です。

  2. LinkedList<T>はノードポインタベースのコレクションです。 .NETでは、二重リンクの実装になっています。これは、前/次の要素が現在の要素にリンクしていることを意味します。そしてデータは断片化されています - 異なるリストオブジェクトはRAMの異なる場所に配置することができます。また、LinkedList<T>またはArrayよりもList<T>に使用されるメモリが多くなります。

  3. .NETのList<T>は、Javaの代わりのArrayList<T>です。これはこれが配列ラッパーであることを意味します。そのため、連続した1つのデータブロックとしてメモリに割り当てられます。割り当てられたデータサイズが85000バイトを超えると、ラージオブジェクトヒープに移動されます。サイズによっては、これがヒープの断片化を引き起こす可能性があります(軽度の形式のメモリリーク)。しかし、同時にサイズ<85000バイトの場合 - これはメモリ内で非常にコンパクトで高速アクセスの表現を提供します。

  4. ランダムアクセスのパフォーマンスとメモリ消費のためには単一の連続ブロックが好まれますが、サイズを規則的に変更する必要があるコレクションの場合は、通常Arrayなどの構造を新しい場所にコピーする必要があります。ノードを削除しました。

15
Andrew

配列に対するリンクリストの主な利点は、リンクによってアイテムを効率的に並べ替えることができることです。 Sedgewick、p。 91

11
Dr. Alrawi

LinkedListを使用する一般的な状況は次のとおりです。

例えば100,000と言う大きなサイズの文字列のリストから多くの特定の文字列を削除したいとします。削除する文字列はHashSetのdicで調べることができ、文字列のリストには削除する文字列が30,000から60,000の間に含まれると考えられています。

それでは、10万個の文字列を格納するのに最適な種類のリストは何ですか?答えはLinkedListです。それらがArrayListに格納されている場合、それを反復処理し、一致したStringsを削除すると最大で数十億の操作が必要になりますが、イテレータと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();
}
2
Tom

組み込みのインデックス付きアクセス、ソート(およびこのバイナリ検索の後)、および "ToArray()"メソッドが必要な場合は、Listを使用してください。

2
Michael Damatov

これは Tono Nam の受け入れられた答えがその中のいくつかの間違った測定を修正することから適応されています。

テスト:

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)ランダムアクセスのようにパフォーマンスが良くなる他の分野があります。

1
nawfal

基本的に、.NETのList<>配列のラッパーです。 LinkedList<>リンクリスト。問題は配列とリンクリストの違い、そしてリンクリストの代わりに配列を使うべきなのはいつかということです。どちらを使用するかの決定において、おそらく最も重要な2つの要因は次のようになります。

  • 挿入/削除がコレクションの最後の要素にない限り、リンクリストの挿入/削除のパフォーマンスは大幅に向上します。これは、配列が挿入/削除ポイントの後にある残りのすべての要素をシフトしなければならないためです。ただし、挿入/削除がリストの末尾にある場合は、このシフトは不要です(ただし、容量を超えている場合は、配列のサイズを変更する必要があります)。
  • 配列ははるかに優れたアクセス機能を持っています。配列は直接インデックスを付けることができます(一定時間)。リンクリストはトラバースする必要があります(線形時間)。
1
iliketocode

LinkedList<>を使用する場合

  1. 水門を通過するオブジェクトの数はわかりません。たとえば、Token Stream
  2. あなたが最後に削除\挿入したいだけのとき。

それ以外の場合は、List<>を使用することをお勧めします。

0
Antony Thomas

私は上記の点の大部分に同意します。そして私は、Listがほとんどの場合、もっと明白な選択のように見えることにも同意します。

しかし、LinkedListがListよりもはるかに優れた効率のための選択である多くの例があることを付け加えたいと思います。

  1. あなたが要素を調べていて、たくさんの挿入/削除を実行したいとします。 LinkedListは線形O(n)時間でそれを行いますが、Listは二次O(n ^ 2)時間でそれを行います。
  2. もっと大きなオブジェクトに何度もアクセスしたいとすると、LinkedListはさらに便利になります。
  3. Deque()とqueue()はLinkedListを使ってより良く実装されています。
  4. LinkedListのサイズを大きくすることは、より大きな、そしてより大きなオブジェクトを扱うようになれば、はるかに簡単でより良いものになります。

誰かがこれらのコメントが役に立つことを願ってください。