Annexe B Algorithmes de la bibliothèque standard STL Hansel, G., Passeport pour C++ Concepts et mise en pratique. Chap. 23 et annexe A, 1999. Cay Horstmann & Timothy Budd, La Bible C++. John Wiley & Sons, Inc., 2004, 1230p. On retrouve aussi dans la bibliothèque standard STL un jeu de fonctions appelées algorithmes. Pour ces fonctions, l’accès aux données contenues dans un conteneur s’effectue exclusivement par le biais d’itérateurs. Les algorithmes sont indépendants des classes mises en jeu dans les conteneurs. De plus, un pointeur usuel pouvant être considéré comme un cas particulier d’itérateur, les algorithmes sont également utilisables pour les tableaux usuels. Pour utiliser un algorithme, il faut préalablement inclure le fichier <algorithm>. Dans l’annexe A de la référence de Hansel citée plus haut, on retrouve une description complète des algorithmes de la STL. C’est aussi le cas des deux ouvrages suivants : Bjarne Stroustrup, The C++ Programming Language, Special Ed., AddisonWesley, 2000. Stanley B. Lippman et Josée Lajoie, C++ Primer, 3rd ed., Addison-Wesley, 1998. La bibliothèque standard contient plusieurs douzaines d’algorithmes génériques. Nous étudierons ici que les plus fréquents et les plus utiles. Pour simplifier la présentation de ces fonctions, celles-ci sont réparties en plusieurs catégories. A. Algorithmes d’initialisation Ce sont des algorithmes servant principalement à initialiser une séquence. Un conteneur peut être initialisé par remplissage (initialiser avec une même valeur fixe), par copie (dupliquer des valeurs depuis un conteneur pour en remplir un second) ou par génération (exécuter une fonction de façon répétée et employer les valeurs obtenues pour initialiser un conteneur). Remplissage : Deux algorithmes permettent de remplir un conteneur. Le premier utilise comme arguments une plage d’itérateurs et une valeur, puis affecte à chaque élément de la plage la valeur spécifiée. vector<int> a(10); fill(a.begin(), a.end(), 1); Le deuxième algorithme reçoit un itérateur, un compteur entier et une valeur. Il initialise le nombre de positions spécifié par le compteur avec la valeur donnée. 1 fill_n(a.begin(), 5, 2); // Remplit les 5 premières positions de a avec la // valeur 2. Ces deux algorithmes ne peuvent servir à ajouter de nouveaux éléments dans un conteneur mais, uniquement, à modifier la valeur de certains éléments d’un conteneur. Copie : L’algorithme générique copy sert à copier un conteneur dans un autre. Il reçoit 3 arguments, les deux premiers spécifiant la plage du conteneur d’entrée, le troisième indiquant le point de départ du conteneur de destination. Il est supposé, et non vérifié, que la destination est suffisamment grande pour accepter l’entrée. vector<int> b(20); copy(a.begin(), a.end(), b.begin()); // Initialise les 10 premières positions de b. Génération : L’algorithme générique generate permet d’initialiser un conteneur grâce à un générateur. vector<int> c(20); generate(c.begin(), c.end(), AleatoireInt(7, 25)); // Remplit le vecteur c de valeurs aléatoires comprises entre 7 et 25. L’algorithme generate_n reçoit un itérateur, un compteur entier et un générateur. generate_n(c.begin(), 10, AleatoireInt(10, 30)); // Remplit les 10 premières composantes du vecteur c de valeurs aléatoires // comprises entre 10 et 30. B. Algorithmes de transformations Cette catégorie d’algorithmes génériques permet de transformer une séquence de valeurs en une nouvelle séquence. l’algorithme reverse : Cet algorithme inverse les éléments d’une séquence, de façon à ce que le dernier élément devienne le nouveau premier, le premier le nouveau dernier, etc. vector<int> a(10); generate(a.begin(), a.end(), Generateur_suite(1)); // Initialement 1, 2, …, 10. reverse(a.begin(), a.end()); // Maintenant 10, 9, 8, …, 1. l’algorithme sort : Pour trier des collections stockées dans un conteneur vector, un deque ou un tableau ordinaire, nous pouvons recourir à l’algorithme générique sort. 2 vector<int> a(10); generate(a.begin(), a.end(), AleatoireInt(10, 30)); sort(a.begin(), a.end()); L’algorithme sort ne peut être employé avec des listes, car il nécessite l’emploi d’itérateurs d’indexation, absents des listes. Toutefois, le conteneur list contient une fonction membre sort utilisant un autre algorithme. l’algorithme random_shuffle : Cet algorithme réarrange de façon aléatoire les éléments d’une collection. Il peut être utilisé avec des vecteurs, des dèques ou des tableaux ordinaires mais non avec des listes, des ensembles ou des cartes (map et multimap). Les arguments doivent être des itérateurs d’accès aléatoire. vector<int> a(10); generate(a.begin(), a.end(), Generateur_suite(1)); random_shuffle(a.begin(), a.end()); Il est impossible de prédire ce que pourrait être la séquence résultante. Un résultat possible serait : 9, 4, 8, 6, 2, 10, 1, 3, 5, 7. l’algorithme rotate : L’algorithme effectue dans une séquence une rotation autour d’un élément pivot spécifié par un itérateur. Cela divise la séquence en deux parties non nécessairement de même longueur, puis intervertit l’ordre des 2 parties, en conservant l’ordre relatif dans chaque partie. vector<int> a(10); generate(a.begin(), a.end(), Generateur_suite(1)); vector<int>::iterator pivot = find(a.begin(), a.end(), 7); rotate(a.begin(), pivot, a.end()); La séquence 1 2 3 4 5 6 7 8 9 10 devient 7 8 9 10 1 2 3 4 5 6. Le pivot doit faire partie de la plage des itérateurs. l’algorithme partition : La séquence est formée en déplaçant tous les éléments satisfaisant à un prédicat à un bout d’une séquence et tous les éléments échouant à ce prédicat à l’autre extrémité. vector<int> a(10); generate(a.begin(), a.end(), Generateur_suite(1)); partition(a.begin(), a.end(), paire); 3 Les valeurs paires sont déplacées au début et les valeurs impaires à la fin ce qui donne comme résultat : 10 2 8 4 6 5 7 3 9 1. Comme on peut le constater, l’ordre relatif des éléments dans une partition dans le conteneur résultant diffère de celui des valeurs dans la séquence originale. En substituant partition par stable_partition, cela garantit le maintien de l’ordre des valeurs résultantes : 2 4 6 8 10 1 3 5 7 9. l’algorithme transform : 1ière version : Étant donné une séquence d’entrée spécifiée par des itérateurs de début et de fin, une destination de sortie spécifiée par un unique itérateur et une fonction unaire, l’algorithme applique la fonction à chaque élément de l’entrée pour créer les éléments de sortie. int carre(int n) { return n * n; } vector<int> a(10); generate(a.begin(), a.end(), Generateur_suite(1)); vector<int> b(10); transform(a.begin(), a.end(), b.begin(), carre); 2ième version : Étant donné deux séquences d’entrée dont la première est spécifiée par des itérateurs de début et de fin, la seconde spécifiée par un itérateur de début, une destination de sortie spécifiée par un unique itérateur et une fonction binaire, l’algorithme applique la fonction binaire aux éléments correspondants de l’entrée pour créer les éléments de sortie. On suppose sans le vérifier que la deuxième séquence d’entrée comporte au moins autant d’éléments que la première et que la séquence résultat disposera de suffisamment de place pour accueillir le résultat. int addition(int a, int b) { return a + b; } vector<int> c(10); transform(a.begin(), a.end(), b.begin(), c.begin(), addition); l’algorithme merge : Étant donné deux séquences d’entrée triées spécifiées par des itérateurs de début et de fin, une destination de sortie spécifiée par un unique itérateur, l’algorithme fusionne les 2 séquences triées pour obtenir une nouvelle séquence triée. vector<int> a(10); vector<int> b(20); generate_n(a.begin(), 10, Generateur_suite(1)); generate_n(b.begin(), 20, Generateur_suite(5)); vector<int> c(30); merge(a.begin(), a.end(), b.begin(), b.end(), c.begin()); 4 l’algorithme next_permutation : Une permutation est une réorganisation des valeurs dans une séquence. Ex. : 6 permutations des valeurs 1 2 3 : 123 132 213 231 312 3 2 1. Une séquence de n éléments possède en principe n! permutations; la première est telle que les valeurs apparaissent en ordre croissant, tandis que dans la dernière, les valeurs figurent en ordre décroissant. L’algorithme next_permutation est une fonction booléenne qui transforme une séquence à la prochaine permutation et retourne true si elle existe, false autrement. Ex. : vector<int> a(4); generate(a.begin(), a.end(), Generateur_suite(1)); while (next_permutation(a.begin(), a.end())) { cout << “Permutation de sortie “; vector<int>::iterator p = a.begin(); while (p != a.end()) { cout << *p << “ “; ++p; } cout << endl; } Ex. : char mot[] = "ouf" ; char * p = mot; // Itérateur de début. char * q = mot + strlen(mot); // Itérateur de fin. sort(p, q); // Trie initialement les lettres en ordre croissant. while (next_permutation(p, q)) cout << mot << endl; C. Algorithmes de recherche Ces algorithmes permettent de localiser des éléments dans une séquence; ils renvoient un itérateur décrivant une position. Si aucun élément n’est localisé, ils renvoient l’itérateur de fin. max_element et min_element : Ex. : vector<int> d(10); generate(d.begin(), d.end(), AleatoireInt(10, 30)); vector<int>::iterator mx = max_element(d.begin(), d.end()); vector<int>::iterator mn = min_element(d.begin(), d.end()); 5 if (mx != d.end()) cout << *mx << " " << *mn << endl; else cout << "vecteur vide " << endl; find et find_if : Étant donné une séquence d’entrée spécifiée par des itérateurs de début et de fin, et la valeur de l’élément recherché, cette fonction find renvoie un itérateur identifiant le premier élément répondant à la condition de recherche, comme par exemple, pour localiser la position d’une valeur dans une liste. Une variante de l’algorithme find est find_if. Le troisième argument est un prédicat au lieu d’un élément du conteneur. L’algorithme renvoie la position de la première valeur satisfaisant au prédicat. Ex. : La portion de code renvoie la première année bissextile d’une liste d’années : bool annee_bissextile(int annee) { if (0 == annee % 400) return true; if (0 == annee % 100) return false; if (0 == annee % 4) return true; return false; } list<int> annees; … list<int> ::iterator premiere_annee_bissextile = find_if(annees.begin(), annees.end(), annee_bissextile); binary_search : Étant donné une séquence d’entrée triée spécifiée par des itérateurs de début et de fin, et la valeur de l’élément recherché, cette fonction booléenne indique si la valeur est ou non trouvée dans le conteneur. Ex. : vector<int> a(10); generate(a.begin(), a.end(), AleatoireInt(10, 30)); sort(a.begin(), a.end()); if(binary_search(a.begin(), a.end(), 7)) cout << "7 est trouve " << endl; lower_bound et upper_bound : La fonction lower_bound renvoie un itérateur indiquant où une valeur peut être insérée pour conserver la séquence triée; autrement dit, l’itérateur fait référence soit au premier élément supérieur ou égal à l’élément de recherche, soit à l’itérateur de fin si aucun élément n’est plus grand ou égal à l’élément de recherche. Par contre, la fonction upper_bound localise le premier élément strictement supérieur à l’élément de recherche. Ex. : vector<int> a(10); generate(a.begin(), a.end(), AleatoireInt(10, 30)); sort(a.begin(), a.end()); if(binary_search(a.begin(), a.end(), 7)) 6 { vector<int>::iterator p = lower_bound(a.begin(), a.end(), 7); vector<int>::iterator q = upper_bound(a.begin(), a.end(), 7); while (p != q) { cout << "trouve " << *p << endl; ++p; } } else cout << "non trouve " << endl; Si une collection contient déjà plusieurs copies de l’élément cherché, le sous-ensemble décrit par ces 2 valeurs correspond à la portion du conteneur où se trouvent ces valeurs. adjacent_find : Renvoie la position du premier élément égal au prochain élément qui suit. Ex. : À partir de la séquence des valeurs suivantes, 1 4 2 5 6 6 7 5, l’algorithme renvoie un itérateur correspondant à la première valeur de 6. search : Renvoie la position du premier élément d’un sous-ensemble dans une séquence de plus grande taille. equal : Cette fonction travaille avec 2 séquences de même taille et renvoie true si les éléments associés sont tous égaux. mismatch : Cette fonction travaille avec 2 séquences de même taille et renvoie une paire d’itérateurs indiquant ensemble les premières positions où les séquences possèdent des éléments distincts. D. Algorithmes de suppression et de remplacement remove_copy : Cette fonction reçoit comme argument une valeur spécifique et copie vers un autre conteneur la séquence du conteneur original à l’exception des instances de la valeur spécifique. Ex . : soient a = 1 2 3 2 4 2 5 6, b = 0 0 0 0 0 0 0 0, 7 L’appel à remove_copy(a.begin(), a.end(), b.begin(), 2) copie toutes les valeurs rencontrées sauf le 2 aboutissant à : a = 1 2 3 2 4 2 5 6, b = 1 3 4 5 6 0 0 0. La séquence résultante est plus petite que l’original. La fonction renvoie l’itérateur vers b indiquant la fin de la séquence résultat. remove_copy_if : L’appel à remove_copy_if(a.begin(), a.end(), b.begin(), divisible_par(2)) copierait toutes les valeurs sauf celles divisibles par 2, aboutissant à ceci : a = 1 2 3 2 4 2 5 6, b = 1 3 5 0 0 0 0 0. On peut remarquer que la taille du conteneur résultat, b, n’est pas modifiée même si seulement une portion initiale a été employée. remove : Cette fonction permet de supprimer toutes les instances d’une valeur fixe. Généralement, les algorithmes génériques ne modifient pas la taille des conteneurs sur lesquels ils fonctionnent. Cela peut donner lieu à des situations problématiques, comme dans l’exemple suivant : remove(a.begin(), a.end(), 2); Après cet appel, la séquence devient : a = 1 3 4 5 6 2 5 6. La portion initiale (les valeurs avant l’itérateur), est le résultat désiré. Les valeurs depuis l’itérateur jusqu’à la fin sont uniquement les éléments originaux du conteneur. Pour éliminer les valeurs superflues, il faut employer la fonction erase fournie par le conteneur : vector<int>::iterator p = remove(a.begin(), a.end(), 2); a.erase(p, a.end()); // Supprime les valeurs restantes. remove_if : Cette fonction est semblable à remove mais elle a recours à un prédicat : vector<int>::iterator p = remove(a.begin(), a.end(), divisible_par(2)); a.erase(p, a.end()); // La collection devient 1, 3, 5. replace : Cette fonction permet de remplacer les occurrences d’une valeur fixe par une autre. 8 vector<int> a(10); generate(a.begin(), a.end(), AleatoireInt(1, 5)); replace(a.begin(), a.end(), 3, 4); replace_if : Cette fonction permet de remplacer les éléments d’une séquence vérifiant un prédicat par une valeur fixe. vector<int> a(10); generate(a.begin(), a.end(), AleatoireInt(1, 5)); replace_if(a.begin(), a.end(), paire, 0); // Remplace tous les nombres pairs par 0. E. Autres algorithmes count et count_if : Ils servent à compter le nombre de valeurs d’une collection ou à compter le nombre de valeurs satisfaisant un prédicat. Exemple I: int count(input_iterator i, input_iterator j, const T &v); compte le nombre n de fois où une valeur v se rencontre entre les positions i et j du conteneur et retourne cette valeur n. #include <iostream> #include <algorithm> using namespace std; int table[] = {12, 321, 34, 13, 12, 15, 12}; ……. int n = 0; n = count(tab, tab + 7, 12); cout << n <<endl; Exemple II : int count_if(input_iterator i, input_iterator j, fonction booléenne F); compte le nombre d’occurrences dans l’intervalle [i, j[ qui satisfont à la condition du prédicat F et incrémente la variable n de ce nombre. #include <iostream> #include <algorithm> #include <vector> #include <math.h> using namespace std; bool premier(unsigned int n) 9 { for (int i = 2; i <= sqrt(n); i++) if ((n%i) == 0) return 0; return 1; } class Nombre_premier { public: bool operator()(unsigned int n) { for (int i = 2; i <= sqrt(n); i++) if ((n%i) == 0) return 0; return 1; }; }; void main() { int n = 0; int table[100]; for (int i = 2; i < 100; i++) table[i] = i; cout << "Il y a " << count_if(table+2,table+100, premier) << " nombres premiers entre 2 et 100" << endl; vector<int> v(table, table+100); cout << "Il y a " << count_if(v.begin(), v.end(), premier) << " nombres premiers entre 2 et 100" << endl; cout << "Il y a " << count_if(v.begin(), v.end(), Nombre_premier()) << " nombres premiers entre 2 et 100" << endl; } Quelques algorithmes numériques sont présents dans le fichier d’en-tête <numeric>, plutôt que dans <algorithm>. accumulate : Ex. : list<double> D; 10 ... double somme = accumulate(D.begin(), D.end(), 0.0); // somme contient maintenant la somme des éléments de la liste. etc. 11