Когда лучше использовать List(Of T)
vs a LinkedList(Of T)
?
Когда мне следует использовать список против LinkedList
Ответ 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 является альтернативой JavaArraList<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 позволяет перемещать элементы взад и вперед спереди и сзади. Он похож на связанный список, но с улучшенной производительностью.