Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUM C++ FAQs C++ TUTORIELS C++ LIVRES C++ COMPILATEURS C++ SOURCES C++ Qt

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 fr 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 :
// déclaration d'une fonction dum1 qui prend une référence vers une instance de UneClasse en paramètre.
void dum1(UneClasse & objet) ; 

// classe UneClasse qui possède une fonction membre dum2
class UneClasse
{
public :
void dum2() ;
};

// un vecteur de UneClasse
std::vector<UneClasse> v1; 

// un vecteur de pointeurs sur UneClasse
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 ) ; // ok

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 ); // ko

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 ) ); //ok
std::for_each( v2.begin(), v2.end(), mem_fun( &UneClasse::dum2 ) ); //ok

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 ) ); //ko

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 ) ) ); //ok

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:
// on commence par définir un foncteur qui va nous permettre de construire un conteneur rapidement
struct Identity
{
	Identity():count_(0){}
	int operator () () { return count_++; }
	int count_;
};

// on défini également un prédicat binaire qui compare 2 entiers
bool IsSup(int a, int b) {return a>b;}

// on déclare un vecteur d'entiers de 5 éléments
std::vector<int> v(5);

// ainsi qu'un itérateur qui va nous permettre de travailler sur notre vecteur
std::vector<int>::iterator it ;

// et enfin, on rempli notre vecteur grâce à l'algorithme generate() et notre foncteur
std::generate( v.begin(), v.end(), Identity() );
// nous avons maintenant  v = {0,1,2,3,4}

Voir description de la fonction generate()

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) ); //ko

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 ) ); //ok
// le résultat de cette ligne est que it pointe sur 3 (la première valeur supérieure à 2)

Voir description de la fonction find_if()

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 ) );
//résultat: it pointe sur 0, puisque c'est le premier élément du tableau qui soit inférieur à 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 en boost::bind et en 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
Vous trouverez des exemples d'utilisation des inserteurs dans ce document, notemment dans le chapitre sur la fonction copy().

Références:

fr FAQ C++ sur les itérateurs
en La doc de rogueware sur les inserters


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 :
// une première fonction gen1() qui effectue une boucle for simple et utilise l'itérateur correspondant au conteneur
void gen1()
{
	MyIterator end = container.end();
	int i = -1;
	for (MyIterator it = container.begin(); it != end; ++it)
		*it = ++i;
}
// une deuxième fonction gen2() qui utilise un pointeur pour parcourir le conteneur
// attention, cette méthode ne fonctionne que si tous les éléments du conteneur utilisé sont assurés d'être contigüs 
// en mémoire.
void gen2()
{
	int i = -1;
	int * end = & container [c_size-1];
	for(int * it = & container [0]-1; it != end;)
		*++it = ++i;
}
// une troisième fonction gen3() qui utilise l'algorithme generate() et un foncteur MyGenerate
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:
	// constructeurs, accesseurs et autres
private:
	Artist		artist_; // un objet de type Artist qui définit notamment le nom de l'artiste
	std::string		title_; // titre le l'album
	unsigned short	year_; //année de sortie de l'album
	unsigned short	nbTracks_; //nombre de pistes sur l'album
};
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:
// surcharge de l'opérateur d'indirection
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;
}

// affiche un album sur la sortie standard en utilisant l'opérateur d'indirection défini au dessus
void PrintAlbum(const Album & album)
{
	std::cout << album;
}

// affiche une liste d'albums sur la sortie standard
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" ;

// tmpStrVect_ est un vecteur de string qui contient des noms d'artistes
// le code suivant rajoute un nouvel artiste dans tmpStrVect_ si cet artiste n'est pas déjà présent dans ce vecteur
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:
// HaveSameTitle est un prédicat qui retourne true si le titre passé en paramètre est le même que celui de l'album
bool Album::HaveSameTitle(std::string title) const { return title_ == title; }

// cette fonction retrouve un album en fonction de son titre
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} ; // ne compile pas (cf. chapitre 2.0)
std::vector<int> v2 = {6, 7, 8}; // ne compile pas (cf. chapitre 2.0)
std::vector<int>::iterator result = std::find_end( v1.begin(), v1.end(), v2.begin(), v2.end() ); 
//retourne un iterateur pointant sur le 6eme élément de v1

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:
// Cette fonction créé un vecteur avec les albums qui sont communs à deux vecteurs d'albums
bool AlbumManager::GetCommonAlbums(std::vector<Album> & albums)
{
	// on commence par vider le vecteur résultat
	std::vector<Album>().swap( albums );

	// on déclare plusieurs itérateurs dont nous avons besoin dans la boucle
	std::vector<Album>::iterator begin = main_list_.begin();
	std::vector<Album>::iterator end = main_list_.end();
	std::vector<Album>::iterator it;
	
// A chaque itération, nous recherchons le premier album commun aux 2 listes, puis, si on a trouvé un,
	// on repositionne begin afin de recommencer la boucle en ignorant la partie de main_list_ qui
	// a déjà été traitée.
	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 ); // si it == end, ça signifie que l'on a rien trouvé, on peut donc arêter

	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:
// recherche des doublons dans un vecteur.
// on boucle sur le vecteur fusionné, et à chaque fois 
// qu'on tombe sur un doublon, on le rajoute au vecteur resultat
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} ; // ne compile pas (cf. chapitre 2.0)
int count = std::count(v.begin(), v.end(), 2) ; //retourne 3


   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.

warning 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}; // ne compile pas (cf. chapitre 2.0)
std::vector<int> v2 = {3,4,6,8}; // ne compile pas (cf. chapitre 2.0)
// v1.size()</