Всем привет! Давно хотел собрать пост по структурам данным, которые есть в C++ и кратенько описать преимущество каждой из них.
В первой итерации статьи начнем с тех, что есть в стандартной библиотеке STL.

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

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

1. Общая классификация контейнеров STL

Категории

Примеры

Характеристика

Последовательные

vector, deque, list, forward_list, array

Элементы упорядочены по позиции

Ассоциативные

set, map, multiset, multimap

Хранят ключи в отсортированном виде (деревья)

Неупорядоченные

unordered_set, unordered_map, unordered_multiset, unordered_multimap

Основаны на хеш-таблицах

Адаптеры

stack, queue, priority_queue

Используют другие контейнеры как основу

Начнем с последовательных контейнеров:

1) std::vector - Динамический массив с непрерывным размещением элементов в памяти.

std::vector<int> v {1, 2, 3};
v.push_back(4);
v.emplace_back(5);

Внутреннее устройство:
Контейнер поддерживает три указателя:

  • на начало массива,

  • на конец заполненной части,

  • и на конец выделенной памяти (capacity).

Когда текущая ёмкость (capacity) исчерпана, при следующем push_back происходит реаллокация — выделяется новый участок памяти, обычно увеличенный в 1.5–2 раза, и все элементы копируются или перемещаются в него и тут внимание на таблицу с асимптотикой:

Операция

Средняя сложность

Худший случай

push_back()

O(1) амортизированно

O(n) при реаллокации

insert()/erase() в середине

O(n)

O(n)

Доступ по индексу (operator[])

O(1)

O(1)

pop_front()

O(n)

O(n)

Особенности:

  • При реаллокации все итераторы, указатели и ссылки на элементы становятся недействительными.

  • Гарантируется непрерывное размещение элементов (&v[0] — указатель на блок памяти).

  • Отличная кэш-локальность: последовательный обход элементов максимально эффективен.

Лучше использовать когда нужен быстрый доступ по индексу и частые добавления в конец, а вставки в середину и удаление в начале редкие.

2) std::deque - Двусторонняя очередь(double-ended queue) реализована как массив блоков фиксированного размера, управляемый таблицей указателей. Это позволяет эффективно добавлять и удалять элементы с обоих концов, не перемещая весь контейнер.

std::deque<int> d {1, 2, 3};
d.push_front(0);
d.push_back(4);

Операция

Средняя сложность

Худший случай

push_front() / push_back()

O(1)

O(1)

insert() в середину

O(n)

O(n)

Доступ по индексу (operator[])

O(1)

O(1)

Особенности:

  • Элементы могут храниться в непоследовательных блоках памяти (в отличие от vector).

  • Кэш-локальность ниже, чем у vector.

  • Итераторы стабильнее: добавление элементов не инвалидирует существующие, кроме редких случаев при перераспределении блоков.

  • Используется как базовый контейнер для std::queue и std::stack(о них ниже).

Используется когда важны операции добавления и удаления с обоих концов, а произвольный доступ в середину всё ещё нужен.

3) std::list - Двусвязный список реализована так, что каждый элемент хранит указатели на предыдущий и следующий узел. Элементы не лежат в непрерывной памяти, что делает вставки и удаления эффективными, но ухудшает кэш-локальность.

std::list<int> l {1, 2, 3};
auto it = l.begin();
std::advance(it, 1);
l.insert(it, 42);

Внутреннее устройство:

  • Каждый узел содержит три поля: prev, next, value.

  • Хранение разрознено в памяти: каждый элемент выделяется отдельно.

Операция

Средняя сложность

Комментарий

Вставка/удаление по итератору

O(1)

только перенастройка ссылок

Поиск по значению

O(n)

требуется полный обход

Итерация

O(n)

линейная, плохо кэшируется

Особенности:

  • Итераторы не инвалидируются при вставках/удалениях (кроме удалённого элемента).

  • Нет доступа по индексу.

  • Высокие накладные расходы по памяти (указатели на соседей).

  • Плохая кэш-локальность: CPU часто промахивается по кэшу.

Используется когда когда важна стабильность итераторов и часто удаляются элементы в середине, также для реализации LRU-кэшей, очередей задач, логов с удалением из середины.

4) std::forward_list - односвязный список, реализован как двусвязный список, с итерацией только вперед.

std::forward_list<int> fl {1, 2, 3};
fl.push_front(0);

Внутреннее устройство:

  • Каждый элемент хранит только указатель на следующий.

  • Нет size() и двунаправленных итераторов.

  • Добавление возможно только через push_front() или insert_after().

Операция

Средняя сложность

Вставка/удаление после итератора

O(1)

Поиск

O(n)

Итерация

O(n)

Особенности:

  • Минимальные накладные расходы (1 указатель).

  • Хорош для потоковых или однопроходных алгоритмов.

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

5) std::array - статический массив, безопасная обёртка над обычным C-массивом с интерфейсом STL. Размер задаётся на этапе компиляции, динамических аллокаций нет.

std::array<int, 3> arr  {1, 2, 3};
arr[1] = 42;

Внутреннее устройство:

  • Элементы располагаются в непрерывной области памяти (аналог T[N]).

  • Размер — часть типа (std::array<int, 3> и std::array<int, 4> — разные типы).

  • Не выделяет память динамически.

Операция

Средняя сложность

Комментарий

Доступ (operator[], at)

O(1)

мгновенный

Итерация

O(n)

линейная

Изменение размера

-

размер фиксирован

Особенности:

  • Полностью совместим с алгоритмами STL.

  • at() проверяет границы (выбрасывает std::out_of_range).

  • Отличная кэш-локальность.

Банальная обертка над статическим массивом, применение как и у него.

Ассоциативные контейнеры (упорядоченные)

Ассоциативные контейнеры основаны на сбалансированных деревьях поиска (обычно красно-чёрных). Они обеспечивают упорядоченность элементов и логарифмическое время поиска.

1) std::set - множество уникальных элементов, отсортированных по ключу. Реализовано на сбалансированном красно-чёрном дереве. Обеспечивает логарифмический доступ (O(log n)). Подходит для хранения уникальных данных с упорядоченным обходом.

 std::set<int> s = {3, 1, 2};
    s.insert(4);              // добавляем элемент
    s.insert(2);              // дубликат игнорируется

std::multiset - вариант set, допускающий дубликаты ключей. Сохраняет отсортированный порядок и аналогичную асимптотику (O(log n)). Применяется при необходимости хранить повторяющиеся элементы с автоматической сортировкой.

std::multiset<int> ms = {1, 2, 2, 3};
    ms.insert(2);
    ms.erase(2); // удалит только один экземпляр

Внутреннее устройство:

  • Каждый элемент — узел дерева, хранящий ключ и указатели на потомков.

  • При вставке/удалении дерево автоматически ребалансируется.

  • multiset допускает дубликаты элементов, set — нет.

Операция

Средняя сложность

Вставка / удаление / поиск

O(log n)

Итерация (в порядке сортировки)

O(n)

Особенности:

  • Элементы всегда отсортированы.

  • Итераторы стабильны при вставке.

  • Использует больше памяти (каждый элемент — отдельный узел).

Использовать если надо: Хранение уникальных значений с автоматической сортировкой или
реализация таблиц лидеров, рейтингов, топ-N списков, или алгоритмы, где важен поиск по диапазону (lower_bound, upper_bound).

2) std::map - ассоциативный массив «ключ → значение», реализованный через красно-чёрное дерево. Все ключи уникальны, элементы хранятся отсортированными. Доступ и вставка выполняются за O(log n).

 std::map<std::string, int> ages = {
        {"Alice", 25}, {"Bob", 30}
    };
    ages["Charlie"] = 28;

std::multimap - вариант map, допускающий несколько значений для одного ключа. Удобен при моделировании связей “один-ко-многим” (например, индексных таблиц или группировок).

// multimap: несколько значений для одного ключа
std::multimap<std::string, int> scores;
    scores.insert({"Alice", 90});
    scores.insert({"Alice", 95});
    scores.insert({"Bob", 88});

    auto range = scores.equal_range("Alice");
    for (auto it = range.first; it != range.second; ++it)
        std::cout << it->first << ": " << it->second << "\n";

Внутреннее устройство:

  • То же сбалансированное дерево, что и в set.

  • Каждый элемент — пара (ключ, значение).

  • В map ключи уникальны, multimap допускает несколько элементов с одинаковыми ключами.

Операция

Средняя сложность

Вставка / удаление / поиск

O(log n)

Итерация по ключам

O(n)

Особенности:

  • operator[] создаёт новый элемент, если ключ не найден.

  • Поддерживает пользовательский компаратор.

  • Итераторы не инвалидируются при вставке.

Использовать если надо: Ассоциативные словари, индексы и конфигурационные таблицы, когда нужен отсортированный порядок ключей, гуд еще при работе с диапазонами или иерархическими данными (например, интервалы времени).

Неупорядоченные контейнеры (хеш-таблицы)

Неупорядоченные контейнеры (семейство unordered_) реализованы через хеш-таблицы, где доступ к элементу осуществляется по вычисленному хешу ключа.
Средняя сложность основных операций (insert, find, erase) — O(1), но в худшем случае (при коллизиях) может достигать O(n).*

1) std::unordered_set - множество уникальных элементов, реализованное через хеш-таблицу. Среднее время доступа — O(1), худшее — O(n) при коллизиях. Элементы не сортируются, порядок не определён.

std::unordered_set<std::string> users = {"Alice", "Bob"};
    users.insert("Charlie");

    if (users.contains("Bob"))
        std::cout << "Bob найден!\n";

    for (auto& u : users)
        std::cout << u << " ";

std::unordered_multiset - аналог unordered_set, но допускает дубликаты. Эффективен при частом поиске и подсчёте повторяющихся значений без необходимости сортировки.

std::unordered_multiset<int> nums;
    nums.insert(10);
    nums.insert(10);
    nums.insert(20);

    std::cout << "Количество 10: " << nums.count(10) << "\n";

Внутреннее устройство:

  • Массив корзин (buckets), каждая содержит список элементов с одинаковым хешем.

  • При увеличении числа элементов выполняется rehash() — перераспределение элементов между корзинами.

  • Хеш-функция (std::hash) определяет, в какую корзину попадёт элемент.

Операция

Средняя сложность

Худший случай

Вставка / удаление / поиск

O(1)

O(n)

Итерация (в порядке сортировки)

O(n)

O(n)

Особенности:

  • Элементы не сортируются.

  • Итераторы инвалидируются при rehash().

  • Качество хеш-функции влияет на производительность.

Быстрые проверки наличия элементов, реализация кэшей, множеств токенов, фильтров,используется вместо set, когда порядок не важен.

2) std::unordered_map - ассоциативный контейнер «ключ → значение», построенный на хеш-таблице. Обеспечивает средний доступ O(1). Порядок элементов не гарантируется. Используется для быстрых словарей и кэшей.

std::unordered_map<std::string, int> ages = {
        {"Alice", 25}, {"Bob", 30}
    };
    ages["Charlie"] = 28;

    for (auto& [name, age] : ages)
        std::cout << name << ": " << age << "\n";

std::unordered_multimap - аналог unordered_map, допускающий несколько значений на один ключ. Применяется для реализации индексных структур и групп данных.

std::unordered_multimap<std::string, int> scores;
    scores.insert({"Alice", 90});
    scores.insert({"Alice", 95});
    scores.insert({"Bob", 88});

    auto range = scores.equal_range("Alice");
    for (auto it = range.first; it != range.second; ++it)
        std::cout << it->first << ": " << it->second << "\n";

Внутреннее устройство:

  • Таблица корзин, каждая хранит список пар (ключ, значение).

  • При росте размера выполняется rehash() для перераспределения элементов.

  • Хеш-функция и оператор == используются для сравнения ключей.

Операция

Средняя сложность

Худший случай

Вставка / удаление / поиск

O(1)

O(n)

Итерация (в порядке сортировки)

O(n)

O(n)

Особенности:

  • Порядок элементов не определён.

  • При rehash() итераторы становятся недействительными.

  • Поддерживает собственные хешеры и компараторы.

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

Адаптеры контейнеров STL

Адаптеры контейнеров — это обёртки над другими контейнерами (vector, deque, list), которые ограничивают интерфейс и предоставляют специализированное поведение. Они не хранят данные напрямую, а используют внутренний контейнер, скрывая лишние операции.
Типы адаптеров:

  • std::stack — стек (LIFO)

  • std::queue — очередь (FIFO)

  • std::priority_queue — очередь с приоритетами (heap)

1) std::stack - реализует стек (LIFO — Last In, First Out): последний добавленный элемент извлекается первым. По умолчанию использует std::deque, но можно указать и другой контейнер (например, std::vector или std::list).

std::stack<int> st;
    st.push(10);
    st.push(20);
    st.push(30);

    std::cout << "Top: " << st.top() << "\n"; // 30
    st.pop();
    std::cout << "Now top: " << st.top() << "\n"; // 20

Пример со std::vector как базовым контейнером

std::stack<int, std::vector<int>> st;
    for (int i = 1; i <= 5; ++i)
        st.push(i * 10);

    while (!st.empty()) {
        std::cout << st.top() << " ";
        st.pop();
    }
    // Вывод: 50 40 30 20 10

Внутреннее устройство:

  • По умолчанию хранит элементы в std::deque.

  • Интерфейс ограничен методами: push(), pop(), top(), empty(), size().

  • Нет доступа к элементам, кроме верхнего (top()).

Операция

Средняя сложность

Комментарий

push(), pop(), top()

O(1)

мгновенный

Особенности:

  • Не предоставляет итераторов.

  • Можно выбрать базовый контейнер при создании.

  • Никакой сортировки или поиска по значению.

Используется для: алгоритмы обхода графов (DFS), парсеры выражений и анализаторы синтаксиса, реализация undo/redo стеков и вызовов функций.

2) std::queue - классическая очередь FIFO (First In, First Out). Первый вставленный элемент извлекается первым. По умолчанию использует std::deque как базовый контейнер.

std::queue<int> q;
    q.push(10);
    q.push(20);
    q.push(30);

    std::cout << "Front: " << q.front() << "\n"; // 10
    q.pop();
    std::cout << "Now front: " << q.front() << "\n"; // 20

Очередь строк

std::queue<std::string> q;
    q.push("task_1");
    q.push("task_2");
    q.push("task_3");

    while (!q.empty()) {
        std::cout << "Processing: " << q.front() << "\n";
        q.pop();
    }

Внутреннее устройство:

  • Использует std::deque по умолчанию.

  • Методы интерфейса: push(), pop(), front(), back(), empty(), size().

  • Не поддерживает произвольный доступ и итераторы.

Операция

Средняя сложность

Комментарий

push(), pop(), front(), back()

O(1)

мгновенный

Особенности:

  • Принцип работы — FIFO.

  • Можно задать базовый контейнер (например, list).

  • Безопасен для использования в потоках при внешней синхронизации.

Используется для реализации: Планировщики задач и событий, очереди запросов в многопоточном окружении, буферизация сетевых сообщений и потоков данных.

3) std::priority_queue - очередь с приоритетом, использующая vector и кучу (heap). Элемент с наивысшим приоритетом всегда доступен через top(). Вставка и удаление — O(log n). Применяется в алгоритмах поиска пути и планировании задач.
По умолчанию реализует макс-кучу (max-heap) на базе std::vector. Для обратного порядка можно использовать std::greater.

 std::priority_queue<int> pq;
    pq.push(5);
    pq.push(10);
    pq.push(3);

    std::cout << "Top: " << pq.top() << "\n"; // 10
    pq.pop();
    std::cout << "Now top: " << pq.top() << "\n"; // 5

Очередь с минимальным приоритетом (min-heap)

std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;
    min_pq.push(50);
    min_pq.push(10);
    min_pq.push(30);

    while (!min_pq.empty()) {
        std::cout << min_pq.top() << " ";
        min_pq.pop();
    }
    // Вывод: 10 30 50

Внутреннее устройство:

  • Хранит элементы в std::vector.

  • Использует алгоритмы std::make_heap, push_heap, pop_heap.

  • Гарантирует, что элемент с наибольшим приоритетом всегда первый.

Операция

Средняя сложность

push() / pop()

O(log n)

top()

O(1)

Особенности:

  • Не предоставляет итераторов и прямого доступа к элементам.

  • Можно задать произвольный компаратор (std::greater, пользовательский).

  • Поддерживает частично отсортированные наборы.

Используется для: алгоритмы поиска кратчайшего пути (Дейкстра, A*), планировщики задач и таймеров, очереди обработки событий по приоритету.

Сводная таблица контейнеров STL

В статье я постарался охватить все контейнеры стандартной библиотеки C++.
Ниже представлю краткое сравнение их характеристик, асимптотики и областей применения.

Последовательные контейнеры

Контейнер

Основная идея

Доступ

Вставка в конец

Вставка в начало / середину

Память / структура

Особенности

Где использовать

std::array

Статический массив фиксированной длины

O(1)

Непрерывная память (стек)

Без динамических аллокаций

Малые фиксированные структуры (векторы, матрицы)

std::vector

Динамический массив с реаллокацией

O(1)

O(1) аморт., O(n) худший

O(n)

Непрерывная память (куча)

Высокая кэш-локальность, возможна реаллокация

Универсальный контейнер, массивы, буферы

std::deque

Массив блоков (двусторонняя очередь)

O(1)

O(1)

O(1) / O(n)

Массив блоков в куче

Добавление с обоих концов

Очереди, буферы, оконные алгоритмы

std::list

Двусвязный список

O(1)

O(1)

Узлы в куче (prev/next)

Итераторы стабильны, плохо кэшируется

Частые вставки/удаления в середину

std::forward_list

Односвязный список

O(1) (front)

Минимальные накладные расходы

Очень лёгкий, только прямой обход

Потоковые структуры, низкоуровневые списки

Ассоциативные контейнеры (упорядоченные)

Контейнер

Основная структура

Доступ / Поиск

Вставка / Удаление

Порядок

Особенности

Где использовать

std::set

Красно-чёрное дерево

O(log n)

O(log n)

Отсортирован

Уникальные значения

Упорядоченные множества, ранжирование

std::multiset

Красно-чёрное дерево

O(log n)

O(log n)

Отсортирован

Разрешены дубликаты

Частотные таблицы, рейтинги

std::map

Красно-чёрное дерево (ключ → значение)

O(log n)

O(log n)

Отсортирован по ключу

operator[]; стабильные итераторы

Словари, таблицы конфигураций

std::multimap

Красно-чёрное дерево

O(log n)

O(log n)

Отсортирован по ключу

Несколько значений на ключ

Индексные таблицы, группировки

Неупорядоченные контейнеры (на хеш-таблицах)

Контейнер

Структура

Средняя / Худшая сложность

Упорядоченность

Особенности

Где использовать

std::unordered_set

Хеш-таблица

O(1) / O(n)

Нет

Быстрые lookup, без сортировки

Проверка наличия, фильтры, кэши

std::unordered_multiset

Хеш-таблица

O(1) / O(n)

Нет

Поддерживает дубликаты

Подсчёт частот, неупорядоченные наборы

std::unordered_map

Хеш-таблица (ключ → значение)

O(1) / O(n)

Нет

Пары ключ-значение, высокая скорость

Словари, таблицы маршрутов, парсеры

std::unordered_multimap

Хеш-таблица

O(1) / O(n)

Нет

Несколько значений на ключ

Группировки данных, кэширование

Адаптеры контейнеров

Контейнер

Базовый контейнер

Принцип

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

Сложность

Где использовать

std::stack

deque (по умолчанию)

LIFO

push, pop, top

O(1)

Рекурсии, парсеры, обходы графов

std::queue

deque

FIFO

push, pop, front, back

O(1)

Очереди задач, событий, потоков

std::priority_queue

vector (heap)

Приоритет

push, pop, top

O(log n)

Планировщики, алгоритмы Дейкстры

Итог

Надеюсь, тем, кто раньше не особо разбирался в STL, стало понятнее, какие контейнеры бывают, чем они отличаются и где их лучше использовать.

Я не ставил цель подробно описать весь функционал каждого контейнера — это заняло бы не одну статью. Хотел лишь кратко ввести в курс дела и показать, как устроены основные структуры данных, чтобы при работе с кодом было проще понимать, что происходит “под капотом”. Главное — понимать, как они работают внутри, и тогда выбор подходящего варианта станет очевиден.

Если после прочтения стало хоть немного понятнее, когда стоит брать vector, а когда лучше подойдёт map — значит, цель достигнута

Выбор контейнера, в зависимости от поставленной задачи

Цель

Лучший выбор

Почему

Быстрый произвольный доступ

std::vector

Непрерывная память, O(1) доступ

Частые вставки в середину

std::list

Стабильные итераторы, O(1) вставка

Очередь с двумя концами

std::deque

Эффективно добавляет с обоих концов

Быстрый поиск по ключу

std::unordered_map

Средняя сложность O(1)

Отсортированные данные

std::set / std::map

Гарантированный порядок элементов

Очередь по приоритету

std::priority_queue

На вершине элемент с max приоритетом

Минимальные накладные расходы

std::forward_list

Один указатель на следующий элемент

Небольшие фиксированные массивы

std::array

Без динамических аллокаций

Жуков Матвей /НИУ МЭИ ИВТИ /Кафедра управления и интеллектуальных технологий

C++ Разработчик

Комментарии (2)


  1. kovserg
    07.12.2025 19:29

    А как дела обстаят со структурами данных для растровой графики и линейной алгебры?


  1. CatAssa
    07.12.2025 19:29

    Хорошая статья-памятка.