Что такое связный список (Linked List)?

Ответ

Связный список — это линейная структура данных, состоящая из последовательности узлов (Node), где каждый узел хранит:

  1. Данные (значение).
  2. Ссылку (указатель) на следующий узел в списке.

В отличие от массива (List в Dart), элементы связного списка не хранятся в непрерывном блоке памяти, а распределены динамически.

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

  • Односвязный список: Узел ссылается только на следующий.
  • Двусвязный список: Узел ссылается на следующий и предыдущий, что позволяет обход в обоих направлениях.
  • Кольцевой связный список: "Хвост" списка ссылается на его "голову".

Реализация односвязного списка на Dart:

class Node<T> {
  T value;
  Node<T>? next; // Ссылка на следующий узел (null для последнего)

  Node(this.value, [this.next]);
}

class LinkedList<T> {
  Node<T>? head;
  Node<T>? tail;

  void addToTail(T value) {
    final newNode = Node(value);
    if (tail == null) { // Список пуст
      head = tail = newNode;
    } else {
      tail!.next = newNode;
      tail = newNode;
    }
  }

  void printList() {
    Node<T>? current = head;
    while (current != null) {
      print(current.value);
      current = current.next;
    }
  }
}

void main() {
  final list = LinkedList<int>();
  list.addToTail(1);
  list.addToTail(2);
  list.addToTail(3);
  list.printList(); // Вывод: 1 2 3
}
Сравнение с массивом (List в Dart): Операция Связный список Массив (List)
Вставка/удаление в начале O(1) O(n) (сдвиг элементов)
Доступ по индексу O(n) (последовательный обход) O(1)
Вставка в конец O(1) (если храним tail) O(1) амортизированно
Расход памяти Больше (хранит ссылки) Меньше

Применение в разработке: Прямое использование связных списков в Dart встречается редко, так как встроенный List (реализованный как расширяемый массив) эффективен для большинства задач. Однако понимание принципа работы важно для изучения более сложных структур (деревья, графы) и алгоритмов.

Ответ 18+ 🔞

Давай разберём эту тему, но без занудства, а как есть. Представь, что тебе нужно хранить кучу данных, но не в одном аккуратном блоке, как в шкафу, а раскидать их по всей квартире. И чтобы не потеряться, к каждой пачке носков ты приклеиваешь записку: «Следующие носки лежат на балконе». Вот это, ёпта, и есть связный список в его первозданном виде.

Что это вообще такое? Это просто цепочка узлов. Каждый узел — это как коробка из-под пива. Внутри коробки две вещи:

  1. Само значение (пустая банка, как память о былых подвигах).
  2. Записка с адресом, где искать следующую такую же коробку.

Вот и вся магия. Никакого единого массива, всё разбросано где попало, но связано этими самыми записками. Главное — не потерять первую коробку, иначе потом все остальные нихуя не найдешь.

Какие бывают разновидности?

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

Вот как это может выглядеть на Dart:

class Node<T> {
  T value; // Само значение (та самая банка)
  Node<T>? next; // Ссылка-записка на следующий узел (null, если это конец)

  Node(this.value, [this.next]);
}

class LinkedList<T> {
  Node<T>? head; // Самая первая коробка. Потерял её — всё, пиздец.
  Node<T>? tail; // Последняя коробка, чтобы не бегать каждый раз до конца.

  // Метод "добавить в конец"
  void addToTail(T value) {
    final newNode = Node(value); // Сделали новую коробку
    if (tail == null) { // Если список пустой, охуеть
      head = tail = newNode; // Новая коробка сразу и голова, и хвост
    } else {
      tail!.next = newNode; // Старому хвосту пишем: "Следующий — вот эта новая"
      tail = newNode;       // Теперь новая коробка — официальный хвост
    }
  }

  // Метод "напечатать всё, что насобирали"
  void printList() {
    Node<T>? current = head; // Начинаем с первой коробки
    while (current != null) { // Пока коробка существует
      print(current.value);   // Смотрим, что в ней
      current = current.next; // Берём записку и идём к следующей
    }
  }
}

void main() {
  final list = LinkedList<int>();
  list.addToTail(1); // Кинули первую банку
  list.addToTail(2); // Кинули вторую
  list.addToTail(3); // Кинули третью
  list.printList(); // Вывод: 1 2 3
}

А зачем это всё, если есть обычный List? Отличный вопрос! Всё упирается в то, где ты ковыряешься.

Что делаем Связный список Обычный List (массив)
Вставить в начало О, это быстро! O(1). Сделал новую коробку, написал в ней "следующая — старая голова", и всё. O(n), пиздец. Придётся сдвигать все существующие элементы, чтобы освободить место.
Достать по номеру O(n), жесть. Чтобы найти 100-й элемент, надо пройти 99 предыдущих коробок. O(1), мгновенно. Математика, детка.
Вставить в конец O(1), если храним tail. O(1) в среднем.
Память Жрёт больше, надо хранить ссылки. Экономнее.

Так где это применяется? Честно? В обычной Dart-разработке — редко. Встроенный List настолько оптимизирован, что в 95% случаев тебе его хватит за глаза. Но понимать, как работает связный список — это как знать, как устроен двигатель. Сам можешь и не чинить, но когда начнёшь разбираться с деревьями, графами или какими-нибудь хитрыми алгоритмами, это знание выручит. Иначе будешь как мартышлюшка с гранатой — вроде делаешь что-то, но волнение ебать, что всё взорвётся.