Коли я повинен використовувати Список проти LinkedList


392

Коли краще використовувати Список проти LinkedList ?


3
Java q , не повинно бути дуже різним.
nawfal

1
@ jonathan-allen, будь ласка, подумайте про зміну прийнятої відповіді. Нинішній неточний і вкрай оманливий.
Xpleria

Відповіді:


107

Редагувати

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

Оригінальна відповідь ...

Я знайшов цікаві результати:

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

Тому лише якщо ви плануєте вставити кілька елементів, а також десь у вас є посилання на те, куди ви плануєте вставити елемент, тоді використовуйте пов'язаний список. Тільки тому, що вам потрібно вставити багато предметів, це не зробить це швидше, тому що пошук місця, куди ви хочете вставити, потребує часу.


99
Існує одна перевага для LinkedList over List (це специфічна .net): оскільки Список підтримується внутрішнім масивом, він розподіляється в одному суміжному блоці. Якщо розміщений блок перевищує розмір 85000 байт, він буде виділений на великій купі об'єктів, генеруючий генератор. Залежно від розміру, це може призвести до роздроблення купи, легкої форми витоку пам'яті.
JerKimball

35
Зауважте, що якщо ви готуєте багато (як ви це робите в останньому прикладі) або видаляєте перший запис, пов'язаний список майже завжди буде значно швидшим, оскільки не потрібно робити пошук чи переміщення / копіювання. Список потребує переміщення всього місця, щоб розмістити новий елемент, зробивши попередньо операцію O (N).
cHao

6
Чому в list.AddLast(a);останні пет прикладах LinkedList? Я роблю це один раз перед циклом, як і list.AddLast(new Temp(1,1,1,1));в наступному до останнього LinkedList, але мені здається, що ви додаєте вдвічі більше темп-об’єктів у самі петлі. (І коли я ще раз перевіряю себе тестовим додатком , напевно, вдвічі більше у LinkedList.)
ruffin

7
Я відповів на цю відповідь. 1) ваші загальні поради I say never use a linkedList.є хибними, як виявляється у подальшій публікації. Ви можете відредагувати його. 2) Які ви терміни? Ігнорування, додавання та перерахування цілком за один крок? Здебільшого, опис та перерахування - це не те, про що хвилюються ppl, це один час. Конкретні терміни вставок та доповнень дали б кращу ідею. 3) Найголовніше, що ви додаєте більше, ніж потрібно, до пов’язаного списку. Це неправильне порівняння. Поширює неправильну думку про пов’язаний список.
nawfal

47
Вибачте, але ця відповідь справді погана. Будь ласка, НЕ слухайте цю відповідь. Причина в двох словах: Повністю помилково вважати, що реалізовані списки реалізацій списку досить дурні, щоб змінити розмір масиву для кожної вставки. Зв'язані списки, природно, повільніше, ніж списки, підтримувані масивом, при проходженні, а також при вставці на будь-якому кінці, оскільки лише їм потрібно створювати нові об'єкти, тоді як списки, підтримувані масивом, використовують буфер (в обох напрямках, очевидно). Саме (погано зроблені) орієнтири вказують саме на це. Відповідь повністю не дозволяє перевірити випадки, коли перелічені списки є кращими!
мафу

277

У більшості випадків List<T>корисніше. LinkedList<T>буде менше витрат при додаванні / видаленні елементів у середині списку, тоді як List<T>можна лише дешево додавати / видаляти в кінці списку.

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

List<T>також пропонує багато методів підтримки - Find, ToArrayі т.д .; однак вони також доступні для LinkedList<T>.NET 3.5 / C # 3.0 за допомогою методів розширення - так що це не є чинником.


4
Одна з переваг List <> vs. LinkedList <>, що я ніколи не замислювався над питанням того, як мікропроцесори реалізують кешування пам'яті. Хоча я не розумію це повністю, автор цієї статті в блозі багато говорить про "місцевості довідки", що робить проходження масиву набагато швидшим, ніж проходження пов'язаного списку, принаймні, якщо зв'язаний список став дещо фрагментованим у пам'яті . kjellkod.wordpress.com/2012/02/25/…
RenniePet

Список @RenniePet реалізований з динамічним масивом, а масиви - це суміжні блоки пам'яті.
Кейсі

2
Оскільки List - це динамічний масив, тому іноді добре вказувати ємність списку в конструкторі, якщо ви знаєте його заздалегідь.
Cardin Lee JH

Чи можливо, що реалізація всіх, масиву, списку <T> та LinkedList <T> C # є дещо неоптимальною для одного дуже важливого випадку: вам потрібен дуже великий список, додавання (AddLast) та послідовний обхід (в одному напрямку) абсолютно добре: я не хочу, щоб масив змінив розмір, щоб отримати безперервні блоки (чи гарантується це для кожного масиву, навіть масивів 20 ГБ?), і я не знаю заздалегідь розмір, але я можу заздалегідь здогадатися про розмір блоку, наприклад 100 МБ, щоб резервувати кожен раз заздалегідь. Це було б гарною реалізацією. Або масив / список подібний до цього, і я пропустив бал?
Філм

1
@Philm - це такий сценарій, коли ви пишете власний лайф над обраною стратегією блоку; List<T>і T[]не вдасться бути занадто грубим (усі одні плити), LinkedList<T>буде ридати за занадто зернисте (плита на елемент).
Марк Гравелл

212

Розгляд пов'язаного списку як переліку може бути дещо оманливим. Це більше схоже на ланцюжок. Насправді в .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>пропонує лінійний час, оскільки додаткові елементи у списку повинні бути перемішані навколо після вставки / видалення.


2
Чи постійний список лічильників? Я думав, що це буде лінійно?
Iain Ballard

10
@Iain, кількість рахунків кеширується в обох класах списку.
Дрю Ноакс

3
Ви писали, що "Список <T> .Додати (пункт) логарифмічний час", проте насправді це "Постійний", якщо ємність списку може зберігати новий елемент, і "Лінійний", якщо в списку не вистачає місця та нового бути перерозподіленим.
aStranger

@aStranger, звичайно, ти маєш рацію. Не впевнений, про що я думав у вищесказаному - можливо, амортизований нормальний час справи є логарифмічним, чого це не так. Насправді амортизований час є постійним. Я не потрапив у найкращий / гірший випадок операцій, прагнучи до простого порівняння. Я думаю, що операція додавання є достатньо вагомою для того, щоб надати цю деталь. Відредагуйте відповідь. Дякую.
Дрю Ноакс

1
@Philm, можливо, ви можете почати нове запитання, і ви не кажете, як ви будете використовувати цю структуру даних, коли будете побудовані, але якщо ви говорите мільйон рядків, вам може сподобатися якийсь гібрид (пов'язаний список фрагменти масиву або подібне), щоб зменшити фрагментацію купи, зменшити накладні витрати на пам'ять і уникати жодного величезного об'єкта на LOH.
Дрю Ноакс

118

Пов'язані списки забезпечують дуже швидке вставлення або видалення члена списку. Кожен член у зв'язаному списку містить вказівник на наступного члена у списку, щоб вставити член у позиції i:

  • оновіть вказівник у члені i-1, щоб вказати на нового члена
  • встановити покажчик у новому члені, щоб вказати на член i

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


6
Я додам, що пов’язані списки мають накладні дані на один елемент, що зберігається вище, через LinkedListNode, який посилається на попередній та наступний вузол. Окупність цього суміжного блоку пам'яті не потрібна для зберігання списку, на відміну від списку на основі масиву.
paulecoyote

3
Невже зазвичай не притаманні суміжні блоки пам'яті?
Джонатан Аллен

7
Так, суміжний блок є кращим для продуктивності доступу до випадкового доступу та споживання пам’яті, але для колекцій, яким потрібно регулярно змінювати розмір, структуру, таку як масив, як правило, потрібно копіювати в нове місце, тоді як пов'язаний список повинен лише керувати пам'яттю для щойно вставлені / видалені вузли.
jpierson

6
Якщо вам коли-небудь доводилося працювати з дуже великими масивами чи списками (список просто охоплює масив), ви почнете стикатися з проблемами пам’яті, хоча на вашій машині виявляється багато пам’яті. Список використовує стратегію подвоєння, коли він виділяє новий простір у своєму базовому масиві. Таким чином, повноцінний масив 1000000 елементів буде скопійовано у новий масив із 2000000 елементів. Цей новий масив потрібно створити у суміжному просторі пам'яті, який є достатньо великим, щоб вмістити його.
Андрій

1
У мене був конкретний випадок, коли все, що я робив, було додавання та вилучення, а також циклічне поодинці ... тут пов'язаний список набагато перевершував звичайний список ..
Пітер

26

Моя попередня відповідь була недостатньо точною. Як справді це було жахливо: D Але тепер я можу розмістити набагато кориснішу і правильнішу відповідь.


Я зробив кілька додаткових тестів. Ви можете знайти його джерело, перейшовши за наступним посиланням та повторно перевірити його у своєму оточенні: https://github.com/ukushu/DataStructuresTestsAndOther.git

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

  • Масив потрібно використовувати:

    • Так часто можливо. Це швидко і займає найменший діапазон оперативної пам’яті для тієї ж кількості інформації.
    • Якщо ви знаєте точний підрахунок комірок
    • Якщо дані збережені в масиві <85000 b (85000/32 = 2656 елементів для цілих даних)
    • При необхідності висока швидкість випадкового доступу
  • Список потрібно використовувати:

    • Якщо потрібно, щоб додати комірки до кінця списку (часто)
    • Якщо потрібно, щоб додати комірки на початку / середині списку (НЕ ОФТЕН)
    • Якщо дані збережені в масиві <85000 b (85000/32 = 2656 елементів для цілих даних)
    • При необхідності висока швидкість випадкового доступу
  • LinkedList потрібно використовувати:

    • Якщо потрібно, щоб додати комірки на початку / середині / в кінці списку (часто)
    • За потреби лише послідовний доступ (вперед / назад)
    • Якщо вам потрібно зберегти ВЕЛИКІ елементи, але кількість елементів невелика.
    • Краще не використовувати для великої кількості елементів, так як використовувати додаткову пам'ять для посилань.

Детальніше:

введіть сюда опис зображень Цікаво знати:

  1. LinkedList<T>внутрішньо не є списком у .NET. Це навіть не реалізує IList<T>. І тому відсутні індекси та методи, пов’язані з індексами.

  2. LinkedList<T>це колекція на основі вузла. У .NET він знаходиться в подвійній взаємодії. Це означає, що попередні / наступні елементи мають посилання на поточний елемент. А дані фрагментовані - різні об'єкти списку можуть розташовуватися в різних місцях ОЗУ. Також буде використано більше пам'яті, LinkedList<T>ніж для List<T>Array або Array.

  3. List<T>у .Net - альтернатива Java Java ArrayList<T>. Це означає, що це обгортка масиву. Таким чином, він виділяється в пам'яті як один суміжний блок даних. Якщо розмір виділених даних перевищує 85000 байт, він буде переміщений у групу Big Object Heap. Залежно від розміру, це може призвести до роздроблення купи (легка форма витоку пам'яті). Але в той же час, якщо розмір <85000 байт - це забезпечує дуже компактне і швидке представлення в пам'яті.

  4. Єдиний суміжний блок є кращим для продуктивності довільного доступу та споживання пам’яті, але для колекцій, яким потрібно регулярно змінювати розмір, таку структуру, як масив, як правило, потрібно копіювати в нове місце, тоді як пов'язаний список потребує лише керування пам'яттю для щойно вставленого / видалені вузли.


1
Питання: "Дані, збережені в масиві <або> 85 000 байт", ви маєте на увазі дані на масив / список ЕЛЕМЕНТ? Можна зрозуміти, що ви маєте на увазі розмір даних усього масиву ..
Філм,

Елементи масиву, розташовані послідовно в пам'яті. Так на масив. Я знаю про помилку в таблиці, пізніше я
Андрій

Якщо списки повільно вставляються, якщо список має багато поворотів (багато вставок / видалень), якщо пам'ять займає видалений простір, і якщо так, то чи робить це "повторно" введення швидше?
Роб

18

Різниця між List та LinkedList полягає в їх основній реалізації. Список - це колекція на основі масиву (ArrayList). LinkedList - це колекція на основі вузла (LinkedListNode). За рівнем використання API обидва вони майже однакові, оскільки обидва реалізують один і той же набір інтерфейсів, таких як ICollection, IEnumerable тощо.

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

Сподіваюся, що це допоможе,


4
Список <T> заснований на масиві (T []), а не на основі ArrayList. Повторне вставлення: зміна розміру масиву не є проблемою (алгоритм подвоєння означає, що більшість часу цього не потрібно робити): проблема полягає в тому, що він повинен спершу блокувати копіювати всі існуючі дані, що займає небагато час.
Марк Гравелл

2
@Marc, "алгоритм подвоєння" лише робить його O (logN), але це все ще гірше, ніж O (1)
Ілля Риженков

2
Моя думка полягала в тому, що біль не викликає розмір - це проміжок. Тож найгірший випадок, якщо ми щоразу додаємо перший (нульовий) елемент, тоді бліт повинен щоразу переміщувати все.
Марк Гравелл

@IlyaRyzhenkov - ви думаєте про випадок, коли Addзавжди в кінці існуючого масиву. Listє "досить хорошим" у цьому, навіть якщо не O (1). Серйозна проблема виникає, якщо вам потрібно багато Addс, які не в кінці. Марк вказує, що необхідність переміщувати наявні дані щоразу, коли ви вставляєте (не тільки коли потрібен розмір), є більш значною вартістю продуктивності List.
ToolmakerSteve

Проблема полягає в тому, що теоретичні великі позначення O не розповідають про всю історію. У галузі інформатики - це все, про кого будь-коли хвилюється, але в цьому реально перейматися набагато більше, ніж це в реальному світі.
MattE

11

Основна перевага пов'язаних списків над масивами полягає в тому, що посилання надають нам можливість ефективно переставляти елементи. Седжевік, с. 91


1
ІМО це має бути відповіддю. LinkedList використовуються, коли важливе гарантоване замовлення.
RBaarda

1
@RBaarda: Я не згоден. Це залежить від рівня, про який ми говоримо. Алгоритмічний рівень відрізняється від рівня машинного впровадження. Для розгляду швидкості вам потрібно і останнє. Як зазначається, масиви реалізуються як "один шматок" пам'яті, що є обмеженням, оскільки це може призвести до зміни розміру та реорганізації пам'яті, особливо з дуже великими масивами. Подумавши деякий час, спеціальна власна структура даних, пов'язаний список масивів була б однією ідеєю, щоб забезпечити кращий контроль над швидкістю лінійного заповнення та доступ до дуже великих структур даних.
Філм

1
@Philm - я підтримав ваш коментар, але я хотів би зазначити, що ви описуєте іншу вимогу. Відповідь говорить про те, що зв'язаний список має перевагу щодо продуктивності для алгоритмів, які передбачають багато перестановки елементів. Враховуючи це, я трактую коментар RBaarda як посилання на необхідність додавання / видалення елементів, постійно підтримуючи задане замовлення (критерії сортування). Тож не просто "лінійне наповнення". Враховуючи це, Список програє, оскільки індекси марні (змінюйте щоразу, коли ви додаєте елемент де-небудь, а не в кінці).
ToolmakerSteve

4

Поширена обставина для використання 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();
}

6
Ви можете просто використати RemoveAllдля видалення елементів з, Listне переміщуючи багато предметів навколо, або скористайтеся WhereLINQ для створення другого списку. LinkedListОднак використання тут, в кінцевому рахунку, споживає значно більше пам’яті, ніж інші типи колекцій, і втрата локальності пам’яті означає, що вона буде помітно повільніше перебирати, що робить її трохи гіршою, ніж a List.
Сервіс

@ Серві, зауважте, що у відповіді @ Тома використовується Java. Я не впевнений, чи є RemoveAllв Java еквівалент.
Артуро Торрес Санчес

3
@ ArturoTorresSánchez Ну, в цьому питанні конкретно сказано, що мова йде про .NET, так що просто робить відповідь набагато менш доцільною.
Сервіс

@Servy, тоді ти мав би згадати про це спочатку.
Артуро Торрес Санчес

Якщо RemoveAllце недоступно List, ви можете зробити алгоритм «ущільнення», який би виглядав як цикл Тома, але з двома індексами та необхідністю переміщення елементів, які зберігатимуться по одному вниз, у внутрішньому масиві списку. Ефективність O (n), така ж, як алгоритм Тома для LinkedList. В обох версіях час для обчислення ключа HashSet для рядків переважає. Це не гарний приклад, коли користуватися LinkedList.
ToolmakerSteve

2

Коли вам потрібен вбудований індексований доступ, сортування (і після цього двійкового пошуку) та метод "ToArray ()", ви повинні використовувати List.


2

По суті, List<>.NET є обгорткою над масивом . A LinkedList<> - це пов'язаний список . Отже, питання зводиться до того, яка різниця між масивом і пов'язаним списком, і коли слід використовувати масив замість пов'язаного списку. Напевно, два найважливіші фактори, у вашому рішенні яких використовувати, зводиться до:

  • Пов'язані списки мають набагато кращу ефективність вставки / видалення, доки вставки / видалення не є останнім елементом колекції. Це тому, що масив повинен зміщувати всі елементи, що залишилися після точки вставки / видалення. Якщо вставка / видалення знаходиться в кінці списку, цей зсув не потрібен (хоча масив може знадобитися змінити, якщо його ємність буде перевищена).
  • Масиви мають набагато кращі можливості доступу. Масиви можна індексувати безпосередньо (у постійний час). Пов'язані списки повинні пройти (лінійний час).

1

Це адаптовано від 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) випадковий доступ.


0

Використовуйте LinkedList<>коли

  1. Ви не знаєте, скільки об’єктів проходить через ворота повені. Наприклад,Token Stream .
  2. Коли ви ТІЛЬКИ хотіли видалити \ вставити на кінцях.

Для всього іншого краще використовувати List<>.


6
Я не бачу, чому пункт 2 має сенс. Пов'язані списки чудові, коли ви робите багато вставок / видалень у всьому списку.
Дрю Ноакс

Через те, що LinkedLists не базується на індексі, вам дійсно доведеться просканувати весь список на предмет вставки або видалення, що спричиняє штраф (O). З іншого боку, список <> страждає від зміни розміру масиву, але все-таки IMO є кращим варіантом порівняно з LinkedLists.
Антоній Томас

1
Вам не доведеться сканувати список на LinkedListNode<T>предмет вставок / видалень, якщо ви відстежуєте об'єкти у своєму коді. Якщо ви можете це зробити, то це набагато краще, ніж використовувати List<T>, особливо для дуже довгих списків, де вставки / видалення часті.
Дрю Ноакс

Ви маєте на увазі через хештейн? Якщо це так, то це був би типовий компроміс \ час та час, що кожен програміст повинен зробити вибір на основі проблемної області :) Але так, це зробило б це швидше.
Антоній Томас

1
@AntonyThomas - Ні, він має на увазі, обходячи посиланнями на вузли, а не передаючи посилання на елементи . Якщо все, що у вас є, це елемент , то і List, і LinkedList мають погані показники, тому що вам потрібно шукати. Якщо ви думаєте, "але зі списком я можу просто передати індекс": це справедливо лише тоді, коли ви ніколи не вставляєте новий елемент в середину списку. LinkedList не має цього обмеження, якщо ви тримаєтеся на вузлі (і використовуйте, node.Valueколи потрібно оригінальний елемент). Отже, ви перезаписуєте алгоритм для роботи з вузлами, а не необробленими значеннями.
ToolmakerSteve

0

Я згоден з більшістю пункту, викладеного вище. І я також погоджуюся, що Ліст виглядає як більш очевидний вибір у більшості випадків.

Але я просто хочу додати, що є багато випадків, коли LinkedList набагато кращий вибір, ніж Список для кращої ефективності.

  1. Припустимо, ви переходите через елементи і хочете виконати багато вставок / видалення; LinkedList робить це в лінійний O (n) час, тоді як List робить це в квадратичний O (n ^ 2) час.
  2. Припустимо, ви хочете отримувати доступ до більших об'єктів знову і знову, LinkedList стає дуже кориснішим.
  3. Deque () та queue () краще реалізувати за допомогою LinkedList.
  4. Збільшення розміру LinkedList набагато простіше і краще, коли ви маєте справу з багатьма і більшими об'єктами.

Сподіваюся, хтось вважатиме ці коментарі корисними.


Зауважте, що ця порада призначена для .NET, а не Java. У реалізації пов'язаного списку Java у вас немає поняття "поточний вузол", тому вам доведеться пройти список для кожної вставки.
Джонатан Аллен

Ця відповідь лише частково правильна: 2) якщо елементів є великими, то зробіть тип елемента Класом не Структурою, так що Список просто містить посилання. Тоді розмір елемента стає неактуальним. 3) Деке та черга можна ефективно виконати у списку, якщо ви використовуєте список як "круговий буфер", замість того, щоб на початку вставляти чи видаляти. Дик Стівена Клері . 4) частково вірно: коли для багатьох об'єктів, для LL не потрібна величезна суміжна пам'ять; Мінус - додаткова пам'ять для покажчиків вузлів.
ToolmakerSteve

-2

Стільки середніх відповідей тут ...

Деякі реалізації пов'язаних списків використовують основні блоки попередньо виділених вузлів. Якщо вони цього не роблять, постійний час / лінійний час є менш актуальним, оскільки продуктивність пам’яті буде поганою, а продуктивність кешу - ще гіршою.

Використовуйте пов'язані списки, коли

1) Ви хочете захистити нитку. Ви можете створити кращі альго-безпечні нитки. Блокування витрат буде домінувати в паралельному списку стилів.

2) Якщо у вас є велика структура, схожа на структуру черги, і ви хочете видаляти або додавати будь-де, але кінець, весь час. > 100К списків існує, але вони не такі поширені.


3
Це питання стосувалося двох реалізацій C #, загалом не пов'язаних списків.
Джонатан Аллен

Те саме в усіх мовах
користувач1496062

-2

Я задав аналогічне запитання, пов'язане з виконанням колекції LinkedList , і виявив , що реалізація С # Стівена Клірі в Deque була рішенням. На відміну від колекції черги, Deque дозволяє переміщувати предмети спереду та ззаду. Він схожий на пов'язаний список, але з покращеною продуктивністю.


1
Re вашу заяву , що Dequeце «схоже на зв'язаний список, але з поліпшеними характеристиками» . Будь ласка кваліфікуватися це твердження: Dequeкраще продуктивність , ніж LinkedList, для конкретного коду . Переходячи за вашим посиланням, я бачу, що через два дні ви дізналися від Івана Стоєва, що це не неефективність LinkedList, а неефективність вашого коду. (І навіть якби це була неефективність LinkedList, це не виправдовувало б загального твердження про те, що Deque є більш ефективним; лише в конкретних випадках.)
ToolmakerSteve
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.