Bucles que no tienes que escribir
En la página anterior viste que cada contenedor entrega iteradores: cursores ligeros con begin() y end(). Esa abstracción es la razón de ser de los algoritmos estándar. En lugar de escribir un bucle for a mano cada vez que quieres buscar, contar o transformar datos, llamas a una función con nombre de <algorithm> y le pasas un rango.
Un rango son simplemente dos iteradores: dónde empezar y uno más allá de dónde parar. Como todos los algoritmos hablan ese mismo lenguaje de iteradores, el mismo find funciona sobre un vector, un string o un array simple.
El patrón que debes interiorizar: un algoritmo de búsqueda devuelve el iterador end() para decir "no se encontró nada". Compara siempre con end() antes de desreferenciar el resultado: desreferenciar end() es comportamiento indefinido.
Contar y comprobar con predicados
Muchos algoritmos aceptan un predicado: una función (normalmente una lambda) que devuelve bool para cada elemento. count_if cuenta las coincidencias; all_of, any_of y none_of responden preguntas de sí/no sobre todo el rango.
std::count (sin _if) es el primo más sencillo que cuenta un valor exacto en lugar de una condición. Recurre a las versiones con predicado en cuanto tu prueba sea "cualquier cosa que cumpla una regla" en vez de "este valor concreto".
Transformar y reducir un rango
Dos caballos de batalla cubren la mayor parte del procesamiento de datos: std::transform mapea cada elemento a través de una función, y std::accumulate (de <numeric>, no de <algorithm>) pliega un rango hasta un único valor.
transform escribe sus resultados a través de un iterador de salida. Un error común y peligroso es apuntar la salida a un vector vacío: el algoritmo asume que ya hay espacio y escribe más allá del final. O bien dimensiona primero el destino, o usa un back_inserter para que cada resultado se añada con push_back.
accumulate parte de un valor inicial y combina los elementos de izquierda a derecha. El tipo del valor inicial importa: pasa 0 (un int) y la suma se calcula en int, lo que puede desbordarse o truncarse.
Si hubieras escrito accumulate(prices.begin(), prices.end(), 0) con una semilla int, cada suma ocurriría en int y los centavos desaparecerían. El tipo de la semilla determina silenciosamente el tipo del resultado.
El idiom erase-remove
Aquí está el detalle que sorprende a todo el mundo. std::remove no elimina nada del contenedor. Los algoritmos solo tienen iteradores, así que no pueden cambiar el tamaño de un contenedor: ni siquiera saben que existe uno. Lo que remove hace en realidad es mover todos los elementos conservados al principio, dejando la cola en un estado no especificado, y devolver un iterador al nuevo final lógico.
// remove por sí solo deja el tamaño sin cambios - este es el error:
remove(v.begin(), v.end(), 0); // devuelve un iterador que ignoraste
// v todavía tiene su tamaño original; la cola es basura
Para borrar de verdad los elementos, emparejas remove con el erase del contenedor, y por eso se llama idiom erase-remove:
Usa remove_if para un predicado en lugar de un valor exacto. En C++20 puedes saltarte todo el baile con las funciones libres std::erase / std::erase_if, que hacen ambos pasos por ti: erase(v, 0);.
Los iteradores sobreviven a su validez bajo tu responsabilidad
Como los algoritmos devuelven iteradores, esos iteradores están sujetos a las mismas reglas de invalidación que viste en la página anterior. Guardar un iterador y luego modificar el contenedor - un push_back que provoque una reasignación, o un erase - puede dejar el iterador guardado colgando, y usarlo es comportamiento indefinido.
auto it = find(v.begin(), v.end(), 16);
v.push_back(99); // puede reasignar el almacenamiento de v
cout << *it; // BUG: `it` puede apuntar ahora a memoria liberada
El hábito seguro: usa el iterador que devuelve un algoritmo inmediatamente, antes de cualquier operación que pueda redimensionar o reasignar el contenedor. Si necesitas mutar el contenedor según un resultado, captura un índice (it - v.begin()) en su lugar, ya que los índices sobreviven a la reasignación.
Siguiente: Ordenación
Ya has visto cómo buscar, contar, transformar y reducir, pero un algoritmo es lo bastante importante como para merecer su propia página. std::sort reordena un rango en el sitio, y una vez que los datos están ordenados se abre toda una familia de algoritmos más rápidos y exclusivos para datos ordenados (binary_search, lower_bound, equal_range). A continuación profundizaremos en la ordenación: cómo proporcionar un comparador personalizado, la diferencia entre sort y stable_sort, y las reglas que tu función de comparación debe respetar para evitar comportamiento indefinido.
Preguntas frecuentes
¿Qué es la cabecera <algorithm> en C++?
<algorithm> es la cabecera de la biblioteca estándar que contiene funciones genéricas como std::find, std::sort, std::count_if y std::transform. Operan sobre rangos descritos por un par de iteradores (normalmente begin() y end()), por lo que el mismo algoritmo funciona con un vector, un array, un string o cualquier contenedor que exponga iteradores.
¿Cómo compruebo si un valor existe en un vector en C++?
Usa std::find: auto it = find(v.begin(), v.end(), target);. Si it == v.end() el valor no está presente; en caso contrario it apunta a la primera coincidencia. Para comprobar una condición en lugar de un valor exacto, usa std::any_of con un predicado.
¿Por qué std::remove no elimina realmente los elementos de mi contenedor?
Los algoritmos solo ven iteradores, no el contenedor, así que std::remove no puede reducirlo: desplaza los elementos que se conservan al principio y devuelve un iterador al nuevo final lógico. Debes seguirlo con v.erase(...) (el idiom erase-remove) para eliminar físicamente los sobrantes.