Ordenar es solo otro algoritmo
En la página anterior viste que la biblioteca estándar incluye algoritmos listos para usar que funcionan sobre cualquier rango a través de iteradores. La ordenación es la que usarás con más frecuencia, y tiene su propia página porque presenta algunas aristas afiladas: órdenes personalizados, estabilidad y una regla que, si la incumples, te da comportamiento indefinido en lugar de una respuesta incorrecta.
El caballo de batalla es std::sort de <algorithm>. Le das el principio y el final de un rango, y reordena los elementos en su sitio en orden ascendente:
No se hace ninguna copia: el propio vector se reordena. Por dentro, std::sort suele ser un introsort (quicksort que recurre a heapsort), lo que te da O(n log n) de media. Eso es casi siempre más rápido y mucho menos propenso a errores que escribir tu propia ordenación.
También funciona con arrays de C puros: solo tienes que describir el rango con punteros:
Orden personalizado con un comparador
Por defecto, std::sort ordena los elementos con operator<. Para ordenar de otra forma, pasa un tercer argumento: un comparador que toma dos elementos y devuelve true si el primero debe ir antes que el segundo.
Una lambda encaja de forma natural. El orden descendente es simplemente a > b:
Para el caso habitual de orden descendente puro sobre tipos integrados, la biblioteca incluso trae un comparador listo para usar, greater<T>() de <functional>:
#include <functional>
sort(nums.begin(), nums.end(), greater<int>()); // igual que a > b
El comparador también es la forma de ordenar por algo distinto del propio valor; por ejemplo, ordenar cadenas por longitud en lugar de alfabéticamente:
Recibe los parámetros del comparador por const& para cualquier cosa mayor que un par de bytes (como string): copiar cada elemento en cada comparación es puro desperdicio.
Ordenar structs por un campo
En programas reales normalmente ordenas colecciones de structs por uno de sus campos. El comparador simplemente accede al campo que te interesa. Aquí ordenamos personas por edad, del más joven primero:
Fíjate en que Linus y Dennis tienen ambos 25 años. Aquí salieron en su orden relativo original, pero std::sort no lo garantiza. Si el orden relativo de los elementos iguales importa, usa std::stable_sort, que lo conserva (a un pequeño coste de rendimiento):
Para deshacer empates de forma deliberada (por ejemplo, ordenar por edad y luego alfabéticamente por nombre), compara la clave secundaria solo cuando las claves primarias sean iguales. std::tie lo deja limpio:
La trampa del orden débil estricto
Este es el error más peligroso al ordenar en C++, porque no te da una respuesta incorrecta: te da comportamiento indefinido, que a menudo significa una caída o una lectura fuera de los límites.
std::sort requiere que tu comparador defina un orden débil estricto. La regla práctica: comp(x, x) debe ser false para cualquier elemento x. En otras palabras, un elemento nunca está "antes" que sí mismo. Eso es exactamente lo que te dan < y >, y exactamente lo que rompen <= y >=:
// BUG: devuelve true cuando a == b, violando el orden débil estricto.
sort(v.begin(), v.end(), [](int a, int b) {
return a <= b; // comportamiento indefinido: puede caerse con algunas entradas
});
Con <=, el comparador afirma que 5 va antes que otro 5, lo cual es contradictorio. std::sort puede entonces recorrer un puntero más allá del final del rango. Las entradas pequeñas a veces parecen funcionar, lo que hace que este bug sea aterrador: puede pasar tus pruebas y caerse en producción. La solución es simplemente <:
Un segundo problema clásico: ordenar invalida cualquier cosa que apunte dentro del rango. Los iteradores, punteros e índices que guardaste antes de ordenar ya no se refieren al mismo elemento lógico después, porque los elementos se movieron. Vuelve a obtener cualquier posición que necesites después de ordenar, nunca antes.
Ordenar parte de un rango
A veces no necesitas ordenarlo todo: solo quieres los primeros elementos. Ordenar el vector entero para leer los tres primeros es un desperdicio. std::partial_sort organiza solo los elementos que pidas y deja el resto en un orden no especificado, lo cual es más barato:
Y si solo necesitas el único elemento que estaría en una posición dada (como la mediana), std::nth_element hace aún menos trabajo: coloca el elemento correcto en ese índice, con todo lo menor antes y todo lo mayor después, todo en O(n) de media.
Recurre a estos cuando "totalmente ordenado" es más de lo que el problema realmente necesita: ahorran tiempo real con datos grandes.
Siguiente: Plantillas
¿Te has fijado en que el mismo std::sort manejó int, string y tu propio struct Person, y que greater<int>() podría ser igual de fácil greater<string>()? Esa generalidad no es magia: son las plantillas, el mecanismo que permite que una sola pieza de código funcione para cualquier tipo que el llamante le pase. En la siguiente página veremos cómo escribir tus propias funciones y clases con plantillas, para que tu código pueda ser tan agnóstico respecto al tipo como los algoritmos que has estado usando.
Preguntas frecuentes
¿Cómo se ordena un vector en C++?
Incluye <algorithm> y llama a sort(v.begin(), v.end()). Esto ordena los elementos en su sitio en orden ascendente usando operator<. Para ordenar un array, pasa arr y arr + n (o begin(arr) / end(arr)).
¿Cómo se ordena en orden descendente en C++?
Pasa un comparador que devuelva a > b: sort(v.begin(), v.end(), [](int a, int b){ return a > b; });. También puedes usar el sort(v.begin(), v.end(), greater<int>()); que viene en <functional>.
¿Por qué mi comparador hace que std::sort se caiga en C++?
Tu comparador debe ser un orden débil estricto: tiene que devolver false cuando ambos argumentos son iguales. Usar <= o >= (que devuelven true para elementos iguales) rompe esa regla y es comportamiento indefinido: std::sort puede leer fuera de los límites y caerse. Compara siempre con < o >.