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

Модуль "Стек" • урок 5 из 8 • ~7 минут

Суть урока. Второе физическое представление стека - цепочка узлов в динамической памяти: каждый узел хранит данные и указатель prev на элемент, лежащий под ним, а сам стек - один указатель top на вершину. Такой стек растет, пока есть свободная память: переполнения POOL_SIZE больше нет.
Стек. Физическое представление (динамические объекты): указатель top держит вершину, каждый узел хранит данные и указатель prev на элемент ниже
Цепочка узлов в куче: дно стека - это NULL в поле prev последнего узла

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

В прошлом уроке стек жил в сплошном массиве, и лимит был зашит в код. Теперь пойдем другим путем: будем выделять память под каждый элемент отдельно - в куче (heap). Элементы окажутся разбросаны по памяти, поэтому их нужно связать: каждый узел держит указатель на своего "соседа снизу". Получается однонаправленный связный список, у которого голова цепочки и есть вершина стека.

Зачем это нужно

Главный выигрыш - исчезает Stack Overflow из-за жестко заданного размера. Стек на динамических объектах может расти, пока в оперативной памяти есть место. Это важно для алгоритмов, у которых глубина вложенности заранее непредсказуема: обход глубоких деревьев, разбор выражений, длинные цепочки отмены действий.

Как организовано

Понадобятся две структуры:

Операции - чистая работа с указателями:

Обе операции по-прежнему O(1), но теперь внутри каждой прячется обращение к менеджеру памяти - malloc или free. Это и есть цена свободы: накладные расходы на каждый элемент вместо одного большого массива.

А как в Python? Связывать узлы указателями вручную не придется: обычный list растет практически без ограничений. Правда, внутри CPython это не связный список, а динамически расширяемый массив с запасом памяти - поэтому append() и pop() так быстры. Честный связный список в стандартной библиотеке - collections.deque.

Реализация на C

Стек на связном списке: код целиком C, 40 строк
#include <stdlib.h>
#include <stdbool.h>

/* Узел динамического стека */
typedef struct Item {
    int data;
    struct Item* prev; /* элемент, лежащий ПОД текущим */
} Item;

/* Стек: указатель на вершину и счетчик элементов */
typedef struct {
    Item* top;
    int size;
} Stack;

void Create(Stack* s) {
    s->top = NULL; /* NULL - это дно стека */
    s->size = 0;
}

bool Push(Stack* s, int val) {
    Item* newItem = (Item*)malloc(sizeof(Item));
    if (!newItem) return false; /* память закончилась */

    newItem->data = val;
    newItem->prev = s->top; /* подвязываем к прежней вершине */
    s->top = newItem;       /* новый узел стал вершиной */
    s->size++;
    return true;
}

bool Pop(Stack* s) {
    if (s->size == 0) return false;

    Item* i = s->top;      /* запоминаем узел на удаление */
    s->top = s->top->prev; /* вершина опускается на слой ниже */
    free(i);               /* возвращаем память системе */
    s->size--;
    return true;
}

Обратите внимание на порядок действий в Pop: сначала перенести top, потом free. Освободить узел раньше времени - значит потерять единственную ссылку на остальную часть стека.

Массив или список: что выбрать

Интересный факт: из двух стеков можно собрать очередь - элементы переливаются во вспомогательный стек и обратно, дважды инвертируясь и восстанавливая порядок. Эту задачу мы разберем в теме "Очередь и дек".

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

  • Стек на динамических объектах - однонаправленный связный список узлов.
  • Узел = данные + указатель prev на элемент снизу; стек хранит только top.
  • Push: malloc узла, prev = top, top = новый. Pop: top = top->prev, free.
  • Пустой стек - top == NULL; лимита размера нет, пока есть память.
  • Цена: обращение к malloc/free на каждую операцию и память под указатели.

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

В пустой стек на связном списке выполнили Push(10), затем Push(20). Куда указывает поле prev узла с числом 20?

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