Как устроена очередь (queue) в C++?

Ответ

Очередь (queue) в C++ — это контейнер-адаптер из STL, работающий по принципу FIFO (First In, First Out). Реализована как обертка над другими контейнерами, по умолчанию используется deque.

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

  • push() — добавление элемента в конец очереди (enqueue).
  • pop() — удаление элемента из начала очереди (dequeue).
  • front() — доступ к первому элементу (тому, который будет извлечен следующим).
  • back() — доступ к последнему добавленному элементу.
  • empty() — проверка на пустоту.
  • size() — получение количества элементов.

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

#include <iostream>
#include <queue>

int main() {
    std::queue<int> q;

    // Добавление элементов
    q.push(10); // Очередь: [10]
    q.push(20); // Очередь: [10, 20]
    q.push(30); // Очередь: [10, 20, 30]

    std::cout << "Первый элемент (front): " << q.front() << std::endl; // 10
    std::cout << "Последний элемент (back): " << q.back() << std::endl; // 30

    // Удаление элементов
    q.pop(); // Удаляем 10
    std::cout << "После pop(), front: " << q.front() << std::endl; // 20

    std::cout << "Размер очереди: " << q.size() << std::endl; // 2

    // Очистка очереди
    while (!q.empty()) {
        std::cout << q.front() << " ";
        q.pop();
    }
    // Вывод: 20 30

    return 0;
}

Особенности реализации:

  1. Базовый контейнер: Можно указать при создании:
    std::queue<int, std::list<int>> q_list;     // На основе list
    std::queue<int, std::deque<int>> q_deque;   // На основе deque (по умолчанию)
    // std::queue<int, std::vector<int>> q_vec; // Ошибка: vector не имеет pop_front()
  2. Ограничения:
    • Нет прямого доступа к элементам кроме первого и последнего
    • Нет итераторов
    • Для обхода очереди нужно последовательно извлекать элементы
  3. Временная сложность: Все операции O(1) при использовании deque или list в качестве базового контейнера.

Типичные сценарии использования:

  • Обработка задач в порядке поступления
  • Реализация BFS (поиска в ширину) для графов
  • Буферизация данных в producer-consumer паттернах
  • Обработка сообщений в системах очередей

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

template<typename T>
class ListQueue {
private:
    struct Node {
        T data;
        Node* next;
        Node(T val) : data(val), next(nullptr) {}
    };

    Node* frontNode;
    Node* rearNode;
    int count;

public:
    ListQueue() : frontNode(nullptr), rearNode(nullptr), count(0) {}

    ~ListQueue() {
        while (!isEmpty()) {
            dequeue();
        }
    }

    void enqueue(T value) {
        Node* newNode = new Node(value);
        if (isEmpty()) {
            frontNode = rearNode = newNode;
        } else {
            rearNode->next = newNode;
            rearNode = newNode;
        }
        count++;
    }

    T dequeue() {
        if (isEmpty()) throw std::runtime_error("Queue is empty");

        Node* temp = frontNode;
        T value = temp->data;
        frontNode = frontNode->next;

        if (frontNode == nullptr) {
            rearNode = nullptr;
        }

        delete temp;
        count--;
        return value;
    }

    T front() const {
        if (isEmpty()) throw std::runtime_error("Queue is empty");
        return frontNode->data;
    }

    bool isEmpty() const { return frontNode == nullptr; }
    int size() const { return count; }
};

Ответ 18+ 🔞

Давай разберем эту штуку, которая называется очередью, или queue, если по-ихнему. Представь себе самую обычную очередь, ну, как в столовой или за пивом. Кто первый пришёл — тот первый и обслужен. Это и есть принцип FIFO, или, как я люблю говорить, «первый зашёл — первый вышел, а последний пусть ждёт, пока не сдохнет». Ёпта, логика проще пареной репы.

Что она умеет, эта queue? Вот основные её фокусы, их всего ничего:

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

Вот смотри, как это в коде выглядит, чтоб совсем понятно стало:

#include <iostream>
#include <queue>

int main() {
    std::queue<int> q;

    // Закидываем народ в очередь
    q.push(10); // Стоит один [10]
    q.push(20); // К нему пристроился [10, 20]
    q.push(30); // И ещё один бедолага [10, 20, 30]

    // Смотрим, кто первый
    std::cout << "Первый на вылет (front): " << q.front() << std::endl; // 10
    // Смотрим, кто последний
    std::cout << "Последний пришёл (back): " << q.back() << std::endl; // 30

    // Обслужили первого — пока, 10!
    q.pop();
    std::cout << "После pop(), теперь front: " << q.front() << std::endl; // 20

    std::cout << "Сколько всего осталось: " << q.size() << std::endl; // 2

    // А теперь разгребаем очередь до конца, пока не опустеет
    while (!q.empty()) {
        std::cout << q.front() << " "; // Смотрим, кого выгоняем
        q.pop(); // И выгоняем
    }
    // Напечатает: 20 30
    // Всё, очередь накрылась медным тазом.

    return 0;
}

А теперь подводные камни, блядь:

  1. Из чего она сделана: По умолчанию она внутри использует deque (двустороннюю очередь), но можно сказать ей использовать list. А вот vector не подойдёт — у него нет pop_front(), он как тот пидарас шерстяной, только назад умеет, а спереди выковыривать — нет.
    std::queue<int, std::list<int>> q_list;   // На основе списка
    // std::queue<int, std::vector<int>> q_vec; // Ошибка! Нельзя, сука!
  2. Чего НЕЛЬЗЯ: Нельзя тыкать пальцем в середину очереди и говорить «эй ты, пятый, выходи». Доступ только к первому и последнему. Итераторов тоже нет — нечего там шастать. Чтобы всех посмотреть, надо всех по одному выпнуть.
  3. Скорость: Все основные операции — O(1), то есть мгновенно. Если, конечно, внутри не какой-нибудь мудя контейнер.

Где это нужно? Ну, задачи в планировщике, поиск в ширину по графу (BFS), всякие буферы, где что-то производится, а что-то потребляется — в общем, везде, где нужен честный порядок «живой очереди».

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

// ... (код ручной реализации остаётся без изменений, как в исходнике)

Смотри, тут всё просто: создаём узел, цепляем в хвост, с головы снимаем. Главное — не забыть всё почистить в деструкторе, а то память потечёт, как из дырявого ведра.