Ir al contenido

Contenedores de Secuencia: vector, deque, list, array

Explora los contenedores de secuencia en la STL — vector, deque, list, forward_list y array. Aprende sus compromisos de rendimiento, disposiciones de memoria y reglas de invalidacion de iteradores.

Que Son los Contenedores de Secuencia?

Los contenedores de secuencia almacenan elementos en un orden lineal estricto. La STL proporciona cinco contenedores de secuencia, cada uno optimizado para diferentes patrones de acceso y modificacion:

- std::vector — arreglo dinamico, memoria contigua
- std::deque — cola de doble extremo, insercion rapida en ambos extremos
- std::list — lista doblemente enlazada
- std::forward_list — lista simplemente enlazada
- std::array — arreglo de tamano fijo, asignado en la pila

Elegir el contenedor correcto es una de las decisiones de rendimiento mas importantes que tomaras en C++. Los factores clave son patron de acceso, patron de insercion/eliminacion y localidad de cache.

std::vector — La Opcion por Defecto

std::vector es el contenedor mas utilizado. Almacena elementos en un unico bloque contiguo de memoria, lo que le da un excelente rendimiento de cache y acceso aleatorio O(1). Agregar elementos con push_back es O(1) amortizado porque el vector duplica su capacidad cuando esta lleno.

vector_basics.cpp
#include <iostream>
#include <vector>

int main() {
    std::vector<int> nums;          // empty vector
    nums.reserve(100);              // pre-allocate for 100 elements (no reallocation)

    for (int i = 0; i < 10; ++i) {
        nums.push_back(i * i);      // amortized O(1)
    }

    // Size vs capacity
    std::cout << "Size:     " << nums.size()     << '\n';  // 10
    std::cout << "Capacity: " << nums.capacity() << '\n';  // >= 100

    // Random access — O(1)
    std::cout << "Element 3: " << nums[3] << '\n';         // 9
    std::cout << "Element 3: " << nums.at(3) << '\n';      // 9 (bounds-checked)

    // Erase-remove idiom (pre-C++20)
    nums.erase(std::remove_if(nums.begin(), nums.end(),
                              [](int x) { return x > 50; }),
               nums.end());

    // C++20: std::erase_if (cleaner)
    std::erase_if(nums, [](int x) { return x > 30; });

    // Range-based for loop
    for (int n : nums) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
}

deque, list y forward_list

std::deque proporciona insercion y eliminacion O(1) en ambos extremos, pero utiliza una disposicion de memoria segmentada (arreglo de bloques de tamano fijo) en lugar de una unica asignacion contigua. std::list es una lista doblemente enlazada que ofrece insercion/eliminacion O(1) en cualquier lugar dado un iterador, a costa de una pobre localidad de cache. std::forward_list es una lista simplemente enlazada que usa menos memoria por nodo pero solo soporta recorrido hacia adelante.

deque_list.cpp
#include <deque>
#include <list>
#include <forward_list>
#include <iostream>

int main() {
    // deque — O(1) push_front and push_back
    std::deque<int> dq;
    dq.push_front(1);   // O(1) — vector cannot do this efficiently
    dq.push_back(2);    // O(1)
    dq.push_front(0);   // O(1)
    // dq: {0, 1, 2}

    // list — O(1) insert/erase at any position with iterator
    std::list<std::string> names = {"Alice", "Charlie", "Eve"};
    auto it = std::next(names.begin());  // points to "Charlie"
    names.insert(it, "Bob");             // O(1) insert before "Charlie"
    names.erase(it);                     // O(1) erase "Charlie"
    // names: {"Alice", "Bob", "Eve"}

    // list has splice — O(1) move of entire sublists
    std::list<std::string> more = {"Frank", "Grace"};
    names.splice(names.end(), more);     // moves all of 'more' into 'names'
    // names: {"Alice", "Bob", "Eve", "Frank", "Grace"}
    // more is now empty

    // forward_list — singly linked, no size(), push_front only
    std::forward_list<int> fl = {3, 1, 4, 1, 5};
    fl.push_front(0);
    fl.sort();           // member sort — O(n log n), stable
    fl.unique();         // remove consecutive duplicates
    // fl: {0, 1, 3, 4, 5}

    for (int v : fl) std::cout << v << ' ';
    std::cout << '\n';
}

std::array — Tamano Fijo, Cero Sobrecarga

std::array envuelve un arreglo estilo C en una clase adecuada, proporcionando .size(), .at(), iteradores y compatibilidad con todos los algoritmos de la STL — con cero sobrecarga comparado con un T[N] crudo.

std_array.cpp
#include <array>
#include <algorithm>
#include <iostream>

int main() {
    std::array<int, 5> arr = {5, 2, 8, 1, 9};

    std::sort(arr.begin(), arr.end());       // works with STL algorithms

    std::cout << "Size: " << arr.size() << '\n';   // constexpr — known at compile time
    std::cout << "Front: " << arr.front() << '\n';  // 1
    std::cout << "Back:  " << arr.back()  << '\n';  // 9

    // Bounds-checked access
    try {
        arr.at(10);  // throws std::out_of_range
    } catch (const std::out_of_range& e) {
        std::cout << "Out of range: " << e.what() << '\n';
    }

    // Compile-time size via std::tuple_size
    constexpr std::size_t n = std::tuple_size_v<decltype(arr)>;  // 5
    static_assert(n == 5);
}

Elegir el Contenedor de Secuencia Correcto

Usa esta guia de decision:

Usa std::vector (la opcion por defecto) cuando necesites acceso aleatorio, iteracion amigable con el cache o push/pop al final. La gran mayoria del codigo deberia usar vector.

Usa std::deque cuando necesites insercion/eliminacion frecuente en ambos extremos (por ejemplo, una ventana deslizante, cola BFS).

Usa std::list cuando necesites insercion/eliminacion O(1) en el medio dado un iterador, o cuando necesites splice() para mover sublistas entre listas sin copiar.

Usa std::forward_list cuando la memoria por nodo importa y solo necesitas recorrido hacia adelante.

Usa std::array cuando el tamano se conoce en tiempo de compilacion y nunca cambiara.

Nota de rendimiento: Debido a los efectos de cache de la CPU, std::vector a menudo supera a std::list incluso para inserciones en el medio en hardware moderno, hasta tamanos sorprendentemente grandes (miles de elementos). Siempre mide antes de elegir una lista enlazada.

Error común

Cada contenedor tiene reglas diferentes para cuando los iteradores, punteros y referencias se vuelven invalidos:

vector — La insercion que provoca una reasignacion invalida todos los iteradores. La insercion sin reasignacion invalida los iteradores en o despues del punto de insercion. La eliminacion invalida los iteradores en o despues del punto de eliminacion.

deque — La insercion al frente o al final invalida todos los iteradores pero no las referencias/punteros a los elementos existentes. La insercion en el medio invalida todo. La eliminacion al frente/final solo invalida el iterador del elemento eliminado.

list / forward_list — La insercion y eliminacion nunca invalidan iteradores, referencias o punteros a otros elementos. Solo se invalida el iterador del elemento eliminado.

array — Los iteradores nunca se invalidan (el arreglo no puede cambiar de tamano).

Un error comun es modificar un vector dentro de un bucle range-based for. Si un push_back provoca una reasignacion, los iteradores ocultos del bucle quedan colgantes.

Buena práctica

Si sabes (o puedes estimar) cuantos elementos contendra un vector, llama a reserve() antes de llenarlo. Esto preasigna memoria y evita reasignaciones costosas:

- reserve(n) cambia la capacidad pero no el tamano — no se agregan elementos.
- resize(n) cambia el tamano — realmente construye elementos por defecto.
- shrink_to_fit() es una solicitud no vinculante para reducir la capacidad al tamano actual.

Para bucles criticos que construyen vectores, una sola llamada a reserve() puede reducir drasticamente la sobrecarga de asignacion. Mide con conjuntos de datos grandes para ver la diferencia.

Puntos clave
  • std::vector es la opcion por defecto — la memoria contigua significa iteracion amigable con el cache y acceso aleatorio O(1)
  • Usa reserve() para preasignar memoria del vector cuando se conoce el tamano aproximado
  • std::deque proporciona push/pop O(1) en ambos extremos, pero no es contiguo
  • std::list ofrece insercion/eliminacion O(1) con un iterador y nunca invalida otros iteradores
  • std::array es un envoltorio de cero sobrecarga alrededor de arreglos C con compatibilidad total con la STL
  • Las reglas de invalidacion de iteradores difieren por contenedor — conocelas para evitar errores de iteradores colgantes

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Que contenedor de secuencia proporciona `push_back` amortizado O(1) y disposicion de memoria contigua?

2. Que sucede con todos los iteradores de un vector cuando un `push_back` provoca una reasignacion?

3. Cual es la diferencia entre `reserve(n)` y `resize(n)` en un vector?