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

«В чем разница между std::list и std::vector» — вопрос из категории STL, который задают на 38% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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)
}