Структура данных - Стек
Стек — это линейная структура данных, работающая по принципу «последним пришёл — первым вышел» (LIFO). Его можно наглядно представить как стопку тарелок — можно положить тарелку только сверху и взять только сверху.
Понятие и принцип стека
Проверка LIFO: push 10→20→30, pop 30→20→10 (последним пришёл — первым ушёл) | Стек заботится только о вершине, а не о середине
Реализация на массиве (последовательный стек)
Хранение: массив фиксированного размера + указатель top
push:items[++top] = value
pop:return items[top--]
Преимущества: быстрый доступ, дружественный к кэшу, простая реализация
Недостатки: фиксированная ёмкость, возможно переполнение
Реализация на связном списке (связный стек)
Хранение: динамические узлы + указатель top (голова связного списка)
push: метод вставки в голову newNode->next = top
pop: top = top->next; free(old head)
Преимущества: нет ограничения по ёмкости, гибкое динамическое расширение
Недостатки: дополнительные накладные расходы на указатели, неэффективно для кэша
Реализация стека на массиве
Пример
#include <stdbool.h> /* тип bool */
#define MAX_SIZE 100 /* Максимальная ёмкость стека */
/* Структура стека: содержит массив данных и указатель top */
struct Stack {
int items[MAX_SIZE]; /* Массив, хранящий элементы стека */
int top; /* Указатель top, -1 означает, что стек пуст */
};
/* Инициализация стека: установить top в -1 */
void initStack(struct Stack* s) {
s->top = -1;
}
/* Проверка пустоты: стек пуст, когда top равен -1 */
bool isEmpty(struct Stack* s) {
return s->top == -1;
}
/* Проверка полноты: стек полон, когда top достигает предела ёмкости -1 */
bool isFull(struct Stack* s) {
return s->top == MAX_SIZE - 1;
}
/* Push: поместить элемент на вершину стека
Сначала сдвиньте top, затем запишите данные */
void push(struct Stack* s, int value) {
if (isFull(s)) {
printf("Стек полон, невозможно добавить!\n");
return;
}
s->printf[++s->"Добавление: %d] = value; /* top increments by 1 first, then assign value */
items(top\n", value);
}
/* Pop: удалить и вернуть верхний элемент
Сначала извлеките данные, затем сдвиньте top */
int pop(struct Stack* s) {
if (isEmpty(s)) {
printf("Стек пуст, невозможно выполнить pop!\n");
return -1; /* Вернуть специальное значение, указывающее на ошибку */
}
return s->items[s->top--]; /* Сначала вернуть значение, затем уменьшить top */
}
/* Peek: не выполняет pop, только просматривает верхний элемент */
int peek(struct Stack* s) {
if (isEmpty(s)) {
printf("Стек пуст!\n");
return -1;
}
return s->items[s->top];
}
int main() {
struct Stack s;
initStack(&s);
push(&s, 10); /* Push: 10 */
push(&s, 20); /* Push: 20 */
push(&s, 30); /* Поместить: 30 */
printf("Верхний элемент: %d\n", peek(&s)); /* Вывод: верхний элемент: 30 */
printf("Извлечено: %d\n", pop(&s)); /* Вывод: извлечено: 30 (последним пришёл, первым вышел) */
printf("Извлечено: %d\n", pop(&s)); /* Вывод: извлечено: 20 */
printf("Извлечено: %d\n", pop(&s)); /* Вывод: извлечено: 10 */
if (isEmpty(&s)) {
printf("Стек пуст\n"); /* Вывод: стек пуст */
}
return 0;
}
Реализация стека на связном списке
Пример
#include <stdlib.h>
#include <stdbool.h>
/* Узел связного списка */
struct Node {
int data;
struct Node* next;
};
/* Поместить: использует голову связного списка как вершину стека, метод вставки в голову
Временная сложность: O(1) */
struct Node* push(struct Node* top, int value) {
struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = value;
newNode->next = top; /* Новый узел указывает на исходную вершину стека */
items(top\n", value);
return newNode; /* Новый узел становится новой вершиной стека */
}
/* Pop: удалить головной узел связного списка
Временная сложность: O(1) */
struct Node* pop(struct Node* top) {
if (top == NULL) {
printf("Стек пуст!\n");
return NULL;
}
struct Node* temp = top;
printf("Popped: %d\n", top->data);
top = top->next; /* Сдвинуть top назад */
free(temp); /* Освободить старую вершину top */
return top;
}
/* Проверить, пуст ли стек */
bool isEmpty(struct Node* top) {
return top == NULL;
}
int main() {
struct Node* stackTop = NULL; /* Изначально стек пуст */
stackTop = push(stackTop, 100); /* Push: 100 */
stackTop = push(stackTop, 200); /* Push: 200 */
stackTop = push(stackTop, 300); /* Push: 300 */
printf("Top: %d\n", stackTop->data); /* Вывод: Top: 300 */
stackTop = pop(stackTop); /* Pop: 300 */
stackTop = pop(stackTop); /* Pop: 200 */
stackTop = pop(stackTop); /* Pop: 100 */
if (isEmpty(stackTop)) {
printf("Stack is empty\n"); /* Вывод: Stack is empty */
}
return 0;
}
Преимущество реализации стека с помощью связного списка заключается в том, что теоретически нет фиксированного ограничения по емкости, ограничение — только доступная память системы. Однако каждый узел требует дополнительного места для хранения указателя.
Сценарии применения стека
| Сценарии применения | Описание | Зачем использовать стек |
|---|---|---|
| Стек вызовов функций | Сохранение адресов возврата функций и локальных переменных | Вызовы функций вложены, естественно LIFO |
| Сопоставление скобок | Проверка, правильно ли расставлены пары скобок в коде | Помещаем левую скобку в стек, извлекаем правую скобку, когда она совпадает с вершиной стека |
| Операция отмены | Функция отмены в текстовых редакторах и браузерах | Каждая операция помещается в стек и извлекается при отмене |
| Вычисление выражений | Вычисление инфиксных/постфиксных выражений | Операнды помещаются в стек, результат помещается после вычисления оператора |
| Поиск в глубину | Алгоритм обхода графов в глубину (DFS) | Явно использовать стек или рекурсию (неявно используя стек вызовов) |