В чем разница между std::list и std::vector

Ответ

std::vector и std::list — это принципиально разные контейнеры STL с различными характеристиками производительности, обусловленными их внутренним устройством.

Характеристика std::vector std::list
Внутренняя структура Динамический массив (непрерывная память). Двусвязный список (узлы разбросаны в памяти).
Доступ по индексу O(1) — мгновенный (vec[i]). O(n) — требуется обход от начала/конца.
Вставка/удаление в конце Амортизированное O(1) (возможна реаллокация). O(1).
Вставка/удаление в середине O(n) — требует сдвига всех последующих элементов. O(1) — после нахождения позиции (поиск O(n)).
Локальность данных Отличная. Элементы лежат рядом, что дружественно к кэшу процессора. Плохая. Узлы разбросаны, много промахов кэша.
Накладные расходы Минимальны (только память под элементы). Высокие (на каждый элемент — два указателя prev, next).
Итераторы RandomAccess. Инвалидируются при реаллокации. Bidirectional. Устойчивы к вставке/удалению (кроме удалённого элемента).

Когда использовать std::vector (по умолчанию):

  • В 95% случаев. Это контейнер по умолчанию в C++.
  • Когда нужен частый произвольный доступ.
  • Когда важна производительность из-за локальности данных (последовательный обход).
  • Когда данные добавляются преимущественно в конец.

Когда рассмотреть std::list (или std::forward_list):

  • Когда нужны частые вставки/удаления в произвольных местах последовательности, и позиция уже известна (есть итератор).
  • Когда критически важна стабильность итераторов/ссылок на элементы (они не инвалидируются при модификациях списка).
  • Пример: LRU-кэш, где элементы часто перемещаются из середины в начало/конец.

Практический пример выбора:

// Сценарий 1: Частое чтение и добавление в конец -> Вектор.
std::vector<int> sensorReadings;
sensorReadings.reserve(1000); // Избегаем лишних реаллокаций
for (int i = 0; i < 1000; ++i) {
    sensorReadings.push_back(readSensor()); // Быстро
}
// Быстрая обработка:
int sum = std::accumulate(sensorReadings.begin(), sensorReadings.end(), 0);

// Сценарий 2: Активная очередь задач с приоритетным удалением из середины -> Список.
std::list<Task> taskQueue;
// ... добавление задач
auto it = std::find_if(taskQueue.begin(), taskQueue.end(), isHighPriority);
if (it != taskQueue.end()) {
    taskQueue.erase(it); // Быстрое удаление из середины O(1)
}

Ответ 18+ 🔞

Э, слушай, давай разберёмся с этими двумя зверями — std::vector и std::list. Это ж, ёпта, как два разных автомобиля: один — гоночный болид на трассе, другой — внедорожник, который в грязи ковыряется. И если выбрать не тот, то производительность твоя накроется медным тазом, вот тебе честно.

Кто есть кто, если по-простому:

Что смотрим std::vector (наш болид) std::list (наш внедорожник)
Внутри Обычный массив, все элементы друг за дружкой стоят, как солдаты. Двусвязный список, где каждый элемент — это отдельная избушка на курьих ножках, разбросанная по всему лесу памяти.
Доступ по индексу O(1) — взял и сразу прыгнул на нужное место. vec[999] — и ты уже там. O(n) — это пиздец, чувак. Чтобы дойти до 999-го элемента, надо пройтись по всем 998 предыдущим, как будто по цепочке передач.
Вставка/удаление в конце Амортизированное O(1) — обычно быстро, но иногда он такой: «ой, места мало», и делает реаллокацию — переезжает на новый участок памяти, копируя всё. Это может быть больно. O(1) — всегда быстро. Просто прицепил новую избушку в конец цепи.
Вставка/удаление в середине O(n) — это ядрёна вошь! Представь: ты встроил новый диван в середину шеренги солдат. Всем после него придётся сдвинуться на шаг. Ужас. O(1) — после того, как ты нашёл это место (а поиск — O(n), помнишь?). Нашёл — и просто перекинул пару указателей. Красота.
Дружба с кэшем процессора Овердохуищная! Элементы рядом, процессор их пачками грузит — летает. Пиздопроебибна. Элементы разбросаны где попало, кэш промахивается постоянно. Процессор плачет.
Дополнительные траты Почти нет. Только на сами данные. На каждый элемент — два указателя (на предыдущего и следующего). Для мелких объектов это может быть жирно.
Итераторы Мощные, RandomAccess. Но если вектор переехал (реаллокация), то все старые итераторы и ссылки становятся мусором. Скромные, Bidirectional. Зато живучие — вставка и удаление в других местах их не убивают.

Так когда же что брать?

Берём std::vector (в 95% случаев): Это твой рабочий конь, контейнер по умолчанию. Если не знаешь, что выбрать — бери вектор.

  • Когда тебе нужно часто и быстро прыгать по элементам по индексу.
  • Когда ты в основном добавляешь в конец (например, логи, показания датчиков).
  • Когда важна скорость последовательного обхода — из-за локальности данных он просто летает.

Задумываемся о std::list (или его облегчённой версии std::forward_list): Это специфический инструмент. Не лезь с ним везде, а то сам от себя охуеешь от тормозов.

  • Когда у тебя реально частые вставки и удаления прямо из середины списка, и у тебя уже есть итератор на это место (поиск всё равно O(n), помни!).
  • Когда критически важно, чтобы итераторы и ссылки на элементы не сломались после вставок/удалений в других местах. Например, ты хранишь где-то указатель на элемент, и он должен оставаться валидным.
  • Классический пример — LRU-кэш. Туда часто тащат элемент из середины и кладут в начало. В списке это делается за O(1) перекидыванием указателей.

Ну и примеры, чтобы совсем понятно стало:

// Сценарий 1: Логируем кучу событий, потом их читаем. Классика для вектора.
std::vector<std::string> eventLog;
eventLog.reserve(10000); // Заранее бронируем место, чтобы не было реаллокаций — умно!
for (int i = 0; i < 10000; ++i) {
    eventLog.push_back(generateEvent()); // Добавляем в конец — быстро.
}
// А теперь хотим найти все ошибки:
for (const auto& event : eventLog) { // Обход — овердохуища быстро!
    if (event.contains("ERROR")) { /* ... */ }
}

// Сценарий 2: Очередь игроков, где админ может выкинуть кого-то из середины за флуд.
std::list<Player> playerQueue;
// ... игроки подключаются, queue.push_back(...)
// Админ ищет тролля:
auto trollIt = std::find_if(playerQueue.begin(), playerQueue.end(), isTrolling);
if (trollIt != playerQueue.end()) {
    playerQueue.erase(trollIt); // БАМ! Удалили из середины за O(1). В векторе это была бы катастрофа.
}

Итог: vector — это быстрый и предсказуемый парень для большинства задач. list — это хитрая жопа, которая может выручить в очень специфичных сценариях, но если применять её не к месту, получишь тормоза и удивление пиздец. Думай головой, прежде чем выбрать.