Что такое связный список и каковы его ключевые особенности

Ответ

Связный список (Linked List) — это линейная динамическая структура данных, состоящая из последовательности узлов. В отличие от массива, его элементы хранятся не в непрерывной области памяти, а могут быть разбросаны по ней.

Ключевые характеристики:

  • Узел (Node): Базовый элемент списка, который содержит две части: сами данные (data) и указатель (next) на следующий узел в последовательности.
  • Указатель (Pointer/Next): Ссылка, которая связывает один узел с другим. У последнего узла указатель обычно равен null.
  • Голова (Head): Указатель на самый первый узел списка. Это точка входа в структуру.

Основные типы:

  • Односвязный список (Singly Linked List): Каждый узел ссылается только на следующий.
  • Двусвязный список (Doubly Linked List): Узлы содержат ссылки как на следующий, так и на предыдущий элементы, что упрощает обход в обе стороны.
  • Кольцевой список (Circular Linked List): Последний узел ссылается на первый, замыкая список в кольцо.

Пример реализации узла и односвязного списка на Python:

class Node:
    """Узел связного списка"""
    def __init__(self, data):
        self.data = data  # Данные
        self.next = None  # Указатель на следующий узел

class LinkedList:
    """Простейший односвязный список"""
    def __init__(self):
        self.head = None # Голова списка

    def append(self, data):
        """Добавление элемента в конец списка"""
        new_node = Node(data)
        if not self.head:
            self.head = new_node
            return
        # Идем до последнего элемента
        last_node = self.head
        while last_node.next:
            last_node = last_node.next
        last_node.next = new_node

Преимущества и недостатки

Преимущества:

  • Динамический размер: Легко добавлять и удалять элементы без необходимости перераспределения памяти.
  • Эффективная вставка/удаление: Операции в начале списка (или в середине, если есть указатель на узел) выполняются за константное время O(1).

Недостатки:

  • Медленный доступ к элементам: Для доступа к элементу по индексу требуется последовательный обход от начала списка, что занимает линейное время O(n).
  • Дополнительные затраты памяти: Каждый узел хранит указатель, что увеличивает общий расход памяти по сравнению с массивом.
  • Плохая локальность кэша: Элементы разбросаны по памяти, что делает итерацию по списку медленнее, чем по массиву, элементы которого лежат в памяти рядом.

Ответ 18+ 🔞

А, слушай, вот тебе про связный список, только без этой академической зауми, на пальцах. Представь себе цепочку, блядь, но не золотую, а из узлов.

Что за узел, ёпта? Это такая коробочка. В ней две штуки: первая — сами твои данные (цифра, текст, чёрт в ступе), а вторая — это, сука, верёвочка, которая тянется к следующей такой же коробочке. Эта верёвочка — и есть указатель, next. Последняя коробочка тянет верёвочку в никуда, в null. Вот и вся магия.

А голова — это что? Это просто записка у тебя на столе, на которой жирно написано: «ПЕРВАЯ КОРОБОЧКА ТУТ». Это твоя точка входа, head. Нет её — нихуя не найдёшь.

Какие они бывают, эти списки?

  • Односвязный — самая простая хуйня. Из каждой коробочки верёвочка идёт только к следующей. Назад не дёрнешь, придётся с начала ползти.
  • Двусвязный — тут уже по две верёвочки: одна вперёд, другая назад. Удобно, можно туда-сюда шастать, как по бульвару.
  • Кольцевой — а это когда последняя коробочка свою верёвочку привязывает не к хуям, а обратно к первой. Получился хоровод, ёперный театр. Выхода нет.

Вот, смотри, как это на Питоне выглядит, если делать на коленке:

class Node:
    """Узел связного списка"""
    def __init__(self, data):
        self.data = data  # Собсно, данные
        self.next = None  # Верёвочка к следующему. Пока ни к кому.

class LinkedList:
    """Простейший односвязный список"""
    def __init__(self):
        self.head = None # Та самая записка "ГДЕ НАЧАЛО?"

    def append(self, data):
        """Прилепить новую коробочку в самый конец"""
        new_node = Node(data) # Смастерили новую коробку
        if not self.head: # А если записка "ГДЕ НАЧАЛО?" пустая?
            self.head = new_node # Так вот же начало, охуенно!
            return
        # А если нет — поползли по верёвочкам до последней
        last_node = self.head
        while last_node.next: # Пока верёвочка куда-то ведёт...
            last_node = last_node.next # ...ползи дальше, не останавливайся.
        # Дополз. Теперь привяжи новую коробку к последней.
        last_node.next = new_node

Ну и где тут подвох? Плюсы-минусы.

Плюсы (чтоб не охуеть с тоски):

  • Растёт как хочет. Добавляй хоть тысячу коробок — память сама найдётся, не как в массиве, который одним куском лежит и орёт, когда ему тесно.
  • Впихнуть/выпихнуть в середину — быстро. Особенно если ты уже стоишь у нужной коробки. Просто перевязал верёвочки — и делов-то. В начале списка — вообще за микросекунды.

Минусы (а куда без них, блядь):

  • Доступ по номеру — пиздец. Захотел 100-ю коробку? Извини, дружок, начинай с первой и ползи по верёвочкам все 99 раз. Время O(n), нервы O(over9000).
  • Жрёт память. Каждая коробка тащит с собой эту верёвочку (next), это лишние байты. В массиве данные лежат плечом к плечу, без этих соплей.
  • Кэш процессора его ненавидит. Поскольку коробки раскиданы по памяти как попало, процессору не удаётся их заранее умно подгрузить. Гоняться за ними — то ещё удовольствие, медленнее, чем по массиву скакать.

Короче, инструмент как инструмент. Где надо часто вставлять-удалять — он красавчик. Где надо быстро прыгать по индексам — тут уже массив рулит. Выбирай по задаче, а не потому что модно.