Когда использовать связный список (LinkedList)?

Ответ

Связный список (LinkedList) — это структура данных, где элементы (узлы) хранят значение и ссылку на следующий узел. Используйте его, когда:

  • Критически важны частые вставки и удаления в середине списка. При известном узле эти операции выполняются за O(1) путем перенаправления ссылок, в отличие от массива (Array), где требуется сдвиг элементов (O(n)).
  • Размер коллекции сильно изменяется, и выделение непрерывного блока памяти (как у массива) неэффективно.

Пример структуры узла:

class Node<T> {
    var value: T
    var next: Node?
    init(value: T) { self.value = value }
}
// Вставка нового узла `newNode` после `knownNode`:
newNode.next = knownNode.next
knownNode.next = newNode

Недостатки по сравнению с массивом (Array):

  • Доступ по индексу выполняется за O(n) (требуется последовательный обход).
  • Большие накладные расходы на память для хранения ссылок (next, а в двусвязном списке — и previous).
  • Отсутствие кэш-локальности процессора, так как узлы разбросаны в памяти.

Вывод: Выбирайте связный список для задач, где структура данных часто и динамически изменяется (например, реализация очереди или стека, undo/redo функционал), а не для сценариев, где требуется частый произвольный доступ по индексу.

Ответ 18+ 🔞

Ну вот, опять про эти ваши связные списки. Слушай, представь себе цепочку пьяных гномиков в три часа ночи. Каждый держит в одной руке бутылку (это его значение), а другой рукой — хватается за плечо следующего. Это и есть LinkedList, ёпта. Пока они держатся — цепочка есть. Разжал руку — всё, связь порвалась, следующий гномик потерялся в тумане.

Так вот, зачем эта хуйня нужна? А вот представь, что тебе надо впихнуть нового гномика между третьим и четвёртым. С массивом (Array) была бы пиздец-картина: пришлось бы отодвигать всех последующих гномиков, чтобы освободить место. А тут — элементарно, Ватсон! Берёшь нового гномика, его руку кладёшь на плечо четвёртого, а руку третьего — перекладываешь на плечо новичка. Всё, операция «впендюрить» выполнена за O(1). Красота!

Вот тебе гномик, извини, узел:

class Node<T> {
    var value: T
    var next: Node?
    init(value: T) { self.value = value }
}
// Вставка нового узла `newNode` после `knownNode`:
newNode.next = knownNode.next
knownNode.next = newNode

Но не всё так радужно, блядь. Захочешь ты найти, допустим, пятьдесят седьмого гномика — вот тут-то и начнётся весёлая жизнь. Придётся идти с самого начала, считать: раз, два, три... пятьдесят семь, ебать мои старые костыли! Это O(n), а не моментальный прыжок, как в массиве.

Да и память он жрёт, как не в себя. Каждому гномику, кроме бутылки, ещё и руку-ссылку (next) хранить надо. А если список двусвязный, так он ещё и за штаны предыдущего держится (previous) — вообще манда с ушами получается.

И главное — кэш процессора его ненавидит. Потому что эти гномики разбросаны по всей оперативке, где попало, а не стоят аккуратным строем, как в массиве. Нет тут кэш-локальности, одни расстройства.

Так что вывод, блядь, простой, как три копейки: Тащи эту структуру из чулана, только когда тебе надо постоянно и активно перетряхивать середину цепочки (очередь, стек, история действий) или когда размер коллекции скачет, как хуй в проруби. Если же тебе надо постоянно тыкать пальцем в случайные места — бери массив, и не мучай ни себя, ни процессор. Всё гениальное — просто, а всё сложное — часто нахуй не нужно.