std::map, set, pilas y colas
Elegir el contenedor correcto según el orden, la búsqueda y la complejidad
Un map guarda pares clave-valor ordenados; un unordered_map los guarda en una tabla hash. Un set elimina duplicados y las pilas y colas imponen un orden de salida. Aquí aprenderás qué contenedor elegir y cuánto cuesta cada operación.
- Distinguir std::map de std::unordered_map por orden y complejidad
- Usar set para eliminar duplicados y consultar pertenencia
- Aplicar stack, queue y priority_queue al problema correcto
- Evitar que operator[] inserte claves por accidente
- Elegir contenedor a partir de la complejidad y del orden requerido
Antes necesitas: Arrays, std::vector y std::string · RAII y punteros inteligentes
En esta lección
- ¿Qué es un contenedor asociativo?
- ¿Para qué sirven?
- ¿Cómo funciona por dentro?
- Sintaxis
- Ejemplo básico: contar palabras
- Ejemplo intermedio: intersección de conjuntos
- Caso práctico: los tres mejores puntajes
- Visualización: insertar, actualizar y consultar en un map
- Errores comunes
- Buenas prácticas
- Ejercicios
- Soluciones explicadas
- Resumen
- Evaluación
Ya sabes guardar una secuencia en un std::vector. Pero muchos problemas no piden una secuencia: piden responder «¿cuántas veces aparece esta palabra?», «¿qué elementos son únicos?» o «¿cuál es el siguiente trabajo más urgente?». Para eso están los contenedores asociativos y los adaptadores.
La decisión entre ellos no es estética. Cada contenedor ofrece un orden distinto de recorrido y un coste distinto por operación. Elegir mal convierte un bucle de un segundo en uno de un minuto, o hace que tu programa imprima resultados en un orden impredecible.
¿Qué es un contenedor asociativo?
Un contenedor asociativo guarda elementos identificados por una clave y permite encontrarlos sin recorrer todo. std::map<Clave, Valor> almacena pares y ordena por clave; std::set<Clave> almacena solo claves únicas, también ordenadas. std::unordered_map y std::unordered_set hacen lo mismo con una tabla hash, sin orden de recorrido.
Los adaptadores son otra cosa: no guardan datos con una clave, sino que limitan por dónde entran y salen. std::stack es LIFO (el último que entra es el primero que sale), std::queue es FIFO (el primero que entra es el primero que sale) y std::priority_queue siempre entrega el elemento de mayor prioridad. Stack y queue no son contenedores completos: envuelven un std::deque por debajo y exponen solo unas operaciones.
| Contenedor | Estructura interna | Inserción | Búsqueda | Orden de recorrido | Cuándo usarlo |
|---|---|---|---|---|---|
std::map | Árbol balanceado | O(log n) | O(log n) | Por clave, ascendente | Necesitas claves ordenadas |
std::unordered_map | Tabla hash | O(1) medio | O(1) medio | Sin orden definido | Solo necesitas buscar rápido |
std::set | Árbol balanceado | O(log n) | O(log n) | Ascendente | Elementos únicos y ordenados |
std::stack | Adaptador sobre deque | O(1) push | No busca | LIFO | Deshacer, DFS, paréntesis |
std::queue | Adaptador sobre deque | O(1) push | No busca | FIFO | Turnos, BFS, productor-consumidor |
std::priority_queue | Montículo binario | O(log n) push | O(1) top | Mayor prioridad primero | Colas de urgencia y top-k |
¿Para qué sirven?
- Contar frecuencias: un
map<std::string, int>asocia cada palabra con su número de apariciones. - Eliminar duplicados: insertar en un
setdescarta automáticamente lo repetido y mantiene el orden. - Buscar por identificador: un
unordered_map<int, Registro>pasa de una búsqueda lineal a una consulta directa. - Procesar en orden inverso: una pila resuelve la evaluación de expresiones y el retroceso en un laberinto.
- Atender por turnos: una cola mantiene el orden de llegada en una simulación o en un recorrido BFS.
- Priorizar: una
priority_queueentrega siempre el mayor puntaje, el trabajo más urgente o el camino más corto en Dijkstra.
En Machine Learning aparecen constantemente: un vocabulario es un map<string, int> que asigna un índice a cada token; un conjunto de etiquetas únicas es un set; la búsqueda en anchura de un grafo usa una queue; y los mejores candidatos de una búsqueda se guardan en una priority_queue. Saber qué operación domina el algoritmo te dice qué contenedor necesitas.
¿Cómo funciona por dentro?
std::map y std::set se implementan típicamente como árboles rojo-negro, un árbol binario de búsqueda que se rebalancea al insertar y borrar. Por eso todas las operaciones de búsqueda, inserción y borrado cuestan O(log n): la altura del árbol se mantiene logarítmica aunque los datos lleguen ordenados. El recorrido begin() a end() devuelve las claves de menor a mayor.
std::unordered_map y std::unordered_set usan una tabla hash: convierten la clave en un número con std::hash, lo reducen a un índice de un arreglo de buckets y guardan ahí el elemento. Si la función hash reparte bien, cada bucket tiene pocos elementos y la búsqueda es O(1) en promedio. Si muchas claves caen en el mismo bucket, la búsqueda degenera a O(n). Por eso el estándar no garantiza O(1) en el peor caso.
Un std::stack no es más que un deque con la interfaz limitada a push, pop y top. Un std::queue expone push, pop, front y back. Una priority_queue mantiene un montículo: el elemento mayor está siempre en la raíz, top() lo devuelve en O(1) y push/pop reorganizan en O(log n).
Sintaxis
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
int main() {
std::map<std::string, int> edades;
edades["ana"] = 30;
edades["luis"] = 25;
std::cout << edades.size() << '\n'; // 2
std::set<int> unicos{3, 1, 3, 2};
std::cout << unicos.size() << '\n'; // 3
for (int x : unicos) {
std::cout << x << ' '; // 1 2 3
}
std::cout << '\n';
std::stack<int> pila;
pila.push(1);
pila.push(2);
std::cout << pila.top() << '\n'; // 2
pila.pop();
std::queue<std::string> cola;
cola.push("primero");
cola.push("segundo");
std::cout << cola.front() << '\n'; // primero
cola.pop();
return 0;
}map[clave] = valorinserta o actualiza.map.at(clave)solo lee y lanza si no existe.set.insert(x)devuelve unpair<iterator, bool>: el segundo valor indica si realmente se insertó o ya estaba.stack::top()yqueue::front()sobre un contenedor vacío son comportamiento indefinido; compruebaempty()antes.priority_queue<int>es un montículo de máximos. Para mínimos se declarastd::priority_queue<int, std::vector<int>, std::greater<int>>.
Ejemplo básico: contar palabras
#include <iostream>
#include <map>
#include <string>
#include <vector>
int main() {
std::vector<std::string> palabras{"sol", "mar", "sol", "viento", "mar", "sol"};
std::map<std::string, int> frecuencia;
for (const std::string& p : palabras) {
++frecuencia[p]; // si no existe, la crea con 0 y suma 1
}
for (const auto& [palabra, veces] : frecuencia) {
std::cout << palabra << ": " << veces << '\n';
}
return 0;
}La salida aparece ordenada alfabéticamente: mar: 2, sol: 3, viento: 1. Ese orden no lo escribiste tú: es una propiedad del std::map. La primera vez que se evalúa frecuencia["sol"], el mapa inserta la clave con valor 0 y luego el ++ lo deja en 1. Las siguientes veces encuentra la clave y solo incrementa. El for con [palabra, veces] es un structured binding que descompone el par en dos variables.
Ejemplo intermedio: intersección de conjuntos
#include <iostream>
#include <set>
#include <string>
int main() {
std::set<std::string> ana{"mate", "fisica", "programacion"};
std::set<std::string> luis{"programacion", "quimica", "mate"};
std::set<std::string> comunes;
for (const std::string& curso : ana) {
if (luis.count(curso) > 0) {
comunes.insert(curso);
}
}
std::cout << comunes.size() << '\n'; // 2
for (const std::string& c : comunes) {
std::cout << c << ' '; // mate programacion
}
std::cout << '\n';
return 0;
}count devuelve 1 si la clave existe y 0 si no; en un set nunca puede devolver más de 1 porque los duplicados no se almacenan. Recorrer el conjunto menor sería más eficiente, pero con tres elementos la diferencia es irrelevante: lo importante es que la consulta es logarítmica en lugar de recorrer el otro conjunto. El resultado comunes también está ordenado y no contiene repetidos.
Caso práctico: los tres mejores puntajes
Una priority_queue mantiene el elemento mayor en la cima sin ordenar todo el vector. Si solo necesitas los k mejores de un conjunto grande, es más barato que ordenar: cada push cuesta O(log n) y cada pop también, mientras que un sort completo cuesta O(n log n) aunque luego descartes casi todo.
#include <iostream>
#include <queue>
#include <vector>
int main() {
std::vector<int> puntajes{55, 90, 72, 90, 88, 61};
std::priority_queue<int> max_heap(puntajes.begin(), puntajes.end());
for (int i = 0; i < 3 && !max_heap.empty(); ++i) {
std::cout << max_heap.top() << ' ';
max_heap.pop();
}
std::cout << '\n'; // 90 90 88
return 0;
}El constructor que recibe dos iteradores construye el montículo en tiempo lineal. Cada top() devuelve la raíz, el mayor de los restantes; pop() la elimina y reordena. Los dos 90 aparecen porque son dos elementos distintos: la cola de prioridad no elimina duplicados, a diferencia de un set.
Visualización: insertar, actualizar y consultar en un map
Sigue los pasos y observa dos cosas: el mapa mantiene las claves ordenadas y asignar dos veces la misma clave no crea un duplicado, sino que actualiza el valor. El tamaño solo cambia en las dos primeras inserciones.
std::map<std::string, int> edades;edades["ana"] = 30;edades["luis"] = 25;edades["ana"] = 31;std::cout << edades.size() << '\n';std::cout << edades.at("ana") << '\n';—
Avanza paso a paso: observa la línea resaltada, las variables y la salida.
Errores comunes
- Usar
m[clave]para comprobar: si la clave no existe, se inserta con valor 0 y el mapa crece. Para preguntar sin modificar usam.contains(clave)om.find(clave) != m.end(). - Asumir que
unordered_mapconserva el orden: no lo hace. Si imprimes sus elementos, el orden puede cambiar entre ejecuciones o entre bibliotecas. Usamapcuando el orden importe. - Olvidar comprobar
empty():top()en una pila vacía ofront()en una cola vacía es comportamiento indefinido, no una excepción. - Confundir el orden de una
priority_queue: por defecto es un montículo de máximos. Para el mínimo hay que pasarstd::greater<int>como comparador. - Modificar un elemento de un
set: los elementos sonconstporque cambiar la clave rompería el árbol. Hay que borrar e insertar de nuevo. - Usar una clave sin
operator<o sin hash:std::mapnecesita comparación;std::unordered_map,std::hashyoperator==. Si no existen, el error de compilación ocupa varias pantallas. - Elegir
mappara pocos elementos: con diez elementos, unvectorordenado suele ser más rápido por localidad de caché. La notación O grande no cuenta toda la historia.
Buenas prácticas
- Elige
unordered_mapsi no necesitas orden y la búsqueda es el cuello de botella; eligemapsi necesitas recorrer ordenado o rangos. - Reserva capacidad en
unordered_mapconreserve(n)si conoces el número de claves: evita rehacer la tabla varias veces. - Usa
contains(C++20) para preguntar yatpara leer cuando la clave debe existir. - Recorre con
for (const auto& [clave, valor] : m); el&evita copiar el par yconstdocumenta que no lo modificas. - Comprueba
empty()antes detop,frontoback. - Usa
std::greater<T>para una cola de mínimos y un comparador propio para prioridades complejas. - Si el número de elementos es pequeño, mide: un vector ordenado puede superar a un mapa en velocidad y en memoria.
frecuencia = {}
for p in ["sol", "mar", "sol"]:
frecuencia[p] = frecuencia.get(p, 0) + 1
print(frecuencia) # orden de insercion
print(set(frecuencia))std::map<std::string, int> frecuencia;
for (const std::string& p : {"sol", "mar", "sol"}) {
++frecuencia[p];
}
// recorrido ordenado por clave
for (const auto& [palabra, veces] : frecuencia) {
std::cout << palabra << ": " << veces << '\n';
}El `dict` de Python conserva el orden de inserción; `std::map` ordena por clave y `std::unordered_map` no garantiza ninguno. En Python `d[k]` para leer lanza `KeyError`, mientras que en C++ `m[k]` inserta: esa diferencia es la fuente de muchos conteos inflados.
Ejercicios
Frecuencia de letras
"banana", cuenta cuántas veces aparece cada letra usando un std::map<char, int> y muestra los pares ordenados por letra. El resultado debe ser a: 3, b: 1, n: 2, cada uno en su línea.Pista
Recorre la cadena con for (char c : texto) e incrementa frecuencia[c]. El mapa se encarga de crear la clave la primera vez.
Cola de atención con prioridad
std::priority_queue<int> con las urgencias {3, 1, 4, 1, 5, 9, 2, 6}. Extrae todos los elementos e imprímelos separados por espacios. Después explica qué cambiarías para atender primero las urgencias menores.Pista
top() devuelve el mayor y pop() lo elimina. Para invertir el orden, el tercer parámetro de la plantilla es un comparador.
std::stack<int> pila; pila.(1); pila.(2); std::cout << pila.() << '\n'; // 2 pila.();
Apilar es `push`, mirar la cima es `top` y desapilar es `pop`.
Soluciones explicadas
En el primer ejercicio, el mapa hace dos trabajos a la vez: asocia cada letra con su contador y garantiza el orden alfabético al recorrerlo. Si hubieras usado un unordered_map, el conteo sería correcto pero las líneas podrían salir en cualquier orden. En el segundo, la cola de prioridad no conserva el orden de llegada: conserva el orden de urgencia. Los dos 1 aparecen dos veces porque son dos elementos distintos, y por eso una priority_queue no sirve para deduplicar. Elegir entre set, map y priority_queue depende de si el problema pide unicidad, asociación o prioridad.
Resumen
std::mapystd::setusan un árbol balanceado: O(log n) y recorrido ordenado por clave.std::unordered_mapystd::unordered_setusan hash: O(1) promedio, sin orden y con peor caso O(n).m[clave]inserta si la clave falta;find,containsyatconsultan sin crear.std::setelimina duplicados y no permite modificar sus elementos en el lugar.stackes LIFO,queuees FIFO ypriority_queueentrega primero el mayor por defecto.top,frontybackrequieren comprobarempty()antes.- La complejidad no lo decide todo: para pocos elementos, un vector ordenado puede ser mejor.
- Elige el contenedor por las operaciones que dominan el algoritmo, no por costumbre.
Evaluación
¿Qué contenedor elegirías si necesitas recorrer las claves en orden alfabético y buscar por clave?
std::map<std::string, int> m; está vacío. ¿Qué hace m["x"];?
¿Cuál es la diferencia esencial entre std::stack y std::queue?
¿Ya lo entendiste?
Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.