За яких обставин пов'язані списки корисних?


110

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

В ідеалі відповіді пояснюватимуть критерії, які слід використовувати для вибору структури даних, і які структури даних, ймовірно, найкраще працюють у визначених обставинах.

Редагувати: Треба сказати, мене дуже вражає не лише кількість, але і якість відповідей. Я можу прийняти лише одне, але є ще два-три, я б сказав, що варто було б прийняти, якби щось трохи краще там не було. Лише пара (особливо та, яку я врешті-решт прийняла) вказала на ситуації, коли пов'язаний список давав реальну перевагу. Я думаю, що Стів Джессоп заслуговує на якусь почесну згадку за те, що придумав не один, а три різні відповіді, і все це мені було досить вражаючим. Звичайно, незважаючи на те, що він був розміщений лише як коментар, а не як відповідь, я думаю, що запис у блозі Ніла варто також прочитати - не лише інформативно, але й досить цікаво.


34
Відповідь на ваш другий параграф займає приблизно семестр.
Сева Алексєєва

2
На мою думку, див. Punchlet.wordpress.com/2009/12/27/letter-the-fourth . Оскільки це виглядає як опитування, воно, мабуть, має бути CW.

1
@Neil, приємно, хоча я сумніваюся, що CS Lewis схвалить.
Том

@Neil: Я думаю, що таке опитування. Переважно це спроба зрозуміти, чи може хто-небудь придумати відповідь, яка має підставу, я міг би принаймні купити як розумну. @Seva: так, перечитуючи це, я зробив останнє речення трохи більш загальним, ніж я спочатку мав намір.
Джеррі Труну

2
@Yar Люди (включаючи мене, вибачте, сказати) використовували для реалізації зв'язаних списків без покажчиків такими мовами, як FORTRAN IV (які не мали поняття покажчиків), як і дерева. Ви використовували масиви замість "реальної" пам'яті.

Відповіді:


40

Вони можуть бути корисними для одночасних структур даних. (Нижче є зразок використання в реальному світі, що не відповідає одночасно, - цього не було б, якби @Neil не згадав FORTRAN. ;-)

Наприклад, ConcurrentDictionary<TKey, TValue>у .NET 4.0 RC використовують зв'язані списки для ланцюжка елементів, які хешують до того самого відра.

Базова структура даних ConcurrentStack<T>також є пов'язаним списком.

ConcurrentStack<T>є однією із структур даних, яка служить основою для нового пулу потоків (по суті, локальні "черги", реалізовані як стеки). (Інша основна несуча структура ConcurrentQueue<T>.)

Новий пул потоків, в свою чергу, забезпечує основу для планування роботи нової бібліотеки паралельних завдань .

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

(Список, пов’язаний окремо, робить переконливий вибір без блокування, але не чекає в цих випадках, оскільки основні операції можна виконувати за допомогою одного CAS (+ повторень). У сучасних умовах GC-d - таких як Java та .NET - проблему ABA легко уникнути. Просто загортайте додані вами елементи в свіжо створені вузли та не використовуйте їх повторно - нехай GC виконує свою роботу. Сторінка проблеми ABA також забезпечує реалізацію блокування безкоштовний стек - це фактично працює в .Net (& Java) з вузлом (GC-ed), який містить елементи.)

Редагувати : @Neil: насправді, те, що ви згадали про FORTRAN, нагадало мені, що той самий тип пов'язаних списків можна знайти в, мабуть, найбільш використовуваній та зловживаній структурі даних у .NET: простому .NET generic Dictionary<TKey, TValue>.

Не один, а багато пов'язаних списків зберігаються в масиві.

  • Це дозволяє уникати багатьох невеликих (де) виділень на вставленнях / видаленнях.
  • Початкове завантаження хеш-таблиці відбувається досить швидко, оскільки масив заповнюється послідовно (дуже добре грає з кешем процесора).
  • Не кажучи вже про те, що ланцюгова хеш-таблиця є дорогою з точки зору пам’яті - і цей «трюк» скорочує «розміри вказівників» навпіл на x64.

По суті, багато пов'язаних списків зберігаються в масиві. (по одному для кожного використовуваного відра.) Вільний список вузлів для багаторазового використання "переплетений" між ними (якщо були делети). Масив виділяється на старті / при повторному перезахороненні і в ньому зберігаються вузли ланцюгів. Існує також вільний покажчик - індекс в масив, який слідує за видаленнями. ;-) Отже - вірите чи ні - техніка FORTRAN все ще живе. (... і ніде більше, ніж в одній з найбільш часто використовуваних .NET структур даних ;-).


2
Якщо ви пропустили, ось коментар Ніла: "Люди (включаючи мене, вибачте, сказати) використовували для впровадження пов'язаних списків без покажчиків такими мовами, як FORTRAN IV (що не мала поняття покажчиків), як і дерева Ви використовували масиви замість "реальної" пам'яті ".
Андрас Васс

Додам, що підхід "пов'язаних списків у масиві" у випадку Dictionaryзбереження значно більше у .NET: інакше кожен вузол потребує окремого об'єкта на купі - і кожен об'єкт, виділений на купі, має деякий накладний обсяг. ( en.csharp-online.net/Common_Type_System%E2%80%94Object_Layout )
Андрас Васс

Також добре знати, що за замовчуванням C ++ std::listне є безпечним у багатопотоковому контексті без блокування.
Mooing Duck

49

Пов'язані списки дуже корисні, коли потрібно зробити багато вставок та видалень, але не надто багато шукати, у списку довільної (невідомої на час компіляції) довжини.

Розщеплення та приєднання списків (двосторонній зв’язок) дуже ефективне.

Ви також можете комбінувати пов'язані списки - наприклад, дерева структури можуть бути реалізовані у вигляді "вертикальних" зв'язаних списків (стосунки батьків / дітей), що з'єднують між собою горизонтальні пов'язані списки (побратими).

Використання списку на основі масиву для цих цілей має суворі обмеження:

  • Додавання нового елемента означає, що масив повинен бути перерозподілений (або потрібно виділити більше місця, ніж потрібно для подальшого зростання та зменшення кількості перерозподілів)
  • Видалення предметів залишає витрачений простір або потребує перерозподілу
  • вставлення елементів будь-де, крім кінця, передбачає (можливо, перерозподіл та) копіювання безлічі даних в одну позицію

5
Таким чином, питання зводиться до, коли зробити вам потрібно зробити багато вставок і вилучень в середині послідовності, але не дуже багато пошуку в списку за порядковим номером? Переміщення пов'язаного списку зазвичай як і дорожче, ніж копіювання масиву, тому все, що ви говорите про видалення та вставлення елементів у масиви, так само погано для випадкового доступу до списків. Кеш LRU - це один із прикладів, про які я можу придумати, вам потрібно видалити в середині багато, але вам ніколи не потрібно ходити по списку.
Стів Джессоп

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

1
Припустимо, який? Це виділення надзвичайно швидко очевидно - зазвичай потрібно додати розмір об'єкта до вказівника. Яка загальна накладні витрати на GC низька? Минулого разу я спробував виміряти це в реальному додатку, ключовим моментом було те, що Java робила всю роботу, коли процесор інакше простоював, так що, природно, це не сильно впливало на видимі показники. У орієнтирі навантаженого процесора було легко засмутити Java і отримати дуже поганий час розподілу в гіршому випадку. Це було багато років тому, але покоління сміття поколінь помітно знизила загальну вартість ГК.
Стів Джессоп

1
@Steve: Ви помиляєтесь, що розподіл є "однаковим" між списками та масивами. Кожного разу, коли вам потрібно виділити пам'ять для списку, ви просто виділите невеликий блок - O (1). Для масиву потрібно виділити новий блок, достатньо великий для всього списку, а потім скопіювати весь список - O (n). Щоб вставити у відоме місце у списку, ви оновлюєте фіксовану кількість покажчиків - O (1), але вставляєте у масив та копіюєте будь-які пізніші елементи в одну позицію, щоб звільнити місце для вставки - O (n). Є багато випадків, коли масиви, таким чином, набагато менш ефективні, ніж LL.
Джейсон Вільямс

1
@Jerry: Я розумію. Моя думка полягає в тому, що значна частина витрат на перерозподіл масиву - це не розподілення пам'яті , це необхідність копіювання всього вмісту масиву в нову пам'ять. Щоб вставити в елемент 0 масиву, потрібно скопіювати весь вміст масиву на одну позицію в пам'яті. Я не кажу, що масиви погані; просто, що існують ситуації, коли випадковий доступ не потрібен, і де бажано вводити / видаляти / повторно посилати LL з постійним часом.
Джейсон Вільямс

20

Пов'язані списки дуже гнучкі: Змінивши один вказівник, ви можете внести масштабні зміни, де ця сама операція була б дуже неефективною у списку масивів.


Чи можна було б мотивувати, чому взагалі використовується список, а не набір чи карта?
patrik

14

Масиви - це структури даних, до яких зазвичай порівнюються пов'язані списки.

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

Ось перелік операцій, які можна виконати у списках та масивах, порівняно з відносною вартістю операції (n = список / довжина масиву):

  • Додавання елемента:
    • у списках просто потрібно виділити пам'ять для нового елемента та покажчики переспрямування. O (1)
    • на масивах ви повинні перемістити масив. O (n)
  • Видалення елемента
    • у списках ви просто переспрямовуєте покажчики. O (1).
    • на масивах ви витрачаєте O (n) час на переміщення масиву, якщо елемент для видалення не є першим чи останнім елементом масиву; в іншому випадку ви можете просто перемістити вказівник на початок масиву або зменшити довжину масиву
  • Отримання елемента у відомій позиції:
    • у списках вам потрібно пройти список від першого елемента до елемента в певній позиції. Найгірший випадок: O (n)
    • на масивах ви можете отримати доступ до елемента негайно. O (1)

Це дуже низький рівень порівняння цих двох популярних та базових структур даних, і ви можете бачити, що списки ефективніші в тих ситуаціях, коли вам доведеться внести багато змін у сам список (видалення або додавання елементів). З іншого боку, масиви працюють краще, ніж списки, коли вам доведеться отримувати доступ до елементів масиву.

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

Знаючи ці відмінності, розробникам важливо вибирати між списками та масивами в їх реалізації.

Зауважте, що це порівняння списків та масивів. Тут є хороші варіанти вирішення проблем (наприклад: SkipLists, Dynamic Arrays тощо). У цій відповіді я врахував основну структуру даних, про яку повинен знати кожен програміст.


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

Ця відповідь не повинна охоплювати програму курсу університету структур структур. Це порівняння, написане з урахуванням пов'язаних списків та масивів, які реалізовані так, як ви, я та більшість людей знаєте. Геометрично розширюються масиви, пропуск списків і т. Д. - це рішення, які я знаю, я використовую і вивчаю, але це потребує більш глибокого пояснення, і це не відповідає відповіді stackoverflow.
Андреа Зіліо

1
"З точки зору розподілу пам'яті, списки є кращими, оскільки немає необхідності мати всі елементи поруч." Навпаки, суміжні контейнери краще, тому що вони тримають елементи поруч. На сучасних комп’ютерах локальність даних є царем. Все, що стрибає в пам’яті, вбиває вашу кеш-здатність і призводить до програм, які вставляють елемент у (ефективно) випадкове розташування, виконуючи швидші дії з динамічним масивом, таким як C ++, std::vectorніж із пов'язаним списком, таким як C ++ std::list, просто тому, що проходить список такий дорогий.
Девід Стоун

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

4

Спільно пов'язаний список є хорошим вибором для безкоштовного списку в алокаторі комірок або пулі об’єктів:

  1. Вам потрібен лише стек, тому достатньо окремо пов'язаного списку.
  2. Все вже поділено на вузли. Не існує накладного розподілу для нав'язливого вузла списку, якщо клітини є достатньо великими, щоб містити вказівник.
  3. Вектор або дека накладають накладні витрати на один покажчик на блок. Це важливо, враховуючи, що при першому створенні купи всі комірки є безкоштовними, тому це авансова вартість. У гіршому випадку вона подвоює потребу в пам'яті на комірку.

Ну, домовились. Але скільки програмістів насправді створюють такі речі? Більшість просто повторно реалізують те, що дає вам std :: list тощо. І насправді "нав'язливий" зазвичай має дещо інше значення, ніж ви йому дали - що кожен можливий елемент списку містить вказівник, окремий від даних.

1
Як багато? Більше 0, менше мільйона ;-) Чи було питання Джеррі "давати корисні списки", або "давати корисні списки, якими користується кожен програміст щодня", чи щось середнє? Я не знаю іншого імені, крім "нав'язливого" для вузла списку, який міститься в об'єкті, що є елементом списку - будь то частиною об'єднання (в термінах С) чи ні. Точка 3 застосовується лише мовами, які дозволяють вам це робити - C, C ++, асемблер. Java погана.
Стів Джессоп

4

Подвійно пов'язаний список є хорошим вибором для визначення впорядкування хешмапу, який також визначає порядок по елементах (LinkedHashMap в Java), особливо в разі замовлення останнього доступу:

  1. Більше накладних витрат на пам'ять, ніж асоційований вектор чи deque (2 вказівника замість 1), але краще вставити / видалити продуктивність.
  2. Немає накладних витрат, оскільки вам потрібен вузол для хеш-запису.
  3. Місцевість посилань не є додатковою проблемою порівняно з вектором або декею покажчиків, оскільки вам доведеться в будь-якому випадку витягувати кожен об’єкт у пам'ять.

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


4

Пов'язані списки - один із природних варіантів, коли ви не можете контролювати, де зберігаються ваші дані, але все одно потрібно якось дістатися від одного об’єкта до іншого.

Наприклад, впроваджуючи відстеження пам'яті в C ++ (нова / видалена заміна), вам або потрібна структура керуючих даних, яка відстежує, які покажчики були звільнені, і які вам потрібно повністю реалізувати самостійно. Альтернативою є перерозподіл та додавання зв'язаного списку до початку кожного фрагменту даних.

Оскільки ви завжди негайно знаєте, де ви знаходитесь у списку, коли викликається видалення, ви можете легко відмовитися від пам'яті в O (1). Крім того, додавання нового фрагмента, який щойно був порушений, знаходиться в O (1). Ходити за списком в цьому випадку дуже рідко потрібно, тому вартість O (n) тут не є проблемою (ходіння структури в будь-якому разі є O (n)).


3

Вони корисні, коли вам потрібно швидкодіювати, натискати та обертати, і не заперечуйте O (n) індексування.


Ви коли-небудь турбували про час, пов'язаний зі списком C ++, порівняно з (скажімо) декетом?

@Neil: Не можу сказати, що я маю.
Ігнасіо Васкес-Абрамс

@Neil: якщо C ++ навмисно саботував свій клас пов'язаного списку, щоб зробити його повільніше, ніж будь-який інший контейнер (що не далеко від істини), що це стосується мовно-агностичного питання? Нав'язливий пов'язаний список все ще є зв'язаним списком.
Стів Джессоп

@Steve C ++ - це мова. Я не бачу, як це може мати волю. Якщо ви припускаєте, що члени Комітету C ++ якимось чином саботували пов'язані списки (що логічно повинно бути повільним для багатьох операцій), то назвіть винних людей!

3
Це не зовсім диверсія - зовнішні вузли списку мають свої переваги, але продуктивність не одна з них. Однак, безумовно, всі знали, коли здійснювали компроміс того самого, про що ви знаєте, а це те, що досить складно придумати корисне використання std::list. Нав'язливий список просто не відповідає філософії С ++ про мінімальні вимоги до елементів контейнерів.
Стів Джессоп

3

Однозначно пов'язані списки - це очевидна реалізація загального типу даних «списку» у функціональних мовах програмування:

  1. Додавання до голови відбувається швидко (append (list x) (L))і (append (list y) (L))може обмінюватися майже всіма своїми даними. Не потрібно копіювати на мові без запису. Функціональні програмісти знають, як цим скористатися.
  2. Додавання до хвоста, на жаль, повільне, але так би було і будь-яке інше реалізація.

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


3

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


2

З мого досвіду, реалізація малих матриць і кульових груп. Пов'язані списки дають вам більше контролю над загальною структурою таких структур даних. Хоча я не впевнений, що розріджені матриці найкраще реалізовувати за допомогою зв'язаних списків - ймовірно, є кращий спосіб, але це дійсно допомогло вивчити входи та виходи з розріджених матриць за допомогою зв'язаних списків у нижній CS :)


1

Є дві додаткові операції, які тривіально O (1) у списках і дуже важко реалізувати в O (1) в інших структурах даних - вилучення та вставлення елемента з довільного положення, припускаючи, що вам потрібно підтримувати порядок елементів.

Хеш-карти очевидно можуть робити вставку та видалення в O (1), але тоді ви не можете перебирати елементи по порядку.

З огляду на вищевикладений факт, хеш-карту можна поєднувати з пов’язаним списком, щоб створити чудовий кеш LRU: Карта, яка зберігає фіксовану кількість пар ключових значень і скидає найменш недавно доступний ключ, щоб звільнити місце для нових.

Записи на хеш-карті повинні мати вказівники на пов'язані вузли списку. Під час доступу до карти хешу, вузол пов'язаного списку від’єднується від його поточного положення та переміщується до голови списку (O (1), так, для пов'язаних списків!). Коли потрібно видалити найменш використаний нещодавно елемент, той, що залишився з хвоста списку, повинен бути скинутий (знову ж таки O (1), припускаючи, що ви тримаєте вказівник на хвостовий вузол) разом із відповідним записом хеш-карти (тому зворотні посилання з список на хеш-мапі необхідний.)


1

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

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

Будучи прихильником коду, який читає як природну мову, мені подобається, як це дозволить програмісту запитати ланцюжок, яку вагу він несе. Він також зберігає стурбованість обчисленням цим дітям властивостей у межах реалізації посилання, усуваючи необхідність у послузі обчислення ваги ланцюга ".


1

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

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

В результаті візьмемо випадок, коли ми хочемо зробити аналогічний еквівалент зберігання послідовності змінної довжини, яка містить мільйон вкладених підрядів змінної довжини. Конкретний приклад - індексована сітка, яка зберігає мільйон багатокутників (деякі трикутники, кілька квадратиків, деякі п’ятикутники, деякі шестикутники тощо), а іноді багатокутники видаляються з будь-якого місця в сітці, а іноді багатокутники відновлюються, щоб вставити вершину до існуючого багатокутника або видалити одну. У такому випадку, якщо ми зберігаємо мільйон крихітних std::vectors, ми стикаємося з куповим виділенням для кожного окремого вектора, а також з потенційно вибухонебезпечним використанням пам'яті. Мільйон крихітних SmallVectorsможе не постраждати від цієї проблеми так само часто, як правило, але тоді їх попередньо виділений буфер, який не виділений окремо з купи, все ще може спричинити використання вибухової пам'яті.

Проблема тут полягає в тому, що мільйон std::vectorпримірників намагається зберігати мільйон речей різної довжини. Речі змінної довжини, як правило, хочуть виділити купу, оскільки вони не можуть дуже ефективно зберігатись та видалятися постійно (принаймні прямолінійно, без дуже складного розподільника), якщо вони не зберігали їх вміст в іншому купі.

Якщо натомість ми робимо це:

struct FaceVertex
{
    // Points to next vertex in polygon or -1
    // if we're at the end of the polygon.
    int next;
    ...
};

struct Polygon
{
     // Points to first vertex in polygon.
    int first_vertex;
    ...
};

struct Mesh
{
    // Stores all the face vertices for all polygons.
    std::vector<FaceVertex> fvs;

    // Stores all the polygons.
    std::vector<Polygon> polys;
};

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

Ось ще один приклад:

введіть тут опис зображення

... де осередки сітки використовуються для прискорення зіткнення частинок-частинок для, скажімо, 16 мільйонів частинок, що рухаються кожен кадр. У цьому прикладі сітки частинок, використовуючи пов'язані списки, ми можемо перемістити частинку з однієї комірки сітки в іншу, просто змінивши 3 індексу. Стирання з вектора і відведення назад до іншого може бути значно дорожчим і ввести більше купових виділень. Зв'язані списки також зменшують пам'ять комірки до 32 біт. Вектор, залежно від реалізації, може попередньо розподілити свій динамічний масив до тієї точки, де він може взяти 32 байти для порожнього вектора. Якщо у нас є близько мільйона комірок сітки, це зовсім різниця.

... і саме тут я знаходжу пов'язані списки найкориснішими в наші дні, і я конкретно вважаю різноманітність "індексованого пов'язаного списку" корисною, оскільки 32-бітні індекси наполовину знижують вимоги до пам'яті посилань на 64-бітних машинах, і вони означають, що вузли постійно зберігаються в масиві.

Часто я також комбіную їх з індексованими безкоштовними списками, щоб дозволити видалення та вставки постійного часу в будь-якому місці:

введіть тут опис зображення

У цьому випадку nextіндекс або вказує на наступний вільний індекс, якщо вузол був видалений, або наступний використаний індекс, якщо вузол не був видалений.

І це є випадком використання номер один, який я знаходжу для пов’язаних списків сьогодні. Коли ми хочемо зберегти, скажімо, мільйон підрядних послідовностей змінної довжини, що в середньому становлять, скажімо, 4 елементи кожен (але іноді з елементами видаляються та додаються до однієї з цих підрядів), пов'язаний список дозволяє нам зберігати 4 мільйони зв'язані вузли списку безперервно замість 1 мільйона контейнерів, кожен з яких виділений окремо в купу: один гігантський вектор, тобто не мільйон малих.


0

У минулому я використовував пов'язані списки (навіть подвійно пов'язані списки) в додатку C / C ++. Це було раніше .NET і навіть stl.

Напевно, я б зараз не використовував пов'язаний список мовою .NET, оскільки весь необхідний код проходження надається вам методами розширення Linq.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.