Линейный список. Физическое представление (массив): индексный вектор

Модуль "Линейные списки" • урок 3 из 9 • ~6 минут

Суть урока. Физическое представление списка на векторе (массиве) имеет проблему: вставка в середину вызывает сдвиг хвоста за O(N). Чтобы избежать этого, вводят индексный вектор - суррогат указателей, где вместо сдвигов перенастраиваются индексы, а программист сам ведет учет "дырок" (свободных ячеек).
Линейный список. Физическое представление (массив): каждая ячейка пула хранит данные и индекс следующего элемента, а освободившиеся ячейки связаны в отдельный список свободных мест
Порядок задают индексы: удаление оставляет дырку, а не сдвигает соседей

Проблема простого массива

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

Придется физически сдвинуть все последующие элементы вправо на одну позицию (в среднем N/2 операций). Аналогично, при удалении элемента из середины придется сдвинуть весь хвост влево, чтобы избежать пустот. Это делает вставку и удаление очень медленными операциями со сложностью O(N).

Линейный список. Физическое представление (массив): как работает индексный вектор

Чтобы удешевить вставку и удаление, сохраняя компактность массива, над основным массивом данных надстраивается дополнительная структура - управляющий индексный вектор. Вместо того чтобы физически перемещать элементы, мы храним в каждой ячейке индекс следующего логического элемента.

А как в Python? Индексный вектор (и список свободных ячеек) - это исторический механизм ручного управления памятью из времен, когда динамической памяти еще не было. В Python программисту не нужно вручную управлять дырками в массивах: автоматический Garbage Collector сам следит за тем, какие ячейки освободились, и переиспользует их.

Как это выглядит на языке C

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

#define MAX_SIZE 100

/* Элемент массива, содержащий данные и индекс следующего */
typedef struct {
    int data;
    int next_index; /* Индекс следующего элемента (-1 если конец списка) */
} SurrogateNode;

typedef struct {
    SurrogateNode pool[MAX_SIZE]; /* Сплошной блок памяти (пул) */
    int head_index;               /* Индекс начала логического списка */
    int free_list_head;           /* Индекс первой свободной ячейки */
} SurrogateList;

Если мы удаляем элемент под индексом 5, элементы с 6-го по 100-й остаются на своих местах. Мы просто добавляем число 5 в цепочку free_list_head, чтобы повторно использовать эту ячейку при следующей вставке.

Коротко: главное

  • Простой массив: Вставка и удаление в произвольном месте занимают время O(N) из-за физического сдвига элементов.
  • Индексный вектор (суррогат): Использует целые числа (индексы) вместо физических указателей для связи элементов.
  • При удалении элементы не сдвигаются, в массиве образуются "дырки".
  • Для повторного использования освобожденной памяти ведется отдельный список свободных ячеек (ручная сборка мусора).

Проверьте себя

Что происходит с элементами массива при удалении узла в списке на индексном векторе?

Как вам урок? Спасибо за отзыв!