Как устроен стек (stack) в C++?

Ответ

Стек — это структура данных, работающая по принципу LIFO (Last In, First Out). В C++ стек обычно реализуется как адаптер поверх других контейнеров (по умолчанию deque) в стандартной библиотеке (std::stack).

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

  • push() — добавление элемента на вершину стека.
  • pop() — удаление элемента с вершины.
  • top() — получение верхнего элемента без удаления.
  • empty() — проверка на пустоту.
  • size() — получение количества элементов.

Пример использования std::stack:

#include <iostream>
#include <stack>

int main() {
    std::stack<int> s;

    s.push(10); // Стек: [10]
    s.push(20); // Стек: [10, 20]
    s.push(30); // Стек: [10, 20, 30]

    std::cout << "Верхний элемент: " << s.top() << std::endl; // 30

    s.pop(); // Удаляем 30
    std::cout << "После pop(): " << s.top() << std::endl; // 20

    std::cout << "Размер стека: " << s.size() << std::endl; // 2
    std::cout << "Стек пуст? " << (s.empty() ? "Да" : "Нет") << std::endl;

    return 0;
}

Ключевые особенности и внутреннее устройство:

  1. Временная сложность: Все основные операции (push, pop, top, empty) выполняются за O(1).
  2. Реализация: Можно выбрать базовый контейнер через второй шаблонный параметр:
    std::stack<int, std::vector<int>> stack_vec; // На основе vector
    std::stack<int, std::list<int>> stack_list;  // На основе list
  3. Ограничения: Нет прямого доступа к элементам кроме верхнего, нет итераторов.
  4. Типичное применение:
    • Управление вызовами функций (стек вызовов)
    • Алгоритмы обхода графов (DFS)
    • Парсинг выражений (проверка скобок)
    • Реализация отмены операций (undo/redo)

Ручная реализация на массиве:

class ArrayStack {
private:
    int* arr;
    int capacity;
    int topIndex;

public:
    ArrayStack(int size) : capacity(size), topIndex(-1) {
        arr = new int[capacity];
    }

    ~ArrayStack() { delete[] arr; }

    void push(int value) {
        if (topIndex >= capacity - 1) {
            throw std::overflow_error("Stack overflow");
        }
        arr[++topIndex] = value;
    }

    int pop() {
        if (isEmpty()) {
            throw std::underflow_error("Stack underflow");
        }
        return arr[topIndex--];
    }

    int top() const {
        if (isEmpty()) throw std::runtime_error("Stack is empty");
        return arr[topIndex];
    }

    bool isEmpty() const { return topIndex == -1; }
};

Ответ 18+ 🔞

А, стек, говоришь? Ну это ж классика, ёпта, как бутерброд с колбасой в мире структур данных. Представь себе стопку тарелок, блядь, или патроны в магазине автомата — какой положил последним, тот и забираешь первым. Это и есть LIFO, или, как я люблю говорить, «последний зашёл — первый вышел, иди нахуй».

Основные движухи у стека:

  • push() — запихнуть элемент на самый верх. Как последнюю пельмешку в уже полную тарелку.
  • pop() — снять этот самый верхний элемент и выкинуть. Просто взял и снял, нихуя не глядя.
  • top() — посмотреть, что там наверху лежит, но не трогать. Подсмотреть, так сказать.
  • empty() — проверить, а не пустой ли этот стек, как твои карманы после зарплаты.
  • size() — посчитать, сколько всего там навтыкано.

Вот, смотри, как этим пользоваться в C++:

#include <iostream>
#include <stack>

int main() {
    std::stack<int> s;

    s.push(10); // Засунули 10
    s.push(20); // Засунули 20
    s.push(30); // Засунули 30. Теперь наверху 30, как самый наглый.

    std::cout << "Верхний элемент: " << s.top() << std::endl; // 30, ебать!

    s.pop(); // Выкинули 30 к хуям
    std::cout << "После pop(): " << s.top() << std::endl; // Теперь 20 рулит

    std::cout << "Размер стека: " << s.size() << std::endl; // 2
    std::cout << "Стек пуст? " << (s.empty() ? "Да" : "Нет") << std::endl; // Нет, пока ещё есть что вынимать

    return 0;
}

А теперь под капотом, чувак:

  1. Скорость: Все эти операции — push, pop, top — делаются за O(1). То есть моментально, нихуя не думая. Красота.
  2. Внутренности: По умолчанию эта std::stack — просто хитрая жопа, которая сидит на deque. Но можно сказать ей: «Слушай, давай на векторе» или «Давай на списке». Вторым параметром шаблона.
    std::stack<int, std::vector<int>> stack_vec; // Теперь на векторе
    std::stack<int, std::list<int>> stack_list;  // А теперь на списке, ядрёна вошь
  3. Ограничения: Сюда нельзя воткнуть палец посередине и что-то выковырять. Только верхушку. Итераторов нет, нихуя. Принцип «не суй нос, куда не просят».
  4. Где это нужно: Ну, везде, блядь! Стек вызовов функций, когда одна вызывает другую — это же чистый LIFO. Обход графов в глубину (DFS). Проверка, правильно ли расставлены скобки в выражении. Или вот «отмена» в фотошопе — последнее действие отменяешь первым.

А если вдруг захотелось сделать свой, на массиве, вот тебе голая правда:

class ArrayStack {
private:
    int* arr;       // Сам массив, где всё хранится
    int capacity;   // Сколько всего можно засунуть
    int topIndex;   // Индекс той самой верхушки

public:
    ArrayStack(int size) : capacity(size), topIndex(-1) {
        arr = new int[capacity]; // Выделили память
    }

    ~ArrayStack() { delete[] arr; } // Не забыть почистить за собой, а то утечёт

    void push(int value) {
        if (topIndex >= capacity - 1) {
            throw std::overflow_error("Stack overflow"); // Овердохуища! Места нет!
        }
        arr[++topIndex] = value; // Подняли индекс и запихнули значение
    }

    int pop() {
        if (isEmpty()) {
            throw std::underflow_error("Stack underflow"); // А вынимать-то нечего, подлец!
        }
        return arr[topIndex--]; // Вернули верхний и индекс опустили
    }

    int top() const {
        if (isEmpty()) throw std::runtime_error("Stack is empty"); // Пусто, бля!
        return arr[topIndex]; // Показываем, что наверху, но не трогаем
    }

    bool isEmpty() const { return topIndex == -1; } // Если индекс -1, значит, нихуя нет
};

Вот и вся магия. Просто, как три копейки, но без этого нихуя не работает. Главное — не переполнить и не пытаться вытащить из пустого, а то будет тебе underflow, и программа накрылась медным тазом.