Los algoritmos de la STL
Date de publication : 06/11/2008 , Date de mise à jour : 05/07/2010
Par
Rodrigo Pons (home)
Este artículo es una descripción de los conceptos necesarios para una buena utilización de los algoritmos de STL.
Proporciona informaciones sobre cada uno de estos algoritmos (complejidad, precisiones sobre la utilización, etc),
y un ejemplo de utilización para cada uno de ellos.
Introducción
I. Generalidades
I-A. Por qué utilizar los algoritmos de STL
I-B. Functor, predicados...
I-B-1. Functor
I-B-2. Predicado
I-B-3. Función pura
I-B-4. Clase predicado
I-C. ptr_fun(), mem_fun() et mem_fun_ref()
I-D. bind1st() y bind2nd()
I-E. A propósito de los iteradores de inserción
I-F. A propósito de la eficiencia.
I-E-1. Descripción de las pruebas
I-E-2. Resultados
I-E-3. Eficiencia y functor
II. Lista de los algoritmos
II-A. Algoritmos no modificantes
II-A-1. for_each()
II-A-2. find() y find_if()
II-A-3. find_end()
II-A-4. find_first_of()
II-A-5. adjacent_find()
II-A-6. count() y count_if()
II-A-7. mismatch()
II-A-8. equal()
II-A-9. search() y search_n()
II-B. Algoritmos modificantes
II-B-1. copy() y copy_backward()
II-B-2. swap() y swap_ranges()
II-B-3. transform()
II-B-4. Algoritmos de sustitución
II-B-4-1. replace()
II-B-4-2. replace_if()
II-B-4-3. replace_copy()
II-B-4-4. replace_copy_if()
II-B-5. fill() y fill_n()
II-B-6. generate() y generate_n()
II-B-7. algoritmos de supresión
II-B-7-1. remove()
II-B-7-2. remove_if()
II-B-7-3. remove_copy()
II-B-7-4. remove_copy_if()
II-B-8. unique() y unique_copy()
II-B-9. reverse() y reverse_copy()
II-B-10. rotate() y rotate_copy()
II-B-11. random_shuffle()
II-B-12. partition() y stable_partition()
II-C. Algoritmos de clasificación y operaciónes vinculadas
II-C-1. algoritmos de clasificación
II-C-1-1. sort()
II-C-1-2. stable_sort()
II-C-1-3. partial_sort()
II-C-1-4. partial_sort_copy()
II-C-2. nth_element()
II-C-3. Búsqueda binaria
II-C-3-1. lower_bound()
II-C-3-2. upper_bound()
II-C-3-3. equal_range()
II-C-3-4. binary_search()
II-C-4. merge() y inplace_merge()
II-C-5. Operaciones de "set" sobre intervalos clasificados
II-C-5-1. includes()
II-C-5-2. set_union()
II-C-5-3. set_intersection()
II-C-5-4. set_difference()
II-C-5-5. set_symetric_difference()
II-C-6. Montones (heap)
II-C-6-1. make_heap()
II-C-6-2. push_heap()
II-C-6-3. pop_heap()
II-C-6-4. sort_heap()
II-C-7. Mínimo y máximo
II-C-7-1. min()
II-C-7-2. max()
II-C-7-3. min_element()
II-C-7-4. max_element()
II-C-8. lexicographical_compare()
II-C-9. algoritmos de permutación
II-C-9-1. next_permutation()
II-C-9-2. prev_permutation()
II-D. Algorithmos numéricos
II-D-1. accumulate()
II-D-2. inner_product()
II-D-3. adjacent_difference()
II-D-4. partial_sum()
Un ejemplo de utilización intensiva de los algoritmos: AlbumManager
Annexos
V-A. Fonctionoides
V-B. Referencias
volver a la página inicial
Introducción
La librería "algorithm"
es una parte de la STL. Es definida en 2 cabeceras : <algorithm> y <numeric>.
Los algoritmos de la STL forman una herramienta extremadamente poderosa. En ciertas situaciones de la vida
real de un programador C++, esta parte de la STL puede revelarse realmente indispensable. Más de una vez, he debido
desarrollar pequeñas programas, simples y eficaces, y con obligaciones de cualidad y de tiempo que hicieron de este
desarrollo un verdadero desafío. Y si siempre lo he logrado, ha sido a menudo gracias a estos algoritmos de los que
voy hablarle aquí.
Este documento es una vision general sobre el conjunto de los algoritmos. Censé más de 50 funciones, y la mayoría
de ellas tienen varias versiones sobrecargadas. Entonces, no puedo razonablemente hablar en detalle de cada una de
ellas, tanto más que se reencuentra una filosofía común. Además, voy a hablar aquí sólo de los algoritmos que forman
parte del estándar, y que por lo tanto, estarán disponibles en todos los buenos compiladores.
Para ilustrar ciertos algoritmos, escogí hacer un pequeño programa que usted encontrará en archivo adjunto al final
de este artículo. Algunos no estan en este pequeño programa, pero estan ilustrados con unas líneas de código,
en el capítulo dedicado a ellos.
I. Generalidades
I-A. Por qué utilizar los algoritmos de STL
Aparte de unos casos pocos frecuentes, es siempre preferible utilizar los algoritmos proporcionados por STL que algoritmos
"hechos a mano".
Si eligi comenzar mi artículo con esta frase, no es anodino, porque puede ser sujeta a polémica. Sin
embargo, muchos desarrolladores a menudo olvidan que un buen programa debe reunir varias cualidades, y que no
basta que sea solamente más rápido, o más transportable, o más mantenible, o más modular, etc. La mayoría de
las veces, debe ser un buen compromiso de todas estas cualidades. Y los algoritmos (y la STL más generalmente)
proponen el mejor compromiso. ¿Por qué?
- La abstracción:
Estos algoritmos ofrecen un nivel de abstracción que permite un desarrollo muy limpio. Estos algoritmos permiten
aplicar una operación sobre un conjunto de elementos sin preocuparse en detalle de la naturaleza de estos elementos,
y sin tener que hacer un código laborioso y especializado para hacerlo.
- La ganancia de tiempo, en término de "tiempo de desarrollo":
Numerosos algoritmos ya son hechos y permiten resolver los problemas más frecuentes unas líneas de código.
- La robustez del código:
Estos algoritmos son utilizados por millares de programas y respetan estándares que aseguran la robustez.
- La rapidez de ejecución:
La utilización de los algoritmos de la STL suprime ciertas informaciones redundantes (prueba sobre el iterador
de fin de bucle por ejemplo). Además, los desarrolladores que hacen estos algoritmos conocen las estructuras
internas de los contenedores en los cuales trabajan y pueden así optimizar mucho mejor que prodriamos hacerlo.
Finalmente, los algoritmos de la STL utilizan los métodos más recientes del "arte", pues los más eficaces.
- La mantenabilidad del código:
La STL proporciona un estándar conocido y reconocido por millares de desarrolladores. Así, cuando un desarrollador
se encuentra en frente de un código que utiliza estos algoritmos, está en terreno conocido y pasa menos
tiempo para entender este código.
I-B. Functor, predicados...
Todos estos tipos de funciones y de functor (o funtor, o objeto-función, o funciones-objetos) están muy presentes en la STL, y
es indispensable conocer un mínimo de lo que es para utilizar correctamente los algoritmos. Entonces, voy a
comenzar por introducir un poco vocabulario. Es bastante desagradable lo convengo, pero estoy convencido que
usted me agradecerá más tarde por eso.
I-B-1. Functor
Ni el C ni el C++ permiten pasar funciones en parámetros. Es la razón por la cual hay que utilizar a
punteros de función. Un functor hace la misma cosa que un puntero de función, pero permite hacer mas cosas.
Un functor es, sencillamente, una clase que sobrecarga al operador de llamada de función
(operator ()). El functor presenta por lo menos 3 ventajas importantes comparado al
puntero de función:
- Eficacia (rapidez de ejecución): su operador de llamada de función puede ser declarado (inline).
- La utilización de las variables miembros del functor permite una flexibilidad de utilización que es complicado de obtener con punteros de función.
- El functor puede tener un estado.
- El functor presenta un enfoque objeto que es apreciable para un desarollador acostumbrado a esto.
I-B-2. Predicado
I-B-3. Función pura
Una función pura es una función cuyo resultado depende únicamente de sus parámetros.
I-B-4. Clase predicado
Una clase predicado es un functor cuyo el operador de llamada de función devulve un booleano.
Tengo que anotar que los algoritmos de la STL pueden efectuar copias "escondida" de una clase
predicado. Es por eso que el operador de llamada de función de una clase predicado tiene que ser una función
pura. En efecto, es difícil manejar perfectamente el contexto (en particular las variables miembros) de
la clase copiada cuando la copia es más o menos escondida dentro de la STL.
I-C. ptr_fun(), mem_fun() et mem_fun_ref()
Estas funciones, llamadas función-adaptador, no son muy agradables a leer ni a escribir, y sus nombres no son
muy explícitos. Sin embargo, tienen un papel muy importante. Son definidas en el cabecero <functional>.
¿Para qué sirven?
Una vez más, un poco de código vale más que un discurso largo. Fijaos el código siguiente:
void dum1 (UnaClase & objet);
class UnaClase
{
public :
void dum2 ();
} ;
std:: vector< UnaClase> v1;
std:: vector< UnaClase* > v2;
|
Si se desea aplicar nuestra función dum1() a todo el vector v1, podemos hacer así:
for_each ( v1.begin (), v1.end (), dum1 ) ;
|
Este código funciona porque dum1() no es una función miembra.
En cambio, si se desea aplicar la función miembro UnaClase::dum2() en todo el vector, el código
siguiente no funcionará:
for_each ( v1.begin (), v1.end (), & UnaClase:: dum2 );
|
Para hacer simple, el convenio de la STL no permite esto.
Entonces, habrá que utilizar los adaptadores de función objeto, es decir:
- mem_fun_ref() en caso de que el contenedor contiene instancias del objeto (es el caso de v1 en mi ejemplo).
- mem_fun() en caso de que el contenedor contiene punteros sobre instancias de objeto (es el caso de v2 en mi ejemplo).
std:: for_each (v1.begin (), v1.end (), mem_fun_ref ( & UnaClase:: dum2 ) );
std:: for_each ( v2.begin (), v2.end (), mem_fun ( & UnaClase:: dum2 ) );
|
En lo que se refiere a ptr_fun(), vamos a necesitarlo para utilizar los 4 adaptadores de función estándares de la STL, es decir not1(),
not2(), bind1st() y bind2nd(). En efecto, estos adaptadores necesitan unos typedefs específicos que son declarados en ptr_fun().
Por ejemplo, tomemos el caso de un predicado que nos dice si una instancia de UnaClase es válida:
bool IsValid (const UnaClase* object);
|
Si quiero encontrar el primer objeto de v2 que no es válido, sería intentado hacer:
std:: vector< UnaClase* > :: iterator i = std:: find_if ( v2.begin (), v2.end (), std:: not1 ( IsValid ) );
|
El problema es que esto no compilará. Habrá que utilizar un adaptador:
std:: vector< UnaClase* > :: iterator i = std:: find_if ( v2.begin (), v2.end (), std:: not1 ( std:: ptr_fun ( IsValid ) ) );
|
De hecho, los adaptadores de funciones-objeto (mem_fun, mem_fun_ref y ptr_fun) sólo definen tipos los que
las funciones objeto necesitan.
Si quiere saber más sobre este tema, le invito a echar una ojo al item 41 del indispensable "Efective STL" de Scott Meyers.
I-D. bind1st() y bind2nd()
En líneas generales, estas 2 funciones permiten transformar un predicado que toma 2 argumentos en un predicado que
toma sólo uno, dándole el valor del argumento faltante.
Un poco de código para comprender mejor:
struct Identity
{
Identity ():count_ (0 ){ }
int operator () () { return count_+ + ; }
int count_;
} ;
bool IsSup (int a, int b) { return a> b;}
std:: vector< int > v (5 );
std:: vector< int > :: iterator it ;
std:: generate ( v.begin (), v.end (), Identity () );
|
Ahora, querríamos efectuar una búsqueda para encontrar el primer elemento de nuestro vector que sea superior a 2. El algoritmo
find_if() es ideal para hacer esto.
Nos gustaría poder escribir algo así:
it = std:: find_if ( v.begin (), v.end (), IsSup (2 ) );
|
Por supuesto, esto no compila. IsSup es un predicado binario, entonces necesita 2 argumentos. La solución es pasar por un binder:
it = std:: find_if ( v.begin (), v.end (), std:: bind2nd ( std:: ptr_fun ( IsSup ) , 2 ) );
|
Vamos a ver mas en detalle lo que esta última línea de código efectúa, porque en una línea, se hacen muchas cosas.
Así, como explicado en
capítulo precedente,
ptr_fun() es necesario para que el adaptador
bind2nd() pueda funcionar, no diré de eso más aquí.
find_if() va a crear un iterador escondido con el cual va a recorrer el contenedor segun los límites que le serán pasados en argumentos.
Va a aplicar un predicado pasándole el contenido de este iterador escondido hasta que este predicado devuelva
"true" o que el límite
superior sea sobrepasado. En nuestro ejemplo,
v.begin() es el límite inferior,
v.end() el límite superior, y
IsSup nuestro predicado.
El problema es que
find_if() toma un predicado unario, ahora si
IsSup() está bien un predicado, es un predicado binario. Y está alla
dónde
bind2nd() interviene: va a transformar (adaptar) nuestro predicado binario en un predicado unario. De hecho, es como si crearía una
nueva función
IsSup que se parecería a esto:
Bool IsSup ( std:: vector< int > :: iterator it ){ return (* it)> 2 ; }
|
bind2nd() suprime el secundo argumento de IsSup y reemplaza, en el cuerpo de la función, este argumento por el valor que pasamos en
argumento a bind2nd().
bind1st() hace exactamente la misma cosa pero él reemplaza el primero argumento. Por ejemplo, si se reemplaza, en la línea que se
acaba de analizar, bind2nd por bind1st:
IntIt it = std:: find_if ( v.begin (), v.end (), std:: bind1st ( std:: ptr_fun ( IsSup ), 2 ) );
|
En la literatura, encontrará diferentes términos utilizados para nombrar estas funciones particulares que son bind1st() y bind2nd().
En efecto, podremos reencontrarlos con el nombre de adaptador de función, binder, o todavía reductor de functor.
Las combinaciones de adaptadores y de reductores pueden dar el código bastante indigesto. Así otras soluciones son propuestas por boost. Ver particularmente
boost::bind y
boost::function.
I-E. A propósito de los iteradores de inserción
Un iterador de inserción (inserter en inglés) es un iterador un poco particular. Un iterador clásico se contenta de recorrer un contenedor. Un inserter va a
añadir, en otro contenedor, el elemento sobre el cual despunta .
Los iteradores de inserción son muy útiles para algoritmos de copia de elementos, porque ellos permiten construir un nuevo contenedor sin tener que hacer un
new.
Existen varias tipos de iteradores de inserción.
Los más comunes son:
- back_inserter: añade el elemento al fin del nuevo contenedor. (ver función merge() para un ejemplo)
- front_insert: añade el elemento al principio del nuevo contenedor
- ostream_iterator: añade el elemento en un flujo de tipo ostream
I-F. A propósito de la eficiencia.
Con el fin de verificar ciertos aspectos relacionados al eficiencia de la Librería "
algorithm", efectué varias series de pruebas. Estas pruebas no son detalladas aquí
porque el objetivo de este artículo no es hacer una comparación de eficiencia entre diversos modos de programar en C++.
Para estas pruebas, utilicé la función
generate (). Escogí esta función para varias razones.
En primer lugar, es una función simple de utilización y que necesita pocos líneas de código. Luego, es una función modificante, lo que nos permite
de verificar fácilmente su buen funcionamiento. Y finalmente, porque es una función que es fácil para imitar con un boucle
for , lo que devuelve la comparación más aigüe.
Para los resultados, utilicé dos métodos de cuantificación:
- El analizador de eficiencia VTune (cuando escribo estas líneas, una versión de evaluación de 30 días está disponible en el sitio de Intel).
- Un cronómetro "hecho mano" que utiliza las funciones de programa "alto eficiencia" de Windows (QueryPerformanceCounter(), etc).
I-E-1. Descripción de las pruebas
Para estas pruebas, he escrito 3 funciones gen1(), gen2() y gen3(). Todas estas funciones hacen exactamente la misma cosa: ellas
rellenan un contenedor para lo que contenedor[i] = i. Sin embargo, cada función es hecha de manera diferente.
El contenedor (container) y el tamaño de este contenedor (c_size) son variables globales, con el fin de aliviar el código de
las funciones. También utilizo un iterador MyIterator que será definido con arreglo al tipo del contenedor.
He aquí el código de estas tres funciones:
void gen1 ()
{
MyIterator end = container.end ();
int i = - 1 ;
for (MyIterator it = container.begin (); it ! = end; + + it)
* it = + + i;
}
|
void gen2 ()
{
int i = - 1 ;
int * end = & container[c_size- 1 ];
for (int * it = & container[0 ]- 1 ; it ! = end;)
* + + it = + + i;
}
|
struct MyGenerate
{
explicit MyGenerate () : count_ (0 ) { }
int operator () () { return + + count_; }
private :
int count_;
} ;
void gen3 ()
{
std:: generate ( container.begin (), container.end (), MyGenerate () );
}
|
I-E-2. Resultados
Resultados obtenidos con:
std:: vector< int > container;
const int c_size = 5000000 ;
typedef std:: vector< int > :: iterator MyIterator;
|
gen3() es más rápido, por término medio, que gen1() del 4,8 %. No es enorme,
pero ya vemos que la utilización de un algoritmo es más eficaz que de utilizar iterador.
gen3() es más rápido, por término medio, que gen2() del 0,2 %, que no es
representativo verdaderamente, pero el operador [] es el modo más rápido de acceder a un elemento
de uno vector, y el bucle de gen2() es muy optimizado.
Resultados obtenidos con:
std:: deque< int > container;
const int c_size = 500000 ;
typedef std:: deque< int > :: iterator MyIterator;
|
El contenedor deque no almacena sus elementos de manera adyacente, entonces no podemos utilizar
la función gen2() para esta prueba.
gen3() es más rápido, por término medio, que gen1() del 6,5 %. La diferencia es
más importante que con un vector porque la memoria de una deque es administrada de modo un
poco particular, y el algoritmo generate() "sabe " cómo optimizar su recorrido del contenedor.
Vemos pues que, desde el punto de vista del eficienca, los algoritmos son por lo menos tanto rápidos como
funciones muy bien optimizadas.
I-E-3. Eficiencia y functor
Los functores permiten aumentar la rapidez de ejecución, en comparación con un puntero de función, porque
se puede poner inline el operador de llamada de función. Sin embargo, hay que tener cuidado de una cosa, es
que el functor esta copiado frecuentemente, de modo escondido, cuando se utiliza los algoritmos de STL.
Entonces es importante asegurarse que el functor esta el más ligero posible.
Usted encontrará un ejemplo de esto en el código proporcionado con este artículo, en el functor Synchro.
Es exactamente lo que no hay que hacer: un functor muy pesado.
En caso de functor en el cual seríamos intentados añadir código, puede ser interesante utilizar la astucia
siguiente: crear functors específicos que puntan sobre una instancia de este grande functor, y que será
llamado por los algoritmos que lo necesitan, estos algoritmos que no utilizan directamente todavia al grande functor.
Por ejemplo, en el código proporcionado con este artículo, encontramos el functor siguiente:
struct Synchro
{
Synchro ( const StringVector & artists )
: artists_ ( artists )
{
std:: vector< AlbumList> ( artists_.size () ).swap ( albums_ );
StringPairs ().swap ( result_ );
current_ = albums_.begin ();
}
void operator () (const std:: string & str)
{
AlbumManager:: FindAllAlbumsOf ( str, * current_ );
std:: sort ( (* current_).begin (), (* current_).end () );
std:: for_each ( albums_.begin (), current_, Synchro:: Compare );
current_+ + ;
}
static void GetResult (StringPairs & result){ result = result_; }
private :
std:: vector< AlbumList> albums_;
const StringVector & artists_;
static std:: vector< AlbumList> :: iterator current_;
static StringPairs result_;
static void Compare (const AlbumList & list)
{
if ( current_- > size () = = list.size ()
& & std:: equal ( current_- > begin (),
current_- > end (),
list.begin (),
std:: mem_fun_ref ( & Album:: ReleasedSameYear ) ) )
{
result_.push_back ( StringPair ( (* current_)[0 ].GetArtist ().GetName (), list[0 ].GetArtist ().GetName () ) );
}
}
} ;
|
Este código debe ciertamente parecerle bastante oscuro, porque esta sacado de su contexto y no comentado. Pero el objetivo
aquí no es de comprenderlo, sino justo ver que tenemos aquí un grande functor con muchos objetos y funciones miembros,
y que este functor va a ser copiado frecuentemente de modo escondido, haciéndonos perder mucho en término de eficiencia.
Con el fin de evitar esto, es preferible añadir un functor intermediario que se encargará de llamar al operador de
llamada de función del grande functor:
class SynchroCompare
{
public :
SynchroCompare (Synchro* synchro) : synchro_ (synchro){ }
void operator () (const std:: string & str)
{
synchro_- > operator ()(str);
}
private :
Synchro* synchro_;
} ;
|
volver al principio
II. Lista de los algoritmos
Para cada uno de los algoritmos, voy a proporcionar un ejemplo de utilización. Usted encontrará dos tipos de código:
1. Código extraído del programa AlbumManager que usted encontrará al fin de este artículo.
Para estos extractos, utilizo una clase Album del que he aquí la versión simplificada:
class Album
{
public :
private :
Artist artist_;
std:: string title_;
unsigned short year_;
unsigned short nbTracks_;
} ;
|
En ciertos ejemplos, definiré unas funcionalidades suplementarias para esta clase.
En estos extractos de código, utilizaré regularmente 2 vectores:
main_list_ y second_list_ que son 2 vectores de Álbum.
2. Código escrito directamente en el capítulo correspondiente. En este caso, utilizaré a uno std::vector que declararé como un
array C de esta manera:
std:: vector< int > v = { 1 ,2 ,3 ,4 } ;
|
Este código no compila, pero en un objetivo evidente de claridad, utilizo esta notación en lugar de:
std:: vector< int > v;
v.push_back (1 );
v.push_back (2 );
v.push_back (3 );
v.push_back (4 );
|
Que, es correcta y compila, pero demasiado verboso.
II-A. Algoritmos no modificantes
II-A-1. for_each()
Complejidad: lineal.
Efectúa una operación sobre cada elemento de un intervalo.
Extraído de AlbumManager:
std:: ostream & operator < < (std:: ostream & stream, const Album & album)
{
stream < < album.title_ < < " , " < < album.artist_.GetName () < < " , " < < album.year_;
stream < < " , nb tracks: " < < album.nbTracks_ < < std:: endl;
return stream;
}
void PrintAlbum (const Album & album)
{
std:: cout < < album;
}
void PrintAlbumList (std:: vector< Album> albums)
{
std:: for_each ( albums.begin (), albums.end (), PrintAlbum );
}
|
II-A-2. find() y find_if()
Complejidad: lineal
find()devuelve el primer elemento de un intervalo que es igual (operador ==) a un valor dado.
Extraído de AlbumManager:
std:: string artistName = " un artista " ;
if ( std:: find (tmpStrVect_.begin (), tmpStrVect_.end (), artistName ) = = tmpStrVect_.end () )
{
tmpStrVect_.push_back ( artistName );
}
|
find_if() hago la misma cosa pero utiliza un predicado en lugar del operador ==.
Extraído de AlbumManager:
bool Album:: HaveSameTitle (std:: string title) const { return title_ = = title; }
bool FindByTitle (const std:: string & title, Album & album)
{
AlbumList:: iterator searched =
std:: find_if ( main_list_.begin (),
main_list_.end (),
std:: bind2nd ( std:: mem_fun_ref< bool , Album, std:: string> (& Album:: HaveSameTitle), title) );
if ( searched = = main_list_.end () )
return false ;
album = * searched;
return true ;
}
|
II-A-3. find_end()
Complejidad: generalmente cuadrático. Sin embargo, si los iteradores ambos son bidireccionales, entonces complejidad medio es lineal.
find_end() parécese mucho a search(). La diferencia es que search() busca el primer intervalo, mientras que find_end() busca el último intervalo.
std:: vector< int > v1 = { 2 , 6 , 7 , 8 , 4 , 6 , 7 , 8 , 3 } ;
std:: vector< int > v2 = { 6 , 7 , 8 } ;
std:: vector< int > :: iterator result = std:: find_end ( v1.begin (), v1.end (), v2.begin (), v2.end () );
|
II-A-4. find_first_of()
Complejidad: O(N.M), N y M son el tamaño de cada uno de los intervalos.
Busca el primer elemento e1 de un intervalo I1 como e1 sea igual a cualquier elemento de un otro intervalo I2.
Extraído de AlbumManager:
bool AlbumManager:: GetCommonAlbums (std:: vector< Album> & albums)
{
std:: vector< Album> ().swap ( albums );
std:: vector< Album> :: iterator begin = main_list_.begin ();
std:: vector< Album> :: iterator end = main_list_.end ();
std:: vector< Album> :: iterator it;
do
{
it = std:: find_first_of ( begin, main_list_.end (), second_list_.begin (), second_list_.end () );
if ( it ! = end )
{
albums.push_back ( * it );
begin = it + 1 ;
}
}
while ( it ! = end );
return ( albums.size () = = 0 ) ? false : true ;
}
|
II-A-5. adjacent_find()
Complejidad: lineal.
Busca dos elementos adyacentes que son idénticos (por un criterio).
Extraído de AlbumManager:
std:: vector< Album> :: iterator found = mergedList.begin ();
do
{
found = std:: adjacent_find ( found, mergedList.end () );
if ( found ! = mergedList.end () )
{
result.push_back ( * found );
found+ + ;
}
} while ( found ! = mergedList.end () );
|
II-A-6. count() y count_if()
Complejidad: lineal
count() devuelve el número de elementos que son iguales (operador ==) a un elemento dado.
Std :: vector < int > v = { 2 ,3 ,4 ,2 ,4 ,2 } ;
int count = std:: count (v.begin (), v.end (), 2 ) ;
|
count_if() hace la misma cosa, pero utiliza un predicado dado al sitio del operador ==.
Extraído de AlbumManager:
bool Album:: HaveSameArtist (std:: string artist) const { return artist_.GetName () = = artist; }
std:: string artistName = " un artista " ;
int nbAlbums = (int ) std:: count_if ( main_list_.begin (),
cut,
std:: bind2nd ( std:: mem_fun_ref< bool , Album, std:: string> ( & Album:: HaveSameArtist ), artistName) );
|
II-A-7. mismatch()
Complejidad: lineal.
Devuelve los primeros elementos, de dos intervalos, que difieren.
|
Ambos intervalos comparados deben tener el mismo número de elementos. Si no, el resultado es indeterminado.
|
std:: vector< int > v1 = { 3 ,4 ,5 ,8 } ;
std:: vector< int > v2 = { 3 ,4 ,6 ,8 } ;
typedef std:: vector< int > :: iterator IntIt;
std:: pair< IntIt, IntIt> result = std:: mismatch ( v1.begin (), v1.end (), v2.begin () );
|
II-A-8. equal()
Complejidad: lineal.
Compara dos intervalos, elemento por elemento, y devuelve true si estos dos intervalos son idénticos.
|
Ambos intervalos comparados deben tener el mismo número de elementos. Si no, el resultado es indeterminado.
|
Usted encontrará un ejemplo concreto de utilización de equal() en el código de AlbumManager, pero él es utilizado
en un lugar un poco complicado, entonces prefiero darle un ejemplo simple aquí:
std:: vector< int > v1 = { 1 ,2 ,3 } ;
std:: vector< int > v2 = { 1 ,2 ,3 } ;
std:: vector< int > v3 = { 1 ,2 ,4 } ;
bool b = std:: equal (v1.begin (), v1.end (), v2.begin () );
b = std:: equal (v1.begin (), v1.end (), v3.begin () );
|
II-A-9. search() y search_n()
Complejidad: en el peor de los casos, ella es cuadratica, pero este caso es raro. La complejidad media es lineal.
search() Busca el primer caso de un intervalo en otro intervalo.
std:: vector< int > v1 = { 2 , 3 , 4 , 6 , 7 , 8 } ;
std:: vector< int > v2 = { 6 , 7 , 8 } ;
std:: vector< int > :: iterator result = std:: search ( v1.begin (), v1.end (), v2.begin (), v2.end () );
|
Complejidad: lineal
search_n() busca los primeros
n elementos consecutivos que son iguales a un elemento dado.
std:: vector< int > v = { 2 ,3 ,4 ,4 ,4 ,2 } ;
std:: vector< int > :: iterator result = std:: search_n ( v.begin (), v.end (), 3 , 4 );
|
retour au debut
II-B. Algoritmos modificantes
II-B-1. copy() y copy_backward()
Complejidad: lineal.
copy() copia un intervalo a partir del primer elemento.
Usted encontrará un ejemplo concreto de utilización de
copy() en el código de AlbumManager, pero él es utilizado
en un lugar un poco complicado, entonces prefiero darle un ejemplo simple aquí:
std:: vector< int > v = { 0 ,1 ,2 ,3 ,4 } ;
std:: copy ( v.begin (), v.end (), std:: ostream_iterator< int > (std:: cout, " " ));
|
copy_backward() hace la misma cosa que
copy() excepto que él rellena el intervalo de destinación empezando por el fin:
std:: vector< int > v2 ( v1.size ());
std:: copy_backward ( v1.begin (), v1.end (), v2.end () );
|
II-B-2. swap() y swap_ranges()
Complejidad: constante.
swap() intercambia los elementos de dos contenedores.
std:: vector< int > v1 = { 1 ,2 ,3 ,7 } ;
std:: vector< int > v2 = { 4 ,5 ,6 } ;
std:: swap ( v1, v2 );
|
Complejidad: lineal.
swap_range() intercambia los elementos de dos contenedores.
|
Ambos intervalos comparados deben tener el mismo número de elementos.
Si no, el resultado es indeterminado.
|
std:: vector< int > v1 = { 1 ,2 ,3 ,7 } ;
std:: vector< int > v2 = { 4 ,5 ,6 } ;
std:: swap_ranges ( v1.begin (), v1.begin () + v2.size (), v2.begin () );
|
|
Observamos en el ejemplo de arriba que swap_ranges() es aplicado únicamente sobre los tres
primeros elementos de v1. Si, en el lugar de v1.begin() + v2.size() habíamos
eligido v1.end(), el resultado habría sido indeterminado porque que v1 es más grande que v2.
|
II-B-3. transform()
Complejidad: lineal.
Modifica elementos de un intervalo en un otro intervalo aplicando una función. Fijale que el intervalo
de destinación puede ser el mismo que el intervalo fuente.
|
Ambos intervalos (fuente y destinación) deben tener el mismo número de elementos.
Si no, el resultado es indeterminado.
|
Extraído de AlbumManager:
unsigned short GetNbTracks () const { return nbTracks_;}
std:: vector< unsigned short > tracks ( main_list_.size () );
std:: transform ( main_list_.begin (), main_list_.end (), tracks.begin (), std:: mem_fun_ref ( & Album:: GetNbTracks ) );
|
II-B-4. Algoritmos de sustitución
Complejidad: lineal.
II-B-4-1. replace()
replace() reemplaza elementos que son iguales (operador ==) a un valor dado por otro valor.
std:: vector< int > v = { 1 ,4 ,3 ,4 ,1 } ;
std:: replace ( v.begin (), v.end (), 4 , 2 );
|
II-B-4-2. replace_if()
replace_if() hace la misma cosa que replace() pero él utiliza un predicado dado en lugar del operador ==.
bool IsOdd (int number){ return number& 1 ;}
std:: vector< int > v = { 1 ,4 ,3 ,4 ,1 } ;
std:: replace_if ( v.begin (), v.end (), IsOdd, 0 );
|
II-B-4-3. replace_copy()
replace_copy() reemplaza los elementos que tienen un valor especial copiando el intervalo entero.
std:: vector< int > v1 = { 1 ,4 ,3 ,4 ,1 } ;
std:: vector< int > v2 ( v1.size () );
std:: replace_copy ( v1.begin (), v1.end (), v2.begin (), 1 , 0 );
|
II-B-4-4. replace_copy_if()
replace_copy_if() hace la misma cosa que replace_copy() pero él copia sólo los elementos
que corresponden a un predicado dado.
bool IsOdd (int number){ return number& 1 ;}
std:: vector< int > v1 = { 1 ,4 ,3 ,4 ,1 } ;
std:: vector< int > v2 ( v1.size () );
std:: replace_copy_if ( v1.begin (), v1.end (), v2.begin (), IsOdd, 0 );
|
II-B-5. fill() y fill_n()
Complejidad: lineal.
fill() reemplaza cada elemento de un intervalo por un valor dado.
std:: vector< int > v = { 1 ,2 ,3 ,7 } ;
std:: fill ( v.begin (), v1.end (), 5 );
|
fill_n() hace la misma cosa pero se aplica sólo a
n elementos del intervalo.
std:: vector< int > v = { 1 ,2 ,3 ,7 } ;
std:: fill ( v.begin (), 2 , 5 );
|
II-B-6. generate() y generate_n()
Complejidad: lineal.
generate() reemplaza cada elemento de un intervalo por el resultado de una operación.
int count = 0 ;
int MyGenerate (){ return count+ + ; }
std:: vector< int > v (5 );
std:: generate ( v.begin (), v.end (), MyGenerate );
|
generate_n() hace la misma cosa que
generate() pero se aplica sólo a
n
elementos del intervalo. n debe ser inferior al tamaño del intervalo.
int count = 0 ;
int MyGenerate (){ return count+ + ; }
std:: vector< int > v (5 , 0 );
std:: generate_n ( v.begin (), 3 , MyGenerate );
|
II-B-7. algoritmos de supresión
Complejidad: lineal.
II-B-7-1. remove()
remove() suprime los elementos que tienen un valor dado.
std:: vector< int > v1 = { 1 ,2 ,3 ,4 } ;
std:: remove ( v1.begin (), v1.end (), 3 );
std:: copy ( v1.begin (), v1.end (), std:: ostream_iterator< int > (std:: cout, " " ));
|
Es por eso que hay que utilizar el iterador enviado por la función remove():
std:: vector< int > v1 = { 1 ,2 ,3 ,4 } ;
std:: vector< int > :: iterator it = std:: remove ( v1.begin (), v1.end (), 3 );
std:: copy ( v1.begin (), it, std:: ostream_iterator< int > (std:: cout, " " ));
|
Otra solución, por ejemplo, cuando el contenedor contiene punteros, consiste en utilizar la función
partition()
luego la función miembro de borrado del contenedor, generalmente
erase().
II-B-7-2. remove_if()
remove_if() hace lo mismo que remove() pero borra solamente los elementos correspondientes a un predicado dado.
bool IsOdd (int number){ return number& 1 ;}
std:: vector< int > v1 = { 1 ,2 ,3 ,4 ,5 ,6 ,7 } ;
std:: vector< int > :: iterator it = std:: remove_if ( v1.begin (), v1.end (), IsOdd);
std:: copy ( v1.begin (), it, std:: ostream_iterator< int > (std:: cout, " " ));
|
II-B-7-3. remove_copy()
remove_copy() copia los elementos que no corresponden a un valor dado.
std:: vector< int > v1 = { 1 ,2 ,3 ,4 ,3 ,6 } ;
std:: vector< int > v2 ( v1.size () );
std:: vector< int > :: iterator it = std:: remove_copy ( v1.begin (), v1.end (), v2.begin (), 3 );
std:: copy ( v2.begin (), it, std:: ostream_iterator< int > (std:: cout, " " ));
|
II-B-7-4. remove_copy_if()
remove_copy_if() copia los elementos que no corresponden a un predicado dado.
bool IsOdd (int number){ return number& 1 ;}
std:: vector< int > v1 = { 1 ,2 ,3 ,4 ,3 ,6 } ;
std:: vector< int > v2 ( v1.size () );
std:: vector< int > :: iterator it = std:: remove_copy_if ( v1.begin (), v1.end (), v2.begin (), IsOdd);
std:: copy ( v2.begin (), it, std:: ostream_iterator< int > (std:: cout, " " ));
|
II-B-8. unique() y unique_copy()
Complejidad: lineal.
unique(): Cada vez que un grupo de duplicados adyacente (elementos que son iguales a sus predecesores)
esta en un intervalo [principio, fin) dado,
unique() suprime todo los duplicados menos el primero.
unique() devuelve un iterador it tal como el intervalo [principio, it) no contenga duplicados.
Los elementos del intervalo [it, fin) son dereferenciables ya, pero los elementos sobre los cuales puntan son indeterminados.
Usted encontrará un ejemplo concreto de utilización de
unique() en el código de AlbumManager, pero él es utilizado
de manera un poco particular, entonces prefiero darle un ejemplo simple aquí:
std:: vector< int > v = { 3 ,1 ,1 ,2 ,4 } ;
std:: vector< int > :: iterator it = std:: unique ( v.begin (), v.end () );
std:: copy ( v.begin (), it, std:: ostream_iterator< int > (std:: cout, " " ));
|
|
La implementación exacta de unique() depende de su versión de la librería algorithm. El código más arriba utiliza el STL de MS VC 8,
y vemos que la orden de los elementos es modificada por unique().
El estándar especifica solamente que el intervalo situado antes del iterador devuelto por unique() no contiene doblones,
no da ninguna precisión sobre la orden de estos elementos.
|
unique_copy(): unique-copy hace lo mismo que
unique(), pero por encima, esto copia el resultado en otro contenedor.
std:: vector< int > v1 = { 1 ,2 ,2 ,3 ,4 ,4 ,3 ,6 } ;
std:: vector< int > v2;
std:: unique_copy (v1.begin (), v1.end (), std:: back_inserter (v2) );
|
II-B-9. reverse() y reverse_copy()
Complejidad: lineal.
reverse() invierte la orden de los elementos de un intervalo.
std:: vector< int > v = { 1 ,2 ,3 ,4 ,5 } ;
std:: reverse ( v1.begin (), v1.end () );
|
reverse_copy() copia los elementos de un intervalo fuente hacia un intervalo destino invirtiendo su orden.
|
Ambos intervalos deben contener el mismo número de elementos. Sino, el resultado es indeterminado.
|
std:: vector< int > v1 = { 1 ,2 ,3 ,4 ,5 } ;
std:: vector< int > v2 ( v1.size () );
std:: reverse_copy ( v1.begin (), v1.end (), v2.begin () );
|
II-B-10. rotate() y rotate_copy()
Complejidad: lineal.
rotate() efectúa una rotación sobre el orden de los elementos.
rotate() toma los
n
primeros elementos y les pone al fin del intervalo.
std:: vector< int > v = { 1 ,2 ,3 ,4 ,5 ,6 } ;
std:: rotate ( v.begin (), v.begin ()+ 2 , v.end () );
|
rotate_copy() copia los elementos efectuando una rotación sober sus orden.
std:: vector< int > v1 = { 1 ,2 ,3 ,4 ,5 ,6 } ;
std:: vector< int > v2 ( v1.size () );
std:: rotate ( v1.begin (), v1.begin ()+ 2 , v1.end (), v2.begin () );
|
II-B-11. random_shuffle()
Complejidad: lineal.
Modifica, de modo aleatorio, la orden de los elementos de un intervalo.
Extraído de AlbumManager:
bool GetRandomAlbum ( Album & album )
{
if ( main_list_.size () = = 0 )
return false ;
srand ( clock () );
std:: random_shuffle ( main_list_.begin (), main_list_.end () );
album = main_list_[0 ];
return true ;
}
|
II-B-12. partition() y stable_partition()
Complejidad: lineal.
partition() modifica la orden de los elementos de manera que los elementos que corresponden
a un criterio dado se encuentran delante.
Extraído de AlbumManager:
bool Album:: HaveSameArtist (std:: string artist) const { return artist_.GetName () = = artist; }
std:: string artistName = " le nom d'un artiste " ;
AlbumList:: iterator cut =
std:: partition ( main_list_.begin (),
main_list_.end (),
std:: bind2nd ( std:: mem_fun_ref< bool , Album, std:: string> (& Album:: HaveSameArtist ), artistName) );
|
Complejidad:
stable_partition() Es un algoritmo adaptativo: intenta asignar un buffer temporal, y su complejidad depende de la memoria disponible.
En lo peor de los casos (si ninguna memoria auxilliaire está disponible), hay en el peor de los casos N*log (N)
cambios, donde N está el número de elementos en el contenedor. Y en el mejor caso (tan mucha memoria
está disponible), complejidad es lineal. En todos los casos, el predicado es aplicado N la vez.
stable_partition() hace la misma cosa que
partition(), pero conserva la orden relativa de los
elementos correspondientes o no al criterio.
bool IsOdd (int number){ return number& 1 ;}
std:: vector< int > v1 = { 5 ,3 ,1 ,4 ,2 ,8 ,7 ,6 } ;
std:: stable_partition ( v1.begin (), v1.end (), IsOdd );
|
Vuelta al principio
II-C. Algoritmos de clasificación y operaciónes vinculadas
II-C-1. algoritmos de clasificación
II-C-1-1. sort()
Complejidad: N*log(N)
sort() clasifica los elementos de un intervalo.
std:: vector< int > v = { 3 ,2 ,4 ,1 ,5 } ;
std:: sort ( v.begin (), v.end () );
|
II-C-1-2. stable_sort()
Complejidad: stable_sort() es un algoritmo adaptativo: intenta asignar
un buffer temporal, y su complejidad depende de la memoria disponible.
En lo peor de los casos (si ninguna memoria auxilliaire está disponible), hay en el peor de los casos N*log (N)^2
comparaciones, donde N está el número de elementos en el contenedor. Y en el mejor caso (tan mucha memoria
está disponible), complejidad es N*log(N).
Clasifica preservando la orden de los elementos iguales.
bool CompareNoCase (char c1, char c2) { return std:: toupper (c1) < std:: toupper (c2); }
std:: vector< char > s1 = { d,B,b,C,A,D,a,c } ;
std:: stable_sort (s1.begin (), s1.end (), CompareNoCase );
|
II-C-1-3. partial_sort()
Complejidad: aproximadamente N*log(N).
partial_sort() Clasifica un intervalo hasta que n elementos hubieran sido clasificados.
std:: vector< int > v1 = { 5 ,3 ,1 ,4 ,2 ,8 ,7 ,6 } ;
std:: partial_sort ( v1.begin (), v1.begin () + 4 , v1.end () );
|
II-C-1-4. partial_sort_copy()
Complejidad: aproximadamente N*log(N).
partial_sort_copy() copia los elementos an un orden clasificado.
std:: vector< int > v1 = { 5 ,3 ,1 ,4 ,2 ,8 ,7 ,6 } ;
std:: vector< int > v2 (4 );
std:: partial_sort_copy ( v1.begin (), v1.begin () + 4 , v2.begin (), v2.end () );
|
II-C-2. nth_element()
Complejidad: lineal, por término medio.
Clasifica con arreglo a la enésima posición.
nth_element() es igual a partial_sort(), en el sentido donde esta función clasifica
parcialmente un intervalo de elementos: arregla el intervalo
[first, last) de manera que el elemento apuntado por el iterador nth sea el mismo que el elemento
que debería estar en esta posición si el intervalo entero [first, last) había sido clasificado. Además,
ningún elemento del intervalo [nth, last) es inferior a todos los elementos en el intervalo [first, last).
std:: vector< int > v1 = { 5 ,3 ,1 ,4 ,2 ,8 ,7 ,6 } ;
std:: nth_element ( v.begin (), v.begin ()+ 3 , v.end () );
|
|
En el ejemplo más arriba, el contenedor íntegramente esta clasificado. Esto respeta las especificaciones.
Ciertamente por razones de eficacia, ciertas versiones del STL parecen implementar nth_element()
como una clasificación simple en ciertas circunstancias.
|
II-C-3. Búsqueda binaria
II-C-3-1. lower_bound()
Complejidad: log(N).
lower_bound() encuentra el primer elemento superior o igual a un valor dado. Típicamente
lo utilizamos para insertar un elemento en un intervalo clasificado para respetar la orden.
std:: vector< int > v = { 1 ,2 ,4 ,6 ,9 } ;
std:: vector< int > :: iterator it = std:: lower_bound ( v.begin (), v.end (), 4 );
|
II-C-3-2. upper_bound()
upper_bound() encuentra el primer elemento superior a un valor dado.
std:: vector< int > v = { 1 ,2 ,4 ,6 ,9 } ;
std:: vector< int > :: iterator it = std:: upper_bound ( v.begin (), v.end (), 4 );
|
II-C-3-3. equal_range()
Complejidad: o(2 x ln N).
equal_range() intenta encontrar un elemento en un intervalo clasificado. Esta
función devuelve un par de itérateurs i y j tales, que i es la primera posición donde el valor
puede ser insertado sin modificar la orden del intervalo, y j es la última posición donde el
valor puede ser insertado sin modificar la orden de él intervalo. Resulta de eso que cada
elemento del intervalo [i, j) es equivalente al valor buscado.
std:: vector< int > v = { 1 ,2 ,4 ,4 ,6 ,7 } ;
std:: pair< std:: vector< int > :: iterator, std:: vector< int > :: iterator> newRange ;
newRange = std:: equal_range (v.begin (), v.end (), 4 ) ;
|
II-C-3-4. binary_search()
Complejidad: log(N).
binary_search() devuelve true si el intervalo contiene un elemento dado.
std:: vector< int > v1 = { 1 ,2 ,4 ,6 } ;
bool b = std:: binary_search ( v1.begin (), v1.end (), 9 );
|
II-C-4. merge() y inplace_merge()
Complejidad: lineal.
merge() fusiona dos intervalos
I1 et
I2 en un tercero intervalo
I3.
I1 y
I2 deben estar clasificados, y el resultado de la fusión estara igualmente clasificado.
Extraído de AlbumManager:
std:: vector< int > v1 = { 1 ,4 ,5 ,7 } ;
std:: vector< int > v2 = { 2 ,3 ,6 ,8 } ;
std:: vector v3;
std:: merge ( v1.begin (), v1.begin (), v2.begin (), v2.end (), back_inserter (v3) );
|
Complejidad: stable_sort() es un algoritmo adaptativo: intenta asignar
un buffer temporal, y su complejidad depende de la memoria disponible.
inplace_merge() combina 2 intervalos clasificados I1 y I2 del mismo contenedor C. Más precisamente,
comienza por construir un intervalo I3 = [I1.begin, I2.end], luego efectúa una selección ascendiente sobre I3.
inplace_merge() es estable, lo que significa que la orden relativa de los intervalos iniciales es
preservada.
std:: vector< int > v = { 1 ,3 ,5 ,2 ,4 ,5 ,6 } ;
std:: inplace_merge ( v.begin (), v.begin ()+ 3 , v.end () );
|
II-C-5. Operaciones de "set" sobre intervalos clasificados
Complejidad: lineal.
II-C-5-1. includes()
|
Estos algoritmos deben ser aplicados sobre intervalos clasificados.
|
includes() devuelve true si cada elemento de un intervalo también es un elemento de un otro intervalo.
Extraído de AlbumManager :
if ( std:: includes ( main_list_.begin (), main_list_.end (), second_list_.begin (), second_list_.end () ) )
{
return main_list_;
}
if ( std:: includes ( second_list_.begin (), second_list_.end (), main_list_.begin (), main_list_.end () ) )
{
return second_list_;
}
|
II-C-5-2. set_union()
set_union() efectúa la operación aritmética de unión sobre 2 intervalos clasificados.
std:: vector< int > v1 = { 1 ,2 ,2 ,3 ,3 ,4 ,4 ,6 } ;
std:: vector< int > v2 = { 2 ,2 ,6 ,6 ,8 } ;
std:: vector< int > v3;
std:: set_union ( v1.begin (), v1.end (), v2.begin (), v2.end (), std:: back_inserter (v3) );
|
II-C-5-3. set_intersection()
set_intersection() efectúa la operación aritmética de intersection sobre 2 intervalos clasificados.
std:: vector< int > v1 = { 1 ,2 ,2 ,3 ,3 ,4 ,4 ,6 } ;
std:: vector< int > v2 = { 2 ,2 ,6 ,6 ,8 } ;
std:: vector< int > v3;
std:: set_intersection ( v1.begin (), v1.end (), v2.begin (), v2.end (), std:: back_inserter (v3) );
|
II-C-5-4. set_difference()
set_difference() efectúa la operación aritmética de diferencia sobre 2 intervalos clasificados.
std:: vector< int > v1 = { 1 ,2 ,2 ,3 ,3 ,4 ,4 ,6 } ;
std:: vector< int > v2 = { 2 ,2 ,6 ,6 ,8 } ;
std:: vector< int > v3;
std:: set_difference ( v1.begin (), v1.end (), v2.begin (), v2.end (), std:: back_inserter (v3) );
|
II-C-5-5. set_symetric_difference()
set_symetric_difference() efectúa la operación aritmética de diferencia simétrica sobre 2 intervalos clasificados.
std:: vector< int > v1 = { 1 ,2 ,2 ,3 ,3 ,4 ,4 ,6 } ;
std:: vector< int > v2 = { 2 ,2 ,6 ,6 ,8 } ;
std:: vector< int > v3;
std:: set_symetric_difference ( v1.begin (), v1.end (), v2.begin (), v2.end (), std:: back_inserter (v3) );
|
II-C-6. Montones (heap)
Un montón (heap) es una estructura organizada según los criterios siguientes:
No hay elemento estrictamente superior al primer elemento del montón.
El primer elemento del montón debe poder ser quitado (pop) en O(log (N))
Un elemento puede ser añadido (push) también en O (log (N))
Un ejemplo de utilización de los algoritmos "heap".
II-C-6-1. make_heap()
Complejidad: lineal. En el peor de los casos, 3*N comparaisons.
make_heap() transforma un contenedor en montón.
std:: vector< int > v = { 3 ,4 ,5 ,1 ,4 ,3 ,8 } ;
std:: make_heap ( v.begin (), v.end () );
|
II-C-6-2. push_heap()
Complejidad: Logarítmico.
push_heap() añade un elemento en un montón. De verdad, hace la misma cosa que make_heap() pero su
complejidad es menor ya que el contenedor ya es un montón.
std:: vector< int > v = { 8 ,4 ,6 ,4 ,3 ,1 ,3 } ;
v.push_back (9 );
std:: push_heap ( v.begin (), v.end () );
|
II-C-6-3. pop_heap()
Complejidad: Logarítmico.
pop_heap() pop_heap toma el elemento más grande, entonces el primero, y lo pone en el fin del contenedor.
|
pop_heap() no suprime el elemento y entonces, no modifica el tamaño del contenedor
|
std:: vector< int > v = { 9 ,8 ,6 ,4 ,3 ,1 ,3 ,4 } ;
std:: pop_heap ( v.begin (), v.end () );
|
II-C-6-4. sort_heap()
Complejidad: en el peor de los casos O(log(N)).
sort_heap() clasifica un montón.
std:: vector< int > v = { 9 ,8 ,6 ,4 ,3 ,1 ,3 ,4 } ;
std:: sort_heap ( v.begin (), v.end () );
|
II-C-7. Mínimo y máximo
II-C-7-1. min()
Devuelve el más pequeño de 2 elementos.
int num = std:: min (3 , 4 );
|
II-C-7-2. max()
Devuelve el más grande de 2 elementos.
int num = std:: max (3 , 4 );
|
II-C-7-3. min_element()
Complejidad: lineal
Devuelve el más pequeño elemento de un intervalo.
bool Album:: IsNewerThan (const Album & other) const { return year_ < other.GetYear (); }
Album GetOldest ()
{
AlbumList:: const_iterator oldest =
std:: min_element ( main_list_.begin (), main_list_.end (), std:: mem_fun_ref ( & Album:: IsNewerThan ) );
return Album (* oldest);
}
|
II-C-7-4. max_element()
Complejidad: lineal
Devuelve el elemento más grande de un intervalo.
bool Album:: IsNewerThan (const Album & other) const { return year_ < other.GetYear (); }
Album GetMostRecent ()
{
AlbumList:: const_iterator mostRecent =
std:: max_element ( main_list_.begin (), main_list_.end (), std:: mem_fun_ref ( & Album:: IsNewerThan ) );
return Album (* mostRecent);
}
|
II-C-8. lexicographical_compare()
Complejidad: lineal.
Devuelve si un intervalo es inferior a otro según una orden lexicográfica.
|
Ambos intervalos comparados deben contener el mismo número de elementos. Si no es el caso, el resultado es indeterminado.
|
std:: vector< int > v1 = { 1 ,3 ,4 ,5 } ;
std:: vector< int > v2 = { 2 ,3 ,4 ,5 } ;
std:: lexicographical_compare ( v1.begin (), v1.end (), v2.begin (), v2.end () );
|
II-C-9. algoritmos de permutación
II-C-9-1. next_permutation()
Complejidad: lineal.
next_permutation() transforma un intervalo para que se haga la próxima permutación más grande y
lexicográfica. Devuelve true si una permutación se efectuó, false si no.
std:: vector< int > v1 = { 5 ,3 ,1 ,4 ,2 } ;
std:: next_permutation ( v1.begin (), v1.end () );
|
II-C-9-2. prev_permutation()
prev_permutation() transforma un intervalo para que se haga la próxima permutación más pequeña y
lexicográfica. Devuelve true si una permutación se efectuó, false si no.
std:: vector< int > v1 = { 5 ,3 ,1 ,4 ,2 } ;
std:: prev_permutation ( v1.begin (), v1.end () );
|
Vuelta al principio
II-D. Algorithmos numéricos
Los algoritmos numéricos estan definidos en la cabecera <numeric>.
II-D-1. accumulate()
Complejidad: lineal.
Combina todos los valores de los elementos (efectúa la suma, producto, etc.)
La función accumulate() utiliza un valor inicial. Hay varias razones para las cuales este valor
es importante, la primera es que esto permite obtener siempre un resultado válido
(particularmente en caso de que el intervalo está vacío).
Usted encontrará un ejemplo concreto de utilización de accumulate() en el código fuente
de AlbumManager, pero es utilizado en un contexto un poco particular, entonces prefiero darle un ejemplo
simple aquí:
std:: vector< int > v = { 1 ,2 ,3 ,4 ,5 } ;
int r = std:: accumulate ( v.begin (), v.end (), 0 );
int Mult (const int a, const int b){ return a* b; }
r = std:: accumulate ( v.begin (), v.end (), 1 , Mult );
|
II-D-2. inner_product()
Complejidad: lineal.
Combina todos los elementos de dos intervalos.
std:: vector< int > v1 = { 1 ,2 ,4 ,6 } ;
std:: vector< int > v2 = { 2 ,1 ,2 ,- 1 } ;
int r = std:: inner_product ( v1.begin (), v1.end (), v2.begin (), 0 );
|
II-D-3. adjacent_difference()
Complejidad: lineal.
Combina cada elemento con su predecesor.
std:: vector< int > v1 = { 1 ,2 ,4 ,6 } ;
std:: vector< int > v2 ( v1.size () );
std:: adjacent_difference ( v1.begin (), v1.end (), v2.begin () );
|
II-D-4. partial_sum()
Complejidad: lineal.
Combina cada elemento con cada uno de sus predecesores.
std:: vector< int > v1 = { 1 ,2 ,4 ,6 } ;
std:: vector< int > v2 ( v1.size () ) ;
std:: adjacent_difference ( v1.begin (), v1.end (), v2.begin () );
|
Un ejemplo de utilización intensiva de los algoritmos: AlbumManager
Este programa administra una lista de álbumes. Va a leer 2 archivos de texto y a crear 2 listas de álbumes:
una lista principal ( ain_list_) y una lista secundaria (second_list_).
Luego, vamos a poder aplicar toda una variedad de algoritmos sobre estas dos listas.
El fin de este pequeño programa únicamente es implementar un máximo de algoritmos diferentes.
Entonces, desde luego, la utilización de estos algoritmos no es siempre adaptada perfectamente, y a menudo,
las mejores soluciones habrían sido posibles.
Ademas, no intenté optimizar este programa.
|
Los comentarios de este program son en frances.
|
Annexos
V-A. Fonctionoides
No utilizo fonctionoides en el código fuente de ejemplo proporcionado con este artículo, y no es impescindible
saber lo que es para utilizar los algoritmos de la STL. Sin embargo, esta técnica permite resolver hábilmente
problemas que se ponen a veces cuando se utiliza estos algoritmos. Entonces es interesante hablar un poco de esos.
Se trata de un concepto inherente a la poo (programación orientada objeto) que permite utilizar un mecanismo
que reúne las ventajas de los functores y de la herencia del C++.
Tomemos un ejemplo para ayudarnos a entender el funcionamiento de este animal extraño.
Tomemos una clase Coche, proveída de una función GetNbRuedas() que devuelve el
número de ruedas del vehículo. En nuestro programa, tenemos un arrayde Coche que representa nuestro
parque de vehículo, y en un momento dado, necesitamos saber el número total de ruedas de nuestro array de coches.
// estructura Coche
struct Coche
{
int GetNbRuedas() const {return 4 ;}
};
// un array de coches
std::vector<Coche> v;
// Una implementación posible para recuperar el número total de ruedas:
int ComputeWheelNumber()
{
// Creamos un vector que contiene el número de rueda de cada coche
std::vector<int> nbRuedas( v.size() );
std::transform( v.begin(), v.end(), nbRuedas.begin(), std::mem_fun_ref( &Coche::GetNbRuedas ) );
// Devolvemos la suma de todos los elementos de este vector
return std::accumulate( nbRuedas.begin(), nbRuedas.end(), 0 );
}
|
Luego, avanzamos en nuestro programa, y llega un momento cuando se debe añadir motocicletas en nuestro parque
de vehículos. Un modo de resolver el problema de la cuenta de ruedas es utilizar fonctionoides.
Esto consiste en crear una clase madre (a la que se llamará Vehiculo) que contiene una función virtual
pura (GetNbRuedas()) y derivar de eso 2 clases chicas (Coche y Motocicleta) que
implementarán esta función según sus especificidades:
clase Vehiculo
{
public :
virtual int GetNbRuedas () const = 0 ;
} ;
class Coche : public Vehiculo
{
public :
int GetNbRuedas () const { return 4 ;}
} ;
Class Motocicleta : public Vehiculo
{
public :
int GetNbRuedas () const { return 2 ;}
} ;
|
Nuestro vector v será ahora un array de punteros de Vehiculo. En efecto, dado que Vehiculo es una clase abstracta,
no puede ser instanciada, no podemos declarar un vector simple de Vehiculo:
std:: vector< Vehiculo* > v;
|
Uno de los aspectos interesantes de esta técnica es que casi no necesitamos modificar la función
ComputeWheelNumber(). En efecto, la única modificación que tenemos que efectuar está sobre el adaptador de
función
mem_fun() (ver capítulo I-C
chapitre suivant), y viene por
el hecho de que
v es ahora un vector de apuntadores:
int ComputeWheelNumber ()
{
std:: vector< int > nbRuedas ( v.size () );
std:: transform ( v.begin (), v.end (), nbRuedas.begin (), std:: mem_fun ( & Vehicule:: GetNbRuedas ) );
return std:: accumulate ( nbRuedas.begin (), nbRuedas.end (), 0 );
}
|
En realidad, lo que es verdaderamente interesante en el sistema del fonctionoides es poder aliar la potencia
de los functores que permite pasarles parámetros a los constructores (con el fin de servirse de eso
en las funciones miembros) y la fuerza de la herencia y de la sobrecarga. Sin embargo, no es el sujeto de
este artículo, también no hablaré más de esto.
V-B. Referencias
Página inicial
Vuelta al principio
Copyright © 2010 Rodrigo Pons.
Aucune reproduction, même partielle, ne peut être faite
de ce site ni de l'ensemble de son contenu : textes, documents, images, etc.
sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à
trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.