Чи повинні пов'язані списки завжди мати вказівник хвоста?


11

Моє розуміння ...

Переваги:

  • В кінці вставляється O (1) замість O (N).
  • Якщо список є подвійно пов'язаним списком, то видалення з кінця також є O (1) замість O (N).

Недолік:

  • Займає тривіальну кількість додаткової пам’яті: 4-8 байт .
  • Реалізатор повинен слідкувати за хвостом.

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


1
хвостовий вказівник становить 4-8 байт (залежно від 32 або 64 бітної системи)
храповик виродка

1
Здається, ви вже майже це узагальнили.
Роберт Харві

@RobertHarvey Я зараз вивчаю структури даних і не знаю, які найкращі практики є. Тож, що я написав - це мої враження, але те, що я запитую, чи є вони правильними. Але дякую за уточнення!
Адам Зернер

7
"Найкращі практики" - це опіати мас . Відзначайте той факт, що у вас ще є можливість думати про себе.
Роберт Харві

Дякую за посилання @RobertHarvey - Я люблю цю точку! Я безумовно використовую підхід щодо вигоди та витрат, який враховує специфіку ситуації.
Адам Зернер

Відповіді:


7

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

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


9

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


3
Ви б не проти пояснити, чому вони порушують стійкість і незмінність?
Адам Зернер

Будь ласка, додайте занепокоєння щодо зручності кешу
Basilevs

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

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

0

Я рідко використовую вказівник хвоста для пов'язаних списків і частіше використовую поодинокі зв'язані списки частіше там, де достатньо складового типу "push / pop" вставки та вилучення (або просто лінійного вилучення з середини). Це тому, що в моїх звичайних випадках використання вказівник хвоста насправді дорогий, подібно до того, як доробити окремо пов'язаний список у подвійно пов'язаний список дорого.

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

Як приклад, уявіть відеоігри, що використовують сітку 400х400, щоб розділити мільйон частинок, які рухаються навколо та відскакують одна від одної для прискорення виявлення зіткнень. У цьому випадку досить ефективний спосіб зберігання - це зберігання 160 000 однозначно пов'язаних списків, що в моєму випадку означає 160 000 32-бітових цілих чисел (~ 640 кілобайт) і одне 32-бітове ціле число на частину. Тепер, коли частинки рухаються по екрану, все, що нам потрібно зробити, - це оновити кілька 32-бітових цілих чисел для переміщення частинки з однієї комірки в іншу, наприклад:

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

... з nextіндексом ("покажчиком") вузла частинок, який служить або індексом до наступної частинки в клітині, або до наступної вільної частинки, щоб повернути, якщо частка загинула (в основному, це вільна реалізація алокатора списку за допомогою індексів):

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

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

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

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

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

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

Варто також пам’ятати, що в наші дні у нас завантажується DRAM, але це другий найповільніший тип пам’яті. Ми все ще знаходимось на кшталт 64 КБ на ядро, якщо мова йде про кеш L1 з 64-байтовими лініями кеша. Як результат, ці маленькі байтові заощадження можуть дійсно мати значення в критичній для продуктивності області, як симулятор частинок вище, коли їх помножують мільйони разів, якщо це означає різницю між збереженням удвічі більшої кількості вузлів у кеш-лінії чи ні, наприклад

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