Обзор структур данных в C++

Здравствуйте! Предлагаю обзор основных контейнеров C++ STL с пояснением их внутренней структуры, сложности операций и типичных сценариев применения.

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

STL содержит набор контейнеров для разных задач: от упорядоченных последовательностей до ассоциативных и хеш-структур. Давайте кратко рассмотрим их классификацию и особенности.

Классификация контейнеров STL

Категория Контейнеры Описание
Последовательные vector, deque, list, forward_list, array Элементы упорядочены по позиции
Ассоциативные упорядоченные set, multiset, map, multimap Красно-чёрные деревья, автоматическая сортировка по ключу
Хеш-контейнеры unordered_set, unordered_multiset, unordered_map, unordered_multimap Хеш-таблицы, быстрый доступ по ключу
Адаптеры stack, queue, priority_queue Обёртки над другими контейнерами с ограниченным интерфейсом

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

std::vector

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

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

В основе — три указателя: begin, end заполненной части и end выделенного буфера. При исчерпании ёмкости происходит реаллокация с расширением в ≈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)
  • Итераторы и указатели инвалидируются при реаллокации.
  • Гарантирует непрерывную область памяти, отличная кэш-локальность.
  • Лучше всего подходит для частых вставок в конец и редких операций внутри.

std::deque

Двусторонняя очередь, устроенная как массив мелких блоков под управлением таблицы указателей. Обеспечивает O(1) вставки/удаления на обоих концах.

std::deque d = {1, 2, 3};
d.push_front(0);
d.push_back(4);
Операция Средняя Худший
push_front/back() O(1) O(1)
insert() в середине O(n) O(n)
operator[] O(1) O(1)
  • Блоки могут храниться негомогенно, ниже кэш-локальность, чем у vector.
  • Итераторы в большинстве случаев остаются валидными.
  • Используется внутри queue и stack по умолчанию.

std::list

Двусвязный список, где каждый узел хранит ссылки на соседей. Поддерживает O(1) вставку и удаление по итератору.

std::list l = {1, 2, 3};
auto it = l.begin();
std::advance(it, 1);
l.insert(it, 42);
Операция Сложность Комментарий
insert/erase по итератору O(1) перенастройка ссылок
поиск по значению O(n) линейный обход
итерация O(n) низкая кэш-эффективность
  • Итераторы стабильны, кроме удаляемого элемента.
  • Нет доступа по индексу, значительные накладные расходы памяти.
  • Применяется при частых изменениях в середине и в алгоритмах LRU-кэша.

std::forward_list

Односвязный список с минимальным оверхедом: каждый элемент хранит указатель только на следующий узел.

std::forward_list fl = {1, 2, 3};
fl.push_front(0);
Операция Сложность
insert/erase после итератора O(1)
поиск O(n)
итерация O(n)
  • Подходит для однопроходных алгоритмов и встраиваемых систем.
  • Не поддерживает двунаправленный обход и размер за O(1).

std::array

Статический массив фиксированного размера, обёртка над C-массивом с интерфейсом STL.

std::array a = {1, 2, 3};
a[1] = 42;
Операция Сложность Комментарий
operator[]/at() O(1) мгновенно
итерация O(n) линейно
изменение размера фиксировано при компиляции
  • Совместим с алгоритмами STL, at() проверяет границы.
  • Отличная кэш-локальность без динамики.

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

Базируются на сбалансированных деревьях (обычно красно-чёрных), гарантируют отсортированный порядок и O(log n) для основных операций.

std::set / std::multiset

std::set хранит уникальные ключи, std::multiset допускает дубликаты.

std::set s = {3, 1, 2};
s.insert(4);
// s.insert(2) не добавит дубликат

std::multiset ms = {1, 2, 2, 3};
ms.insert(2);
ms.erase(2); // удалит только один экземпляр
Операция Сложность
insert/erase/find O(log n)
итерация O(n)
  • Итераторы стабильны при вставках.
  • Используется для рейтингов, топ-N, поиска по диапазону.

std::map / std::multimap

std::map — ассоциативный массив ключ→значение с уникальными ключами, std::multimap — допускает множественные значения на один ключ.

std::map m = {{"Alice",25}, {"Bob",30}};
m["Charlie"] = 28;

std::multimap mm;
mm.insert({"Alice",90});
mm.insert({"Alice",95});
auto range = mm.equal_range("Alice");
for (auto it = range.first; it != range.second; ++it)
    std::cout << it->first << ": " << it->second << "\n";
Операция Сложность
insert/erase/find O(log n)
итерация O(n)
  • operator[] создаёт элемент по ключу, если не существует.
  • Можно задавать свой компаратор.
  • Подходит для словарей, конфигураций, иерархических данных.

Хеш-контейнеры

Реализованы через хеш-таблицы, средняя сложность операций O(1), в худшем при коллизиях O(n).

std::unordered_set / std::unordered_multiset

unordered_set хранит уникальные элементы, unordered_multiset — допускает дубликаты.

std::unordered_set us = {"Alice","Bob"};
us.insert("Charlie");
if (us.contains("Bob")) std::cout<<"Bob есть\n";

std::unordered_multiset ums;
ums.insert(10); ums.insert(10);
std::cout<<"Число 10: "<
Операция Средняя Худший
insert/erase/find O(1) O(n)
итерация O(n) O(n)
  • Итераторы инвалидируются при rehash().
  • Используется для кэшей, фильтров, быстрых проверок наличия.

std::unordered_map / std::unordered_multimap

unordered_map — ассоциативный массив с хешем, unordered_multimap — несколько значений на ключ.

std::unordered_map um = {{"Alice",25},{"Bob",30}};
um["Charlie"] = 28;
for (auto& [k,v]:um) std::cout<
Операция Средняя Худший
insert/erase/find O(1) O(n)
итерация O(n) O(n)
  • Поддерживает кастомные хеш-функции и сравнения.
  • Используется в парсерах, кэшах, таблицах маршрутов.

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

Обёртки над другими контейнерами с урезанным интерфейсом и специализированным поведением.

std::stack

Стек LIFO. По умолчанию базируется на deque, но может использовать vector или list.

std::stack st;
st.push(10); st.push(20);
std::cout<
  • Операции push/pop/top за O(1).
  • Нет итераторов, доступ только к верхнему элементу.
  • Применяется в DFS, парсерах, undo/redo, вызовах функций.

std::queue

Очередь FIFO на базе deque по умолчанию.

std::queue q;
q.push(10); q.push(20);
std::cout<
  • push/front/back/pop за O(1).
  • Используется для планирования задач, сетевых буферов.

std::priority_queue

Очередь с приоритетом (макс-куча) на базе vector. Для min-heap используйте std::greater.

std::priority_queue pq;
pq.push(5); pq.push(10);
std::cout<, std::greater> minq;
minq.push(50); minq.push(10);
while(!minq.empty()){
  std::cout<
  • push/pop — O(log n), top — O(1).
  • Применяется в алгоритмах Дейкстры, планировщиках, таймерах.

Рекомендации по выбору контейнера

Задача Контейнер Причина
Быстрый доступ по индексу std::vector непрерывная память, O(1)
Частые вставки в середину std::list O(1), стабильные итераторы
Двусторонняя очередь std::deque эффективно оба конца
Быстрый поиск по ключу std::unordered_map O(1) в среднем
Отсортированные данные std::set / std::map гарантированный порядок
Приоритетная обработка std::priority_queue max-heap
Минимальные накладные расходы std::forward_list 1 указатель на элемент
Фиксированные небольшие массивы std::array нет динамики
 

Источник

Читайте также