Реализация стека через динамический список

Что такое стек

Стек - это абстрактный тип данных, представляющий собой коллекцию элементов, в которой добавление и удаление элементов происходит по принципу «последним вошёл — первым вышел» (LIFO).

Стек используется в задачах, где важно временное хранение данных с быстрым доступом к последнему добавленному элементу. Типичные примеры включают реализацию вызовов функций в исполнении программ, обратную польскую запись и отмену действий в приложениях.

Важно понимать, что стек — это не конкретная структура хранения, а интерфейс: стек можно реализовать через массив, через связный список или через другие структуры. В этом конспекте мы рассматриваем реализацию стека через динамический односвязный список.

Основные операции стека

Набор базовых операций стека невелик и интуитивно понятен: push — добавление элемента на вершину, pop — удаление элемента с вершины, top (или peek) — просмотр элемента на вершине без удаления, empty — проверка пустоты. С точки зрения сложности большинство реализаций стремится обеспечить для операций push и pop константную временную сложность O(1)O(1).

При добавлении элемента размер стека увеличивается, что формально можно записать как изменение количества элементов: nn+1n\to n+1. При удалении соответствующее уменьшение размера записывают как nn1n\to n-1.

Реализация через динамический список обеспечивает гибкое использование памяти: размер стека растёт и уменьшается по мере операций, в отличие от статического массива, где заранее требуется резервирование памяти. Это делает связный список удобным в задаче с непредсказуемым количеством элементов.

Структура узла и указатель вершины

Узел - элемент динамического списка, содержащий поле данных и ссылку на следующий узел.

В реализации стека через односвязный список каждый узел содержит полезные данные и указатель на следующий узел. Для управления стеком поддерживается внешний указатель вершины, обычно называемый top. При инициализации стека этот указатель не указывает ни на какой узел: top=NULL\mathrm{top}=\mathrm{NULL}.

Схема узла на логическом уровне проста: поле данных + указатель на следующий узел. На диаграмме памяти вершина указывает на первый узел списка, а последний узел имеет указатель, равный специальному значению конца списка, которое обычно эквивалентно NULL; в тексте это обозначается как отсутствие следующего узла.

Алгоритм операции push

Операция push создаёт новый узел, заполняет его поле данных и делает новый узел вершиной стека. При этом новый узел получает ссылку на предыдущую вершину, и затем внешний указатель вершины перенаправляется на этот новый узел. Логический эффект операции на счётчик элементов можно записать как size=size+1\mathrm{size}=\mathrm{size}+1.

Пошаговый алгоритм: создать узел, положить в него значение, присвоить его полю next ссылку на текущую вершину, затем установить вершину на этот новый узел. При корректной реализации память для нового узла выделяется динамически, а время выполнения операции обычно не зависит от числа элементов в стеке, что выражают формулой O(1)O(1).

Пример: при добавлении элемента в непустой стек новый узел получает ссылку на прежнюю вершину, после чего указатель вершины указывает на новый узел. (Схема иллюстрации: {IMAGE_0})

Алгоритм операции pop

Операция pop удаляет верхний узел: сохраняется ссылка на текущую вершину, вершина перемещается на следующий узел, затем память удаляемого узла освобождается. Логическое изменение счётчика элементов при этом описывается как size=size1\mathrm{size}=\mathrm{size}-1.

Пошагово: проверить, не пуст ли стек; если пуст — возвратить ошибку или специальное значение; иначе сохранить значение вершины, перенаправить указатель вершины на её поле next (то есть выполнить top=topnext\mathrm{top}=\mathrm{top}\rightarrow\mathrm{next}), освободить память удаляемого узла и вернуть сохранённое значение. Эта последовательность действий также имеет константную временную сложность O(1)O(1).

Преимущества и недостатки реализации через динамический список

Преимущества очевидны: динамическое выделение памяти позволяет не ограничивать размер стека заранее и экономно использовать память при небольшом числе элементов. Добавление и удаление выполняются быстро и обычно не требуют сдвигов элементов, как это бывает в массивной реализации.

К недостаткам относятся накладные расходы на хранение ссылок в каждом узле и дополнительная стоимость динамиального выделения/освобождения памяти, которая может быть заметна при интенсивных операциях. Также реализация чувствительна к ошибкам управления памятью: утечки и двойное освобождение — частые источники багов.

Примеры работы и тестирование

Для практического тестирования реализации полезно покрыть набор сценариев: операции на пустом стеке, последовательности push и pop, проверка порядка элементов (LIFO), а также поведение при больших объёмах данных. Автоматические тесты должны проверять корректность возвращаемых значений и состояние указателя вершины после каждой операции.

Пример теста: выполнение последовательности операций push, push, pop, top, pop должен вернуть значения в порядке, соответствующем принципу LIFO. Для проверки корректности также следует убедиться, что после окончательного удаления всех элементов вершина возвращается к состоянию, заданному выражением top=NULL\mathrm{top}=\mathrm{NULL}.

Практические советы по реализации

При реализации на языках с ручным управлением памятью важна аккуратная обработка ошибок выделения памяти: при неудаче создания узла операция push должна корректно сигнализировать об ошибке, не портя текущую структуру. На языках с автоматической сборкой мусора освобождение памяти выполняется автоматически, но всё равно следует избегать оставления «висящих» ссылок на удалённые узлы.

Если требуется хранить размер стека для быстрого ответа на запрос size, полезно поддерживать отдельную переменную-счётчик и обновлять её при каждой операци: увеличение при push и уменьшение при pop, что формально записывается как size=size+1\mathrm{size}=\mathrm{size}+1 и size=size1\mathrm{size}=\mathrm{size}-1 соответственно. Это избавит от необходимости проходить по списку для вычисления размера.

Заключение

Реализация стека через динамический список — эффективный и гибкий способ организовать структуру данных с поведением LIFO. Она проста в понимании и реализации, даёт гарантии по времени выполнения ключевых операций и подходит в тех случаях, когда объём данных заранее неизвестен или меняется динамически.

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