Стек. Физическое представление (динамические объекты): связный список узлов
prev
на элемент, лежащий под ним, а сам стек - один указатель top на вершину.
Такой стек растет, пока есть свободная память: переполнения POOL_SIZE
больше нет.
Стек. Физическое представление (динамические объекты): что это такое
В прошлом уроке стек жил в сплошном массиве, и лимит был зашит в код. Теперь пойдем другим путем: будем выделять память под каждый элемент отдельно - в куче (heap). Элементы окажутся разбросаны по памяти, поэтому их нужно связать: каждый узел держит указатель на своего "соседа снизу". Получается однонаправленный связный список, у которого голова цепочки и есть вершина стека.
Зачем это нужно
Главный выигрыш - исчезает Stack Overflow из-за жестко заданного размера. Стек на динамических объектах может расти, пока в оперативной памяти есть место. Это важно для алгоритмов, у которых глубина вложенности заранее непредсказуема: обход глубоких деревьев, разбор выражений, длинные цепочки отмены действий.
Как организовано
Понадобятся две структуры:
- Узел
Item: полезные данныеdata+ указательprevна элемент, положенный в стек до него (то есть лежащий ниже). - Сам стек: единственный указатель
topна вершину (и, для удобства, счетчикsize). Пустой стек - этоtop == NULL: нулевой указатель играет роль дна.
Операции - чистая работа с указателями:
- Push: выделить память под новый узел, записать в его
prevтекущийtop, перенестиtopна новый узел. - Pop: запомнить узел-вершину, опустить
topнаtop->prev, освободить память запомненного узла.
Обе операции по-прежнему O(1), но теперь внутри каждой прячется обращение
к менеджеру памяти - malloc или free. Это и есть цена
свободы: накладные расходы на каждый элемент вместо одного большого массива.
list растет практически без ограничений. Правда, внутри
CPython это не связный список, а динамически расширяемый массив с запасом памяти -
поэтому append() и pop() так быстры. Честный связный
список в стандартной библиотеке - collections.deque.
Реализация на C
Стек на связном списке: код целиком
#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. Освободить узел раньше времени - значит потерять
единственную ссылку на остальную часть стека.
Массив или список: что выбрать
- Массив: быстрее на практике (сплошная память, никакого malloc), но размер фиксирован.
- Связный список: нет лимита размера, но каждый Push и Pop ходит к менеджеру памяти, а узлы тратят место на указатели.
Интересный факт: из двух стеков можно собрать очередь - элементы переливаются во вспомогательный стек и обратно, дважды инвертируясь и восстанавливая порядок. Эту задачу мы разберем в теме "Очередь и дек".
Коротко: главное
- Стек на динамических объектах - однонаправленный связный список узлов.
- Узел = данные + указатель prev на элемент снизу; стек хранит только top.
- Push: malloc узла, prev = top, top = новый. Pop: top = top->prev, free.
- Пустой стек - top == NULL; лимита размера нет, пока есть память.
- Цена: обращение к malloc/free на каждую операцию и память под указатели.
Проверьте себя
В пустой стек на связном списке выполнили Push(10), затем Push(20). Куда указывает поле prev узла с числом 20?