Линейный список. Физическое представление (динамические объекты): указатели
next и prev). Вставка и удаление сводятся
к переброске указателей за O(1) (данные в памяти не перемещаются).
Почему динамические объекты?
Это наиболее естественное и гибкое представление списков. Оно полностью избавляет программиста от недостатков массива: не нужно сдвигать элементы при вставках и удалениях, нет проблемы "дырок", не нужно вручную программировать суррогатный индексный вектор. Выделение памяти берет на себя операционная система (или среда выполнения) по мере необходимости.
Линейный список. Физическое представление (динамические объекты): как устроен связный список
Элемент списка (узел) состоит из информационной части (данные) и служебной части (один или несколько физических указателей).
Односвязный список
Каждый узел хранит данные и один указатель next на следующий элемент.
Это экономит память, но имеет недостаток: двигаться по такому списку можно только
в одном направлении - от головы к хвосту.
Двусвязный (симметричный) список
Узел содержит два указателя: на следующий (next) и на предыдущий (prev) элементы.
typedef struct Item {
struct Item* prev; /* Ссылка назад */
struct Item* next; /* Ссылка вперед */
int data; /* Полезные данные */
} Item;
Кольцевые списки: Двусвязные списки часто замыкают в кольцо.
Последний элемент своим next указывает на первый, а первый своим prev - на последний.
Это избавляет от проблемы "тупиков" по краям и убирает лишние проверки на NULL.
Вставка и удаление за O(1)
Физически вставка в середину двусвязного списка выглядит как простое "перебрасывание" четырех указателей. Мы разрываем связь между соседями и направляем их указатели на новый узел, а указатели нового узла - на соседей.
Сами данные в памяти никуда не копируются и не перемещаются! Стоимость самой операции переброски - O(1) (но не забывайте, что поиск нужного места все равно занимает O(N)).
list в Python - это скрытый
массив. Однако в стандартной библиотеке есть настоящий двусвязный список -
collections.deque. Он реализован в CPython как двусвязный список блоков
фиксированного размера, что позволяет делать вставку и удаление за O(1) с обоих концов.
Реализация вставки на языке C
Пример функции, которая вставляет новый узел после заданного (pos):
#include <stdlib.h>
/* Функция вставляет новый элемент ПОСЛЕ указанного узла (pos) */
void InsertAfter(Item* pos, int value) {
if (pos == NULL) return;
Item* newItem = (Item*)malloc(sizeof(Item));
newItem->data = value;
/* Настраиваем связи нового узла */
newItem->prev = pos;
newItem->next = pos->next;
/* Перебрасываем связи соседей на новый узел */
if (pos->next != NULL) {
pos->next->prev = newItem;
}
pos->next = newItem;
}
В интерактивном тренажере ниже можно по шагам проследить жизненный цикл связного списка: от выделения памяти в куче до точечной переброски указателей при вставке и удалении узлов, а также увидеть, почему поиск нужного значения все равно занимает линейное время.
Тренажер: список на указателях
Соберем список 5 → 3 → 8, вставим 7 после узла 5, удалим 3 и найдем 8.
Коротко: главное
- Узлы списка физически разбросаны в "куче" и выделяются функциями ОС.
- Структура: Односвязный (
next) или двусвязный (nextиprev). - Вставка/удаление: Простая переброска указателей за время O(1), данные не перемещаются.
- Кольцевой список:
Next(Последний) = Первый, что убирает краевые эффекты.
Проверьте себя
Сколько времени занимает САМА операция вставки нового узла между двумя уже известными соседними узлами в двусвязном списке?