Какую структуру данных из STL выберешь для хранения большого количества целых чисел с частой операцией добавления?

Ответ

Для сценария с частым добавлением элементов в конец оптимальным выбором будет std::vector<int>.

Причины:

  1. Амортизированная константная сложность O(1) для push_back(). Хотя при исчерпании capacity происходит дорогостоящее перевыделение памяти и копирование, стратегия геометрического роста (обычно в 1.5-2 раза) гарантирует, что эти операции редки, а их стоимость «размазывается» по многим дешёвым добавлениям.
  2. Отличная локальность данных. Элементы хранятся в непрерывном блоке памяти, что максимально эффективно для кэша процессора.
  3. Минимальные накладные расходы на хранение. Только данные и небольшой служебный overhead (указатели на начало, конец и конец выделенной памяти).

Критически важная оптимизация: Использование reserve() для предварительного выделения памяти, если примерный размер известен.

#include <vector>
#include <iostream>
#include <chrono>

int main() {
    std::vector<int> data;
    data.reserve(1'000'000); // Ключевая оптимизация: избегаем множественных перевыделений

    for (int i = 0; i < 1'000'000; ++i) {
        data.push_back(i); // Быстрое добавление
    }
    std::cout << "Size: " << data.size() << ", Capacity: " << data.capacity() << 'n';
    return 0;
}

Альтернативы и когда их рассматривать:

  • std::deque: Подходит, если нужны частые вставки и в начало, и в конец (O(1)), но с чуть худшей локальностью, чем у vector.
  • std::list / std::forward_list: Стоит выбрать только если нужны частые вставки/удаления в произвольных позициях (O(1) при наличии итератора), а не только в конце. Они имеют большое overhead на хранение указателей и плохую локальность.

Ответ 18+ 🔞

Смотри, тут вопрос про то, какой контейнер взять, если ты постоянно пихаешь элементы в самый конец. Ответ, который тебя спасёт от овердохуища проблем — это std::vector<int>, и сейчас объясню, почему.

Почему вектор — наш царь и бог в этой ситуации:

  1. Добавление — почти даром. У него push_back() работает в среднем за O(1). Да, иногда, когда места нет, он устраивает ёперный театр: находит новый кусок памяти побольше, копирует туда всё старьё, а старый кусок выкидывает. Но он не дурак, он увеличивает ёмкость не на один элемент, а в полтора-два раза. Так что эти дорогие операции случаются редко, а их стоимость размазывается по всем остальным быстрым добавлениям. В итоге — выгодно.
  2. Данные лежат рядышком. Все элементы сидят в памяти друг за дружкой, как солдаты в строю. Для кэша процессора это просто песня, он подгружает их пачками, и всё летает.
  3. Накладных расходов — кот наплакал. Хранит только сами данные и пару служебных указателей (где начало, где конец данных, а где конец выделенной памяти). Никаких лишних связей, как в списках.

Но есть одна хитрая жопа! Если ты заранее знаешь, что элементов будет, ну, миллион, то сделай ему reserve(). Скажи: «Вектор, готовь место!». И тогда он сразу выделит один большой кусок, и никаких переездов с копированием вообще не будет. Это как приехать на склад и сразу арендовать целый ангар, а не переезжать каждый месяц из гаража в гараж, когда коробок становится больше.

#include <vector>
#include <iostream>
#include <chrono>

int main() {
    std::vector<int> data;
    data.reserve(1'000'000); // Вот это, блядь, ключевой приём! Говорим сразу, сколько надо.

    for (int i = 0; i < 1'000'000; ++i) {
        data.push_back(i); // И теперь добавление — просто праздник какой-то.
    }
    std::cout << "Size: " << data.size() << ", Capacity: " << data.capacity() << 'n';
    return 0;
}

А другие контейнеры? Ну, они тоже не просто так существуют.

  • std::deque: Бери его, если тебе надо пихать элементы и в начало, и в конец часто. У него тоже O(1) на эти операции. Но данные у него лежат чутка похуже для кэша — блоками.
  • std::list / std::forward_list: Вообще, забей на них для этой задачи. Они нужны, только если ты собрался постоянно вставлять и удалять где попало в середине. У них на каждый элемент свой overhead — указатели, которые жрут память, и данные разбросаны по всей оперативке, как ошкуренные. Локальность — пиздец. Для добавления только в конец — это стрельба из пушки по воробьям, да ещё и пушка кривая.