Коли краще використовувати Список проти LinkedList ?
Коли краще використовувати Список проти 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, але мені здається, що ви додаєте вдвічі більше темп-об’єктів у самі петлі. (І коли я ще раз перевіряю себе тестовим додатком , напевно, вдвічі більше у LinkedList.)
I say never use a linkedList.
є хибними, як виявляється у подальшій публікації. Ви можете відредагувати його. 2) Які ви терміни? Ігнорування, додавання та перерахування цілком за один крок? Здебільшого, опис та перерахування - це не те, про що хвилюються ppl, це один час. Конкретні терміни вставок та доповнень дали б кращу ідею. 3) Найголовніше, що ви додаєте більше, ніж потрібно, до пов’язаного списку. Це неправильне порівняння. Поширює неправильну думку про пов’язаний список.
У більшості випадків List<T>
корисніше. LinkedList<T>
буде менше витрат при додаванні / видаленні елементів у середині списку, тоді як List<T>
можна лише дешево додавати / видаляти в кінці списку.
LinkedList<T>
це лише найефективніше, якщо ви отримуєте доступ до послідовних даних (або вперед, або назад) - випадковий доступ є відносно дорогим, оскільки він повинен щоразу ходити по ланцюгу (отже, чому він не має індексатора). Однак, оскільки а 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 APILinkedList<T>
більш громіздкий у використанні, і деталі його внутрішніх потреб випливають у ваш код.
Однак якщо вам потрібно зробити багато вставок / видалень із списку, він пропонує постійний час. List<T>
пропонує лінійний час, оскільки додаткові елементи у списку повинні бути перемішані навколо після вставки / видалення.
Пов'язані списки забезпечують дуже швидке вставлення або видалення члена списку. Кожен член у зв'язаному списку містить вказівник на наступного члена у списку, щоб вставити член у позиції i:
Недоліком пов'язаного списку є те, що випадковий доступ неможливий. Доступ до члена вимагає проходження списку до тих пір, поки бажаний член не буде знайдений.
Моя попередня відповідь була недостатньо точною. Як справді це було жахливо: D Але тепер я можу розмістити набагато кориснішу і правильнішу відповідь.
Я зробив кілька додаткових тестів. Ви можете знайти його джерело, перейшовши за наступним посиланням та повторно перевірити його у своєму оточенні: https://github.com/ukushu/DataStructuresTestsAndOther.git
Короткі результати:
Масив потрібно використовувати:
Список потрібно використовувати:
LinkedList потрібно використовувати:
Детальніше:
LinkedList<T>
внутрішньо не є списком у .NET. Це навіть не реалізує IList<T>
. І тому відсутні індекси та методи, пов’язані з індексами.
LinkedList<T>
це колекція на основі вузла. У .NET він знаходиться в подвійній взаємодії. Це означає, що попередні / наступні елементи мають посилання на поточний елемент. А дані фрагментовані - різні об'єкти списку можуть розташовуватися в різних місцях ОЗУ. Також буде використано більше пам'яті, LinkedList<T>
ніж для List<T>
Array або Array.
List<T>
у .Net - альтернатива Java Java ArrayList<T>
. Це означає, що це обгортка масиву. Таким чином, він виділяється в пам'яті як один суміжний блок даних. Якщо розмір виділених даних перевищує 85000 байт, він буде переміщений у групу Big Object Heap. Залежно від розміру, це може призвести до роздроблення купи (легка форма витоку пам'яті). Але в той же час, якщо розмір <85000 байт - це забезпечує дуже компактне і швидке представлення в пам'яті.
Єдиний суміжний блок є кращим для продуктивності довільного доступу та споживання пам’яті, але для колекцій, яким потрібно регулярно змінювати розмір, таку структуру, як масив, як правило, потрібно копіювати в нове місце, тоді як пов'язаний список потребує лише керування пам'яттю для щойно вставленого / видалені вузли.
Різниця між List та LinkedList полягає в їх основній реалізації. Список - це колекція на основі масиву (ArrayList). LinkedList - це колекція на основі вузла (LinkedListNode). За рівнем використання API обидва вони майже однакові, оскільки обидва реалізують один і той же набір інтерфейсів, таких як ICollection, IEnumerable тощо.
Ключова різниця виникає, коли продуктивність має значення. Наприклад, якщо ви реалізуєте список, який має важку операцію "INSERT", LinkedList перевершує список. Оскільки LinkedList може це зробити за O (1), але Список може знадобитися розширити розмір базового масиву. Для отримання додаткової інформації / деталей ви можете прочитати алгоритмічну різницю між структурами даних LinkedList та масивом. http://en.wikipedia.org/wiki/Linked_list та масив
Сподіваюся, що це допоможе,
Add
завжди в кінці існуючого масиву. List
є "досить хорошим" у цьому, навіть якщо не O (1). Серйозна проблема виникає, якщо вам потрібно багато Add
с, які не в кінці. Марк вказує, що необхідність переміщувати наявні дані щоразу, коли ви вставляєте (не тільки коли потрібен розмір), є більш значною вартістю продуктивності List
.
Основна перевага пов'язаних списків над масивами полягає в тому, що посилання надають нам можливість ефективно переставляти елементи. Седжевік, с. 91
Поширена обставина для використання LinkedList така:
Припустимо, ви хочете видалити багато певних рядків зі списку рядків великого розміру, скажімо, 100 000. Рядки, які потрібно видалити, можна шукати в Dash HashSet, і, як вважається, список рядків містить від 30 000 до 60 000 таких рядків для видалення.
Тоді який найкращий тип списку для зберігання 100 000 рядків? Відповідь - LinkedList. Якщо вони зберігаються в ArrayList, то повторення над ним та видалення відповідних рядків може зайняти до мільярдів операцій, тоді як за допомогою ітератора та методу delete () потрібно близько 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 є обгорткою над масивом . A LinkedList<>
- це пов'язаний список . Отже, питання зводиться до того, яка різниця між масивом і пов'язаним списком, і коли слід використовувати масив замість пов'язаного списку. Напевно, два найважливіші фактори, у вашому рішенні яких використовувати, зводиться до:
Це адаптовано від 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) випадковий доступ.
Використовуйте LinkedList<>
коли
Token Stream
.Для всього іншого краще використовувати List<>
.
LinkedListNode<T>
предмет вставок / видалень, якщо ви відстежуєте об'єкти у своєму коді. Якщо ви можете це зробити, то це набагато краще, ніж використовувати List<T>
, особливо для дуже довгих списків, де вставки / видалення часті.
node.Value
коли потрібно оригінальний елемент). Отже, ви перезаписуєте алгоритм для роботи з вузлами, а не необробленими значеннями.
Я згоден з більшістю пункту, викладеного вище. І я також погоджуюся, що Ліст виглядає як більш очевидний вибір у більшості випадків.
Але я просто хочу додати, що є багато випадків, коли LinkedList набагато кращий вибір, ніж Список для кращої ефективності.
Сподіваюся, хтось вважатиме ці коментарі корисними.
Стільки середніх відповідей тут ...
Деякі реалізації пов'язаних списків використовують основні блоки попередньо виділених вузлів. Якщо вони цього не роблять, постійний час / лінійний час є менш актуальним, оскільки продуктивність пам’яті буде поганою, а продуктивність кешу - ще гіршою.
Використовуйте пов'язані списки, коли
1) Ви хочете захистити нитку. Ви можете створити кращі альго-безпечні нитки. Блокування витрат буде домінувати в паралельному списку стилів.
2) Якщо у вас є велика структура, схожа на структуру черги, і ви хочете видаляти або додавати будь-де, але кінець, весь час. > 100К списків існує, але вони не такі поширені.
Я задав аналогічне запитання, пов'язане з виконанням колекції LinkedList , і виявив , що реалізація С # Стівена Клірі в Deque була рішенням. На відміну від колекції черги, Deque дозволяє переміщувати предмети спереду та ззаду. Він схожий на пов'язаний список, але з покращеною продуктивністю.
Deque
це «схоже на зв'язаний список, але з поліпшеними характеристиками» . Будь ласка кваліфікуватися це твердження: Deque
краще продуктивність , ніж LinkedList
, для конкретного коду . Переходячи за вашим посиланням, я бачу, що через два дні ви дізналися від Івана Стоєва, що це не неефективність LinkedList, а неефективність вашого коду. (І навіть якби це була неефективність LinkedList, це не виправдовувало б загального твердження про те, що Deque є більш ефективним; лише в конкретних випадках.)