Когда мне следует использовать список против LinkedList

Когда лучше использовать List(Of T) vs a LinkedList(Of T)?

Ответ 1

Изменить

Прочтите комментарии к этому ответу. Люди утверждают, что я не делал надлежащие тесты. Я согласен, что это не должно быть приемлемым ответом. Как я был Я провела несколько тестов и почувствовала, как их разделять.

Оригинальный ответ...

Я нашел интересные результаты:

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

Даже если вы только получаете доступ к данным, он значительно медленнее! Я говорю, никогда не используйте связанный список.




Вот еще одно сравнение, выполняющее множество вставок (мы планируем вставить элемент в середине списка)

Связанный список (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;

Итак, только если вы планируете вставлять несколько элементов, а также, где-то есть ссылка, где вы планируете вставить элемент, а затем использовать связанный список. Просто потому, что вам нужно вставить множество элементов, это не ускоряет работу, поскольку поиск места, в который вы хотите вставить, требует времени.

Ответ 2

В большинстве случаев List<T> более полезен. LinkedList<T> будет иметь меньшую стоимость при добавлении/удалении элементов в середине списка, тогда как List<T> может только дешево добавить/удалить в конце списка.

LinkedList<T> работает только в том случае, если вы получаете доступ к последовательным данным (либо вперед, либо назад) - случайный доступ является относительно дорогостоящим, так как он должен каждый раз ходить по цепочке (следовательно, у него нет индексатора). Однако, поскольку List<T> по существу является просто массивом (с оберткой), то произвольный доступ в порядке.

List<T> также предлагает множество методов поддержки - Find, ToArray и т.д.; однако они также доступны для LinkedList<T> с .NET 3.5/С# 3.0 с помощью методов расширения - так что это менее важно.

Ответ 3

Мышление связанного списка в виде списка может немного ввести в заблуждение. Это больше похоже на цепочку. На самом деле, в .NET, LinkedList<T> даже не реализует IList<T>. В связанном списке нет реальной концепции индекса, хотя может показаться, что есть. Конечно, ни один из методов, предоставляемых в классе, не принимает индексы.

Связанные списки могут быть связаны по отдельности или дважды связаны. Это относится к тому, имеет ли каждый элемент в цепочке ссылку только на следующую (односвязную) или на оба предшествующих/следующих элемента (дважды связанную). LinkedList<T> имеет двойную связь.

Внутренне, List<T> поддерживается массивом. Это обеспечивает очень компактное представление в памяти. И наоборот, LinkedList<T> включает дополнительную память для хранения двунаправленных ссылок между последовательными элементами. Таким образом, размер памяти LinkedList<T> обычно будет больше, чем для List<T> (с оговоркой, что List<T> может иметь неиспользуемые внутренние элементы массива для повышения производительности во время операций добавления).

У них также разные характеристики:

Append

  • LinkedList<T>.AddLast(item) постоянное время
  • List<T>.Add(item) амортизированное постоянное время, линейный наихудший случай

Prepend

  • 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) линейное время

Count

  • 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> предлагает линейное время, так как дополнительные элементы в списке должны быть перемешаны после вставки/удаления.

Ответ 4

Связанные списки обеспечивают очень быструю вставку или удаление члена списка. Каждый член в связанном списке содержит указатель на следующий член в списке, чтобы вставить элемент в позицию i:

  • обновить указатель в элементе i-1, чтобы указать на новый элемент
  • установите указатель в новом члене, чтобы указать член i

Недостатком связанного списка является то, что случайный доступ невозможен. Доступ к члену требует прохождения списка до тех пор, пока не будет найден нужный элемент.

Ответ 5

Разница между списком и LinkedList заключается в их основной реализации. Список представляет собой массив на основе массива (ArrayList). LinkedList - это коллекция node -interinter based (LinkedListNode). При использовании уровня API оба они почти одинаковы, поскольку оба реализуют один и тот же набор интерфейсов, таких как ICollection, IEnumerable и т.д.

Ключевое различие возникает, когда производительность имеет значение. Например, если вы реализуете список с тяжелой операцией "INSERT", LinkedList превосходит List. Поскольку LinkedList может сделать это в O (1) раз, но List может потребоваться расширить размер базового массива. Для получения дополнительной информации/подробностей вам может потребоваться ознакомиться с алгоритмической разницей между LinkedList и структурами данных массива. http://en.wikipedia.org/wiki/Linked_list и Array

Надеюсь на эту помощь,

Ответ 6

Основным преимуществом связанных списков по массивам является то, что ссылки предоставляют нам возможность эффективно перестраивать элементы. Sedgewick, p. 91

Ответ 7

Мой предыдущий ответ был недостаточно точным. Как это было ужасно: D Но теперь я могу опубликовать гораздо более полезный и правильный ответ.


Я сделал несколько дополнительных тестов. Вы можете найти его источник по следующей ссылке и переустановить его в своей среде по своему усмотрению: https://github.com/ukushu/DataStructuresTestsAndOther.git

Короткие результаты:

  • Массив должен использовать:

    • Так часто, насколько это возможно. Он быстро и занимает наименьший объем оперативной памяти для информации о том же объеме.
    • Если вы знаете точное количество необходимых ячеек
    • Если данные, сохраненные в массиве < 85000 b
    • Если требуется высокая скорость произвольного доступа
  • Список должен использовать:

    • Если необходимо добавить ячейки в конец списка (часто)
    • Если необходимо добавить ячейки в начало/середину списка (NOT OFTEN)
    • Если данные, сохраненные в массиве < 85000 b
    • Если требуется высокая скорость произвольного доступа
  • LinkedList должен использовать:

    • Если необходимо добавить ячейки в начало/середину/конец списка (часто)
    • При необходимости только последовательный доступ (вперед/назад)
    • Если вам нужно сохранить LARGE-элементы, но количество элементов будет низким.
    • Лучше не использовать для большого количества элементов, так как он использует дополнительную память для ссылок.

Подробнее:

введите сюда описание изображения Интересно знать:

  • Связанный список внутри не является списком в .NET. LinkedList<T>. Он даже не реализует IList<T>. И поэтому отсутствуют индексы и методы, связанные с индексами.

  • LinkedList<T> представляет собой коллекцию node -interinter. В .NET это связано с двойной связью. Это означает, что предыдущие/следующие элементы имеют ссылку на текущий элемент. И данные фрагментированы - разные объекты списка могут быть расположены в разных местах ОЗУ. Кроме того, для LinkedList<T> будет больше памяти, чем для List<T> или массива.

  • List<T> в .Net является альтернативой Java ArraList<T>. Это означает, что это оболочка массива. Таким образом, он выделяется в памяти как один непрерывный блок данных. Если выделенный размер данных превышает 85000 байт, он будет выделен как часть большой кучи объектов. В зависимости от размера это может привести к фрагментации кучи, легкой утечке памяти. Но в то же время, если размер < 85000 байт - это обеспечивает очень компактное представление с быстрым доступом в памяти.

  • Единственный непрерывный блок предпочтительнее для производительности произвольного доступа и потребления памяти, но для коллекций, которые должны регулярно менять размер, структура, такая как массив, обычно должна быть скопирована в новое место, тогда как связанный список должен управлять только память для вновь вставленных/удаленных узлов.

Ответ 8

Общим для использования LinkedList является следующее:

Предположим, вы хотите удалить многие строки из списка строк большого размера, скажем, 100 000. Строки для удаления можно найти в HashSet dic, и, как считается, список строк содержит от 30 000 до 60 000 таких строк для удаления.

Тогда какой лучший тип списка для хранения 100 000 строк? Ответ: LinkedList. Если они хранятся в ArrayList, то итерация по ней и удаление совпадающих строк, которые будут занимать к миллиардам операций, тогда как требуется около 100 000 операций с использованием итератора и метода remove().

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();
}

Ответ 9

Если вам нужен встроенный индексированный доступ, сортировка (и после этого двоичного поиска) и метод ToArray(), вы должны использовать List.

Ответ 10

Это адаптировано из 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 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) произвольный доступ.

Ответ 11

Используйте LinkedList<>, когда

  • Вы не знаете, сколько объектов проходит через шлюз. Например, Token Stream.
  • Когда вы ТОЛЬКО хотите удалить \insert в конце.

Для всего остального лучше использовать List<>.

Ответ 12

Здесь так много средних ответов...

В некоторых реализациях связанных списков используются базовые блоки предварительно выделенных узлов. Если они этого не делают, то постоянное время/линейное время менее важно, так как производительность памяти будет плохой, а производительность кэша еще хуже.

Используйте связанные списки, когда

1) Вам нужна безопасность потоков. Вы можете создавать улучшенные поточные альги. Затраты на блокировку будут доминировать в параллельном списке стилей.

2) Если у вас большая структура, похожая на очередь, и вы хотите удалить или добавить где угодно, кроме конца все время. > 100K-списки существуют, но не так распространены.

Ответ 13

Я спросил аналогичный вопрос, связанный с производительностью коллекции LinkedList, и обнаружил Стивен Клири Реализация С# для Deque была решением. В отличие от коллекции Queue, Deque позволяет перемещать элементы взад и вперед спереди и сзади. Он похож на связанный список, но с улучшенной производительностью.