Les algorithmes de la STL
Date de publication : 04/09/2007 , Date de mise à jour : 12/04/2008
Par
r0d
Cet article constitue une description des concepts nécessaires à une bonne utilisation des algorithmes de la STL.
Il fournit des informations sur chacun de ces algorithmes (complexité, précisions sur l'utilisation, etc.),
ainsi qu'un exemple d'utilisation pour chacun d'entre eux.
Introduction
I. Généralités
I-A. Pourquoi utiliser les algorithmes de la STL
I-B. Foncteurs, prédicats...
I-B-1. Foncteur (ou classe foncteur)
I-B-2. Prédicat
I-B-3. Fonction pure
I-B-4. Classe prédicat
I-C. ptr_fun(), mem_fun() et mem_fun_ref()
I-D. bind1st() et bind2nd()
I-E. Un mot sur les inserteurs
I-F. Notes sur les performances
I-E-1. Description des tests
I-E-2. Résultats
I-E-3. Performances et foncteurs
II. Liste des algorithmes
II-A. Algorithmes non modifiants
II-A-1. for_each()
II-A-2. find() et find_if()
II-A-3. find_end()
II-A-4. find_first_of()
II-A-5. adjacent_find()
II-A-6. count() et count_if()
II-A-7. mismatch()
II-A-8. equal()
II-A-9. search() et search_n()
II-B. Algorithmes modifiants
II-B-1. copy() et copy_backward()
II-B-2. swap() et swap_ranges()
II-B-3. transform()
II-B-4. Algorithmes de remplacement
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() et fill_n()
II-B-6. generate() et generate_n()
II-B-7. algorithmes de suppression
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() et unique_copy()
II-B-9. reverse() et reverse_copy()
II-B-10. rotate() et rotate_copy()
II-B-11. random_shuffle()
II-B-12. partition() et stable_partition()
II-C. Algorithmes de tri et operations liées
II-C-1. algorithmes de tri
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. recherche binaire
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() et inplace_merge()
II-C-5. opérations de "set" sur des intervalles triés
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. Les tas (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. minimum et maximum
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. algorithmes de permutation
II-C-9-1. next_permutation()
II-C-9-2. prev_permutation()
II-D. Algorithmes numériques
II-D-1. accumulate()
II-D-2. inner_product()
II-D-3. adjacent_difference()
II-D-4. partial_sum()
Un exemple d'utilisation intensive des algorithmes: AlbumManager
Annexes
V-A. Fonctionoïds
V-B. Références et liens
V-C. Remerciements
Introduction
La
bibliothèque "
algorithm" est une partie de la STL. Elle est définie dans 2 en-têtes : <algorithm> et <numeric>.
Les algorithmes de la STL forment un outil extrêmement puissant. Dans certaines situations de la vie réelle d'un
développeur c++, cette partie de la STL peut s'avérer réellement indispensable. Il m'est arrivé plus d'une fois de devoir
implémenter de petits outils, simples et efficaces, mais dont les contraintes de qualité et de temps ont transformé
l'implémentation du petit programme en véritable défi. Et si je m'en suis toujours sorti, c'est bien souvent grâce à ces
algorithmes dont je vais vous parler ici.
Ce document est un survol de l'ensemble des algorithmes. J'ai recensé plus de 50 fonctions, et la majorité d'entre
elles comporte plusieurs versions surchargées. Je ne peux donc raisonnablement pas traiter en détail chacune d'entre elles,
d'autant plus que l'on retrouve une philosophie commune. De plus, je ne vais parler ici que des algorithmes qui font partie
du standard et donc, qui seront implémentés par tous les compilateurs sérieux.
Pour illustrer certains algorithmes, j'ai choisi d'implémenter un petit programme que vous trouverez en pièce jointe
à la fin de cet article. Certains ne sont pas implémentés dans ce petit programme, mais ils sont illustrés tout de même par
quelques lignes de code, dans le chapitre qui leur est dédié.
I. Généralités
I-A. Pourquoi utiliser les algorithmes de la STL
A part quelques rares cas, il est toujours préférable d'utiliser les algorithmes fournis par la STL plutôt que des
algorithmes "fait main".
Si j'ai choisi de commencer mon article par cette phrase, ce n'est pas anodin, car elle peut être sujette à
polémique. Cependant, beaucoup de développeurs oublient souvent qu'un bon programme doit rassembler plusieurs qualités,
et qu'il ne suffit pas qu'il soit seulement plus rapide, ou plus
portable
, ou plus
maintenable, ou plus
modulaire, etc..
La plupart du temps, il doit être un bon compromis de toutes ces qualités. Et les algorithmes (et la STL plus généralement)
proposent le meilleur compromis. Pourquoi ?
- L'abstraction:
Ces algorithmes offrent un niveau d'abstraction qui permet une implémentation trés propre. Ces algorithmes permettent d'appliquer une opération à un ensemble
d'éléments sans avoir se préoccuper en détail de la nature de ces éléments, et sans avoir à implémenter un code laborieux et spécialisé pour le faire.
- Le gain de temps, en terme de "temps de développement":
De nombreux algorithmes sont déjà implémentés et permettent de résoudre
les problèmes les plus fréquents en quelques lignes de code.
- La robustesse du code:
Ces algorithmes sont utilisés par des milliers de logiciels et respectent
des standards qui en assurent la robustesse.
- La rapidité d'exécution:
L'utilisation des algorithmes de la STL supprime certaines informations
redondantes (test sur l'itérateur de fin de boucle par exemple). De plus, les développeurs qui implémentent ces algorithmes
connaissent les structures internes des conteneurs
sur lesquels ils travaillent et peuvent ainsi optimiser beaucoup mieux que
nous ne pourrions le faire. Enfin, les algorithmes de la STL utilisent les méthodes les plus récentes de l'art, donc les plus
efficaces.
- La maintenabilité du code:
La STL fournit un standard connu et reconnu par des milliers de développeurs. Ainsi, lorsqu'un développeur se trouve confronté à du code qui
utilise ces algorithmes, il est en terrain connu et passe moins de temps à comprendre ce code.
I-B. Foncteurs, prédicats...
Tous ces types de fonctions et de fonctions-objet sont très présents dans la STL, et il est indispensable d'en connaitre un minimum
à leur sujet pour en utiliser correctement les algorithmes. Je vais donc commencer par introduire un peu de vocabulaire. C'est assez
désagréable j'en conviens, mais je suis convaincu que vous m'en remercierez plus tard.
I-B-1. Foncteur (ou classe foncteur)
Ni le C ni le C++ ne permettent de passer des fonctions en paramètres. C'est la raison pour laquelle il faut utiliser des
pointeurs de fonction. Un foncteur fais la même chose qu'un pointeur de fonction, mais en beaucoup mieux.
Un foncteur est, tout simplement, une classe qui surcharge l'opérateur d'appel de fonction ( operator () ). Le
foncteur présente au moins 3 avantages importants par rapport au pointeur de fonction :
- Performance : son opérateur d'appel de fonction peut être inliné.
- L'utilisation des variables membres du foncteur permet une souplesse d'utilisation qu'il est compliqué d'obtenir avec
des pointeurs de fonction.
- Le foncteur peut avoir un état.
- Le foncteur présente une approche objet qui est appréciable pour un développeur habitué à cela.
I-B-2. Prédicat
Un prédicat est une fonction qui retourne un booléen. Beaucoup d'algorithmes de la STL utilisent un prédicat.
Voir
FAQ C++ sur les prédicats
I-B-3. Fonction pure
Une fonction pure est une fonction dont le résultat ne dépend que de ses paramètres.
I-B-4. Classe prédicat
Une classe prédicat est un foncteur dont l'opérateur d'appel de fonction renvoie un booléen.
Il est à noter que les algorithmes de la STL peuvent être amenés à effectuer des copies d'une classe prédicat.
C'est pourquoi il faut que l'opérateur d'appel de fonction d'une classe prédicat soit une fonction pure. En effet, il est
difficile de gérer parfaitement le contexte (en particulier les variables membres) de la classe copiée lorsque la copie est
plus ou moins cachée dans la STL.
I-C. ptr_fun(), mem_fun() et mem_fun_ref()
Ces fonctions, appelées adaptateurs de fonctions objet (function object adapter en anglais), ou helpers ne sont pas très agréables à lire ni à écrire,
et leurs noms ne sont pas très explicites, cependant, elles ont un rôle très important. Elles sont définies dans l'en-tête <functional>.
A quoi servent-elles ?
Une fois de plus, un peu de code vaut mieux qu'un long discours. Prenons donc le code suivant :
void dum1(UneClasse & objet) ;
class UneClasse
{
public :
void dum2() ;
};
std::vector<UneClasse> v1;
std::vector<UneCLasse*> v2;
|
Si l'on souhaite appliquer notre fonction dum1() à tout le vecteur v1, on peut faire ainsi :
for_each( v1.begin(), v1.end(), dum1 ) ;
|
Ce code fonctionne car dum1() est une fonction non-membre.
En revanche, si l'on souhaite appliquer la fonction membre UneClasse::dum2() à tout le vecteur, le code suivant ne fonctionnera pas :
for_each( v1.begin(), v1.end(), &UneClasse::dum2 );
|
Pour faire simple, la convention de la STL implique que la façon dont sont implémentés les algorithmes ne permet pas cela.
Il faudra donc utiliser les adaptateurs de fonction objet, à savoir :
- mem_fun_ref() dans le cas où le conteneur contient des instances de l'objet (c'est le cas de v1 dans mon exemple).
- mem_fun() dans le cas où le conteneur contient des pointeurs vers des instances d'objet (c'est le cas de v2 dans mon exemple).
std::for_each(v1.begin(), v1.end(), mem_fun_ref( &UneClasse::dum2 ) );
std::for_each( v2.begin(), v2.end(), mem_fun( &UneClasse::dum2 ) );
|
En ce qui concerne ptr_fun(), nous allons en avoir besoin pour utiliser les 4 adaptateurs de fonction standards de la STL, à savoir not1(),
not2(), bind1st() et bind2nd(). En effet, ces adaptateurs nécessitent des typedefs spécifiques qui sont déclarés dans ptr_fun().
Par exemple, prenons le cas d'un prédicat qui nous dit si une instance de UneClasse est valide :
bool IsValid(const UneClasse* object);
|
Si je veux trouver le premier objet de v2 qui n'est pas valide, je serais tenté de faire :
std::vector<UneClasse*>::iterator i = std::find_if( v2.begin(), v2.end(), std::not1( IsValid ) );
|
Le problème c'est que ceci ne compilera pas. Il faudra passer par un adaptateur :
std::vector<UneClasse*>::iterator i = std::find_if( v2.begin(), v2.end(), std::not1( std::ptr_fun( IsValid ) ) );
|
En fait, les adaptateurs de fonctions objet (
mem_fun,
mem_fun_ref et
ptr_fun) ne font que définir des types dont les fonctions objet ont besoin.
Si vous voulez en savoir plus, je vous invite à jeter un coup d'oeil sur l'item 41 de l'indispensable
Effective STL de Scott Meyers.
I-D. bind1st() et bind2nd()
En gros, ces 2 fonctions permettent de transformer un prédicat qui prend 2 arguments en un prédicat qui n'en prend qu'un, en lui donnant la valeur de l'argument manquant.
Un peu de code pour mieux comprendre:
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() );
|
Maintenant, on voudrait effectuer une recherche pour trouver le premier élément de notre tableau qui est supérieur à 2. L'algorithme find_if() est idéal pour faire cela.
Nous aimerions pouvoir écrire quelque chose comme ça:
it = std::find_if( v.begin(), v.end(), IsSup(2) );
|
Bien évidemment, cela ne compile pas. IsSup est un prédicat binaire, il nécessite donc 2 arguments. La solution est de passer par un binder:
it = std::find_if( v.begin(), v.end(), std::bind2nd( std::ptr_fun( IsSup ) , 2 ) );
|
Voyons de plus près ce que cette dernière ligne de code effectue, car en une ligne, on fait beaucoup de choses ici.
Comme expliqué dans le
précédent chapitre,
ptr_fun() est nécessaire pour que l'adaptateur
bind2nd() puisse fonctionner, je n'en dirai pas plus ici.
find_if() va créer un itérateur caché avec lequel il va parcourir le conteneur en fonction des bornes qui lui seront passées en arguments. Il va appliquer un prédicat en lui passant le contenu de cet itérateur caché jusqu'à ce que ce prédicat retourne vrai ou que la borne supérieure soit dépassée. Dans notre exemple,
v.begin() est la borne inférieure,
v.end() la borne supérieure, et
IsSup notre prédicat.
Le problème c'est que
find_if() prend un prédicat unaire, or si
IsSup() est bien un prédicat, c'est un prédicat binaire. Et c'est là que
bind2nd() intervient : il va transformer (adapter) notre prédicat binaire en prédicat unaire. En fait, c'est comme s'il créait une nouvelle fonction
IsSup qui ressemblerait à ça :
Bool IsSup( std::vector<int>::iterator it ){ return (*it)>2; }
|
bind2nd() supprime le 2eme argument de IsSup et remplace, dans le corps de la fonction, cet argument par la valeur que nous avons passée en argument à bind2nd().
bind1st() fait exactement la même chose mais il remplace le 1er argument. Par exemple, si l'on remplace, dans la ligne que l'on vient d'analyser, bind2nd par bind1st:
IntIt it = std::find_if( v.begin(), v.end(), std::bind1st( std::ptr_fun( IsSup ), 2 ) );
|
Dans la littérature, vous trouverez différents termes utilisés pour nommer ces fonctions particulières que sont bind1st() et bind2nd(). En effet, nous pourrons les retrouver sous le nom de function adapter (adaptateur de fonction), binder, ou encore foncteur réducteur.
Les combinaisons d'adaptateurs et de réducteurs peuvent rendre le code assez indigeste. Ainsi d'autres solutions sont proposées par boost. Voir notamment
boost::bind et
boost::function.
I-E. Un mot sur les inserteurs
Un inserteur (inserter en englais) est un itérateur un peu particulier. Un itérateur classique se contente de parcourir un conteneur. Un inserter lui, va ajouter l'élément sur lequel il pointe dans un autre conteneur.
Les inserteurs sont très utiles pour des algorithmes de copie d'éléments, car ils permettent de construire un nouveau conteneur sans avoir à l'allouer.
Il existe plusieurs sortes d'inserteurs.
Les plus couremment utilisés sont:
- back_inserter: il ajoute l'élément à la fin du nouveau conteneur. (voir fonction merge() pour un exemple)
- front_insert: il ajoute l'élément au début du nouveau conteneur
- ostream_iterator: il ajoute l'élément dans un flux de type ostream
I-F. Notes sur les performances
Afin de vérifier certains aspects concernant les performances de la bibliothèque "
algorithm", j'ai effectué plusieurs séries de tests. Ces tests ne sont pas détaillés ici car le but de cet article n'est pas de faire une comparaison de performances entre diverses façons de programmer en C++.
Pour ces tests, j'ai utilisé la fonction
generate(). J'ai choisi cette fonction pour plusieurs raisons.
Tout d'abord, c'est une fonction simple d'utilisation et qui ne nécessite par beaucoup de lignes de code. Ensuite, c'est une fonction modifiante, ce qui nous permet
de vérifier facilement son bon fonctionnement. Et enfin, car c'est une fonction qui est facile à imiter avec une boucle
for, ce qui rend la comparaison plus aigüe.
Pour les résultats, j'ai utilisé deux méthodes de quantification:
- L'analyseur de performance VTune (dont une version d'évaluation de 30 jours est disponible sur le site d'Intel).
- Un chronomètre "fait main" qui utilise les fonctions de timing "haute performance" de Windows ( QueryPerformanceCounter(), etc. ).
I-E-1. Description des tests
Pour ces tests, j'ai écris 3 fonctions gen1(), gen2() et gen3(). Toutes ces fonctions font exactement la même chose : elles remplissent un conteneur de façon à ce que conteneur[i] = i. Cependant, chaque fonction est implémentée de façon différente.
Le conteneur (variable container) et la taille de ce conteneur (c_size) sont des variables globales, afin d'alléger le code des fonctions. J'utilise également un itérateur MyIterator qui sera défini en fonction du type du conteneur.
Voici le code de ces trois fonctions :
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. Résultats
Résultats obtenus avec:
std::vector<int> container;
const int c_size = 5000000;
typedef std::vector<int>::iterator MyIterator;
|
gen3() est, en moyenne, plus rapide que gen1() de 4,8%. Ce n'est pas énorme, mais on voit déjà que l'utilisation d'un algorithme est plus efficace que d'utiliser des itérateurs.
gen3() est, en moyenne, plus rapide que gen2() de 0,2%, ce qui n'est pas vraiment représentatif, mais l'opérateur [] est la façon la plus rapide d'accéder à un élément d'un vector, et la boucle de gen2() est très optimisée.
Résultats obtenus avec:
std::deque<int> container;
const int c_size = 500000;
typedef std::deque<int>::iterator MyIterator;
|
Le conteneur deque ne stocke pas ses éléments de façon contigüe, nous ne pouvons donc pas utiliser la fonction gen2() pour ce test.
gen3() est, en moyenne, plus rapide que gen1() de 6,5%. La différence est plus importante qu'avec un vector car la mémoire d'une deque est gérée de façon un peu particulière, et l'algorithme generate() "sait" comment optimiser son parcours du conteneur.
Nous voyons donc que, du point de vue des performances, les algorithmes sont au moins aussi rapides que des fonctions très bien optimisées.
I-E-3. Performances et foncteurs
Les foncteurs permettent d'accroître la rapidité d'exécution, en comparaison avec un pointeur de fonction, parce qu'on peut inliner l'opérateur d'appel de fonction. Cependant, il faut faire attention à une chose, c'est que le foncteur est fréquemment copié, de façon cachée, lorsque on utilise les algorithmes de la STL. Il est donc important de faire en sorte que le foncteur reste le plus léger possible.
Vous trouverez un exemple de cela dans le code fourni avec cet article, dans le foncteur Synchro. C'est exactement ce qu'il ne faut pas faire: un foncteur très lourd.
Lorsqu'on se retrouve dans le cas où nous avons un foncteur dans lequel nous serions tentés de rajouter du code, il peut être intéressant d'utiliser la ruse suivante : créer des foncteurs spécifiques qui pointent sur une instance de ce gros foncteur, et qui seront appelés par les algorithmes qui en ont besoin, ces algorithmes n'utilisant plus directement le gros foncteur.
Par exemple, dans le code fourni avec cet article, on trouve le foncteur suivant:
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() ) );
}
}
};
|
Ce code doit certainement vous paraître assez obscur, car sorti de sont contexte et non commenté. Mais le but ici n'est pas de le comprendre, mais juste de voir que nous avons là un gros foncteur avec beaucoup d'objets et de fonctions membres, et que ce dernier va être copié fréquemment de façon cachée, nous faisant perdre beaucoup en terme de performance.
Afin d'éviter cela, il est préférable de rajouter un foncteur intermédiaire qui se chargera d'appeler l'opérateur d'appel de fonction du gros foncteur:
class SynchroCompare
{
public:
SynchroCompare(Synchro* synchro) : synchro_(synchro){}
void operator () (const std::string & str)
{
synchro_->operator()(str);
}
}
private:
Synchro* synchro_;
};
|
II. Liste des algorithmes
Pour chacun des algorithmes, je vais fournir un exemple d'utilisation. Vous trouverez deux types de code:
1. Du code extrait du programme AlbumManager dont vous trouverez le code source en pièce jointe à la fin de cet article.
Pour ces extraits, j'utilise une classe Album dont voici la version simplifiée :
class Album
{
public:
private:
Artist artist_;
std::string title_;
unsigned short year_;
unsigned short nbTracks_;
};
|
Dans certains exemples, je serai amené à définir certaines fonctionnalités supplémentaires pour cette classe.
Dans ces extraits de code, j'utiliserai régulièrement 2 vecteurs:
main_list_ et second_list_ qui sont des vecteurs d'Album.
2. Du code écrit directement dans le chapitre correspondant. Pour ce dernier, j'utiliserai un std::vector que je déclarerai à la façon d'un tableau C de cette manière:
std::vector<int> v = {1,2,3,4};
|
Ce code ne compile pas, mais dans un but évident de clarté, j'utilise cette notation à la place de:
std::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
|
Qui elle, est correcte et compile, mais est beaucoup trop verbeuse.
II-A. Algorithmes non modifiants
II-A-1. for_each()
Complexité: linéaire.
Effectue une opération sur chaque élément d'un intervalle.
Extrait 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() et find_if()
Complexité: linéaire
find() retourne le premier élément d'un intervalle qui est égal (opérateur ==) à une valeur donnée.
Extrait de AlbumManager:
std::string artistName = "un artiste" ;
if ( std::find(tmpStrVect_.begin(), tmpStrVect_.end(), artistName ) == tmpStrVect_.end() )
{
tmpStrVect_.push_back( artistName );
}
|
find_if() fait la même chose mais utilise un prédicat à la place de l'opérateur ==.
Extrait 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()
Complexité: généralement quadratique. Cependant, si les itérateurs sont tous deux bidirectionnels, alors la complexité moyenne est linéaire.
find_end() ressemble beaucoup à search(). La différence est que search() cherche le premier intervalle, alors que find_end() cherche le dernier intervalle.
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()
Complexité: O(N.M), N et M étant la taille de chacun des intervalles.
Recherche le premier élément e1 d'un intervalle I1 tel que e1 soit égal à n'importe quel élément d'un autre intervalle I2.
Extrait 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()
Complexité: linéaire.
Recherche deux éléments adjacents qui sont identiques (par un critère).
Extrait 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() et count_if()
Complexité: linéaire
count() retourne le nombre d'éléments qui sont égaux (opérateur ==) à un élément donné.
Std ::vector <int> v = {2,3,4,2,4,2} ;
int count = std::count(v.begin(), v.end(), 2) ;
|
count_if() fait la même chose, mais utilise un prédicat donné à la place de l'opérateur ==.
Extrait de AlbumManager:
bool Album::HaveSameArtist(std::string artist) const { return artist_.GetName() == artist; }
std::string artistName = "un artiste";
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()
Complexité: linéaire.
Retourne les premiers éléments, de deux intervalles, qui diffèrent.
 |
Les deux intervalles comparés doivent comporter le même nombre d'éléments. Si ce n'est pas le cas, le résultat est indéterminé.
|
std::vector<int> v1 = {3,4,5,8};
std::vector<int> v2 = {3,4,6,8};
|