Когда лучше использовать 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<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);
в последних двух примерах LinkedList? Я делаю это один раз перед циклом, как list.AddLast(new Temp(1,1,1,1));
в следующем за последним LinkedList, но мне кажется, что вы добавляете вдвое больше объектов Temp в самих циклах. (И когда я дважды проверяю себя в тестовом приложении , конечно же, вдвое больше, чем в LinkedList.)
I say never use a linkedList.
ошибочен, как показывает ваш последующий пост. Вы можете отредактировать его. 2) Какое у вас время? Инстанцирование, сложение и перечисление всего за один шаг? Главным образом, создание экземпляров и перечисление - это не то, о чем беспокоятся люди, это одноразовые шаги. В частности, сроки вставки и дополнения даст лучшую идею. 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[]
потерпит неудачу за то, что он слишком длинный (все одна плита), LinkedList<T>
будет плачем за то, что он слишком гранулированный (плита за элемент).
Представление связанного списка как списка может быть немного обманчивым. Это больше похоже на цепь. На самом деле в .NET LinkedList<T>
даже не реализуют IList<T>
. В связанном списке нет реальной концепции индекса, хотя может показаться, что она есть. Конечно, ни один из методов, предоставленных в классе, не принимает индексы.
Связанные списки могут быть односвязными или двусвязными. Это относится к тому, имеет ли каждый элемент в цепочке ссылку только на следующий (односвязный) или на оба предыдущих / следующих элемента (двусвязный). 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()
линейное времяКак видите, они в основном эквивалентны. На практике API LinkedList<T>
более громоздок в использовании, и детали его внутренних потребностей выливаются в ваш код.
Однако, если вам нужно сделать много вставок / удалений из списка, он предлагает постоянное время. List<T>
предлагает линейное время, так как дополнительные элементы в списке должны быть перемешаны после вставки / удаления.
Связанные списки обеспечивают очень быструю вставку или удаление члена списка. Каждый член в связанном списке содержит указатель на следующий член в списке, поэтому для вставки члена в позицию i:
Недостатком связанного списка является то, что произвольный доступ невозможен. Доступ к члену требует обхода списка, пока не будет найден нужный член.
Мой предыдущий ответ был недостаточно точным. Как верно было ужасно: D Но теперь я могу опубликовать гораздо более полезный и правильный ответ.
Я сделал несколько дополнительных тестов. Вы можете найти его источник по следующей ссылке и заново проверить его в своей среде: https://github.com/ukushu/DataStructuresTestsAndOther.git
Краткие результаты:
Массив нужно использовать:
Список нужно использовать:
LinkedList нужно использовать:
Больше деталей:
LinkedList<T>
внутренне это не список в .NET. Это даже не реализовать IList<T>
. И именно поэтому отсутствуют индексы и методы, связанные с индексами.
LinkedList<T>
коллекция на основе указателя узла. В .NET это в двусвязной реализации. Это означает, что предыдущие / следующие элементы имеют ссылку на текущий элемент. И данные фрагментированы - разные объекты списка могут быть расположены в разных местах оперативной памяти. Также будет использовано больше памяти, LinkedList<T>
чем для List<T>
или для массива.
List<T>
в .Net является альтернативой Java ArrayList<T>
. Это означает, что это оболочка массива. Таким образом, он выделяется в памяти как один непрерывный блок данных. Если выделенный размер данных превышает 85000 байт, он будет перемещен в кучу больших объектов. В зависимости от размера это может привести к фрагментации кучи (легкая форма утечки памяти). Но в то же время, если размер <85000 байт - это обеспечивает очень компактное и быстрое представление в памяти.
Один непрерывный блок предпочтителен для производительности произвольного доступа и потребления памяти, но для коллекций, которым необходимо регулярно менять размер, структуру, такую как массив, обычно необходимо копировать в новое место, тогда как связанный список должен управлять только памятью для вновь вставленного / удаленные узлы.
Разница между List и LinkedList заключается в их базовой реализации. Список - это массив на основе массива (ArrayList). LinkedList - это основанная на указателе узла коллекция (LinkedListNode). Что касается использования уровня API, то оба они в значительной степени одинаковы, поскольку оба реализуют один и тот же набор интерфейсов, таких как ICollection, IEnumerable и т. Д.
Ключевое различие возникает, когда производительность имеет значение. Например, если вы реализуете список со сложной операцией «INSERT», LinkedList превосходит List. Поскольку LinkedList может сделать это за O (1) раз, но List может потребоваться расширить размер базового массива. Для получения дополнительной информации вы можете прочитать об алгоритмическом различии между LinkedList и структурами данных массива. http://en.wikipedia.org/wiki/Linked_list и Array
Надеюсь, это поможет,
Add
всегда в конце существующего массива. List
достаточно «хорош», даже если не O (1). Серьезная проблема возникает, если вам нужно много Add
s, которых нет в конце. Марк отмечает, что необходимость перемещать существующие данные каждый раз, когда вы вставляете (а не только когда требуется изменение размера), является более существенной потерей производительности List
.
Основное преимущество связанных списков над массивами состоит в том, что ссылки дают нам возможность эффективно переупорядочивать элементы. Седжвик, р. 91
Обычное обстоятельство использования LinkedList выглядит следующим образом:
Предположим, вы хотите удалить много определенных строк из списка строк большого размера, скажем, 100 000. Строки для удаления можно найти в HashSet dic, и список строк, как полагают, содержит от 30000 до 60000 таких строк для удаления.
Тогда какой тип списка лучше всего подходит для хранения 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 для создания второго списка. Однако использование LinkedList
здесь приводит к значительному увеличению объема памяти по сравнению с другими типами коллекций, а потеря локальности памяти означает, что итерация будет заметно медленнее, что делает его немного хуже, чем a List
.
RemoveAll
эквивалент в Java.
RemoveAll
недоступно для List
, вы можете выполнить алгоритм «уплотнения», который будет выглядеть как цикл Тома, но с двумя индексами и необходимостью перемещать элементы, которые будут храниться по одному во внутреннем массиве списка. Эффективность равна O (n), так же, как алгоритм Тома для LinkedList
. В обеих версиях время для вычисления ключа HashSet для строк доминирует. Это не хороший пример того, когда использовать LinkedList
.
Когда вам нужен встроенный индексированный доступ, сортировка (и после этого двоичного поиска) и метод «ToArray ()», вы должны использовать List.
По сути, List<>
в .NET это обертка над массивом . А LinkedList<>
является связанным списком . Таким образом, вопрос сводится к тому, какова разница между массивом и связанным списком, и когда следует использовать массив вместо связанного списка. Вероятно, два наиболее важных фактора в вашем решении, которые следует использовать, сводятся к следующему:
Это адаптировано из принятого ответа Тоно Нама, исправляющего несколько неправильных измерений в нем.
Тест:
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) Если у вас есть большие очереди, подобные структурам, и вы хотите удалить или добавить где угодно, но конец всегда. > 100К списков существует, но не так часто.
Я спросил похожий вопрос, связанный с производительностью коллекции LinkedList , и обнаружил, что реализация Deque Стивена Клири на C # была решением. В отличие от коллекции Queue, Deque позволяет перемещать предметы вперед / назад вперед и назад. Это похоже на связанный список, но с улучшенной производительностью.
Deque
которое «похоже на связанный список, но с улучшенной производительностью» . Пожалуйста квалифицироваться это утверждение: Deque
лучше производительность , чем LinkedList
, для конкретного кода . Перейдя по вашей ссылке, я вижу, что через два дня вы узнали от Ивана Стоева, что это не неэффективность LinkedList, а неэффективность вашего кода. (И даже если бы это был неэффективный LinkedList, это не оправдывало бы общее утверждение, что Deque более эффективен; только в определенных случаях.)