Algorithm C++ STL
Algorithm C++ STL
L'en-tête ctime
Comme son nom l'indique, ce fichier d'en-tête contient plusieurs fonctions liées à la
gestion du temps. La plupart sont assez bizarres à utiliser et donc peu utilisées. De
toute façon, la plupart des autres bibliothèques, comme Qt, proposent des classes
pour gérer les heures, les jours et les dates de manière plus aisée.
#include <iostream>
#include <ctime>
int main()
cout << "Il s'est ecoule " << secondes << " secondes depuis le 01/01/1970." <<
endl;
return 0;
}
Utilisez les conteneurs
Les deux catégories de conteneurs
Les vectorsont bien évidemment des séquences puisque, comme je vous l'ai dit, toutes
les cases sont rangées de manière contiguë dans la mémoire.
une liste de tous les conteneurs de la STL triés suivant leur catégorie.
Séquences :
o vector
o deque
o list
o stack
o queue
o priority_queue
Conteneurs associatifs :
o set
o multiset
o map
o multimap
Pour utiliser ces conteneurs, il faut inclure le fichier d'en-tête correspondant.
if(a.empty())
Les vector ne sont vraiment pas adaptés pour effectuer des opérations mathématiques.
Méthode Description
push_back() Ajout d'un élément à la fin du tableau.
Méthode Description
pop_back() Suppression de la dernière case du tableau.
front() Accès à la première case du tableau.
back() Accès à la dernière case du tableau.
assign() Modification du contenu d'un tableau.
En termes techniques, on parle de structure LIFO (Last In First Out). Le dernier élément ajouté
est le premier à pouvoir être ôté
#include <stack>
#include <iostream>
using namespace std;
int main()
{
stack<int> pile; //Une pile vide
pile.push(3); //On ajoute le nombre 3 à la pile
pile.push(4);
pile.push(5);
cout << pile.top() << endl; //On consulte le sommet de la pile (le nombre 5)
pile.pop(); //On supprime le dernier élément ajouté (le nombre 5)
cout << pile.top() << endl; //On consulte le sommet de la pile (le nombre 4)
return 0;
}
#include <map>
#include <string>
using namespace std;
map<string, int> a;
Ce code déclare une table associative qui stocke des entiers mais dont les indices
sont des chaînes de caractères. On peut alors accéder à un élément via les
crochets[]comme ceci :
Les map ont un autre gros point fort : les éléments sont triés selon leur clé. Donc si l'on
parcourt une map du début à la fin, on parcourt les clés de la plus petite à la plus grande.
Le problème c'est que, pour parcourir une table associative du début à la fin, il faut utiliser
les itérateurs
Les setsont utilisés pour représenter les ensembles. On peut insérer des objets dans
l'ensemble et y accéder via une méthode de recherche. Par contre, il n'est pas possible d'y
accéder via les crochets.
Les multisetet multimapsont des copies des setet mapoù chaque clé peut exister en
plusieurs exemplaires.
map<string, int>::iterator it1; //Un itérateur sur les tables associatives string-
int
int main()
{
deque<int> d(5,6); //Une deque de 5 éléments valant 6
deque<int>::iterator it; //Un itérateur sur une deque d'entiers
Les itérateurs ne sont pas optimisés pour l'opérateur de comparaison. On ne devrait donc
pas écrireit<d.end()comme on en a l'habitude avec les index de tableau. Utiliser!=est
plus efficace.
Parmi les cinq types d'itérateurs, seuls deux sont utilisés pour les conteneurs :
les bidirectional iterators et les random access iterators.
L'important étant que l'on ne peut avancer que d'un seul élément à la fois :
Donc pour accéder au sixième élément d'un conteneur, il faut partir de la position
begin()puis appeler cinq fois l'opérateur++.
Cela veut dire que vous pouvez utiliser aussi bien++que—
Les éléments ne sont pas rangés les uns à côté des autres dans la mémoire.
Chaque « case » contient un élément et un pointeur sur la case suivante, située ailleurs
dans la mémoire
on peut facilement ajouter des éléments au milieu: Il n'est pas nécessaire de décaler
toute la suite comme dans l'exemple de la bibliothèque du chapitre précédent.
Mais (il y a toujours un mais) on ne peut pas directement accéder à une case
donnée… tout simplement parce qu'on ne sait pas où elle se trouve dans la mémoire
La structure interne des map: utilisent ce qu'on appelle des arbres binaires
Grâce aux itérateurs, ce n'est pas à vous de vous préoccuper de tout cela. Vous utilisez
simplement les opérateurs++et—
o Les itérateurs pointent en réalité sur despair. Ce sont des objets avec deux
attributs publics appelésfirstetsecond. Lespairsont déclarées dans le fichier
d'en-têteutility
int main()
{
map<string, double> poids; //Une table qui associe le nom d'un animal à son
poids
int main()
{
map<string, double> poids; //Une table qui associe le nom d'un animal à son
poids
if(trouve == poids.end())
{
cout << "Le poids du chien n'est pas dans la table" << endl;
}
else
{
cout << "Le chien pese " << trouve->second << " kg." << endl;
}
return 0;
}
Créez un foncteur
foncteur est une classe possédant si nécessaire des attributs et des méthodes. Mais, en plus de
cela, elle doit proposer un opérateur()
class Addition{
public:
int main()
{
Addition foncteur;
int a(2), b(3);
cout << a << " + " << b << " = " << foncteur(a,b) << endl; //On utilise le
foncteur comme s'il s'agissait d'une fonction
return 0;
}
Les prédicats
Ce sont des foncteurs prenant un seul argument et renvoyant un booléen
.
#include <iostream>
#include <functional> //Ne pas oublier !
using namespace std;
int main()
{
plus<int> foncteur; //On déclare le foncteur additionnant deux entiers
int a(2), b(3);
cout << a << " + " << b << " = " << foncteur(a,b) << endl; //On utilise le
foncteur comme s'il s'agissait d'une fonction
return 0;
}
Par défaut, si l'on ne spécifie rien, c'est un foncteur construit à partir de l'opérateur<qui sert de
comparaison
#include <string>
using namespace std;
class CompareLongueur
{
public:
bool operator()(const string& a, const string& b)
{
return a.length() < b.length();
}
};
int main()
{
//Une table qui associe le nom d'un animal à son poids
map<string, double,CompareLongueur> poids; //On utilise le foncteur comme
critère de comparaison
//On ajoute les poids de quelques animaux
poids["souris"] = 0.05;
poids["tigre"] = 200;
poids["chat"] = 3;
poids["elephant"] = 10000;
vector
Exemple :vector<int>
vector
deque
Exemple :deque<int>
éléments stockés côte-à-côte ;
optimisé pour l'ajout en début et en fin de tableau ;
éléments indexés par des entiers.
deque
list
Exemple :list<int>
éléments stockés de manière « aléatoire » dans la mémoire ;
ne se parcourt qu'avec des itérateurs ;
optimisé pour l'insertion et la suppression au milieu.
list
map
Exemple :map<string,int>
map
set
Exemple :set<int>
éléments triés ;
ne se parcourt qu'avec des itérateurs.
Décrouvrez la puissance des algorithms
Un premier exemple
Std::generate
#include <algorithm>
int main()
{
list<int> tab; //Une liste d'entiers
Remplir f(0);
return 0;
}
Example:
#include <iostream>
#include <cstdlib> //pour rand()
#include <ctime> //pour time()
#include <vector>
#include <algorithm>
using namespace std;
class Generer
{
public:
int operator()() const
{
return rand() % 10; //On renvoie un nombre entre 0 et 9
}
};
int main()
{
srand(time(0));
cout << "Il y a " << compteur << " elements valant 5." << endl;
return 0;
}
Le retour des prédicats
Si vous pensiez que vous pourriez vous en sortir sans ces drôles de foncteurs, vous
vous trompiez ! Je vous avais dit au chapi
l s'appelle count_if(). La différence avec count()est que le troisième argument n'est pas
une valeur mais un prédicat.
#include <algorithm>
#include <string>
#include <vector>
using namespace std;
class TestVoyelles
{
public:
bool operator()(string const& chaine) const
{
for(int i(0); i<chaine.size(); ++i)
{
switch (chaine[i]) //On teste les lettres une à une
{
case 'a': //Si c'est une voyelle
case 'e':
case 'i':
case 'o':
case 'u':
case 'y':
return true; //On renvoie 'true'
default:
break; //Sinon, on continue
}
}
return false; //Si on arrive là, c'est qu'il n'y avait pas de voyelle du
tout
}
};
int main()
{
vector<string> tableau;
return 0;
}
Cherchez
Chercher un élément dans un tableau est aussi très facile.
On utilise l'algorithme find()oufind_if()
Triez !
class ComparaisonLongueur
{
public:
bool operator()(const string& a, const string& b)
{
return a.length() < b.length();
}
};
int main()
{
vector<string> tableau;
return 0;
}
Std::for_each()
class Sommer
{
public:
Sommer()
:m_somme(0)
{}
void operator()(int n)
{
m_somme += n;
}
private:
int m_somme;
};
int main()
{
srand(time(0));
vector<int> tab(100, -1);
generate(tab.begin(), tab.end(), Generer()); //On génère des nombres
aléatoires
Sommer somme;
cout << "La somme des elements generes est : " << somme.resultat() << endl;
return 0;
}
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
int main()
{
vector<double> a(50, 0.); //Trois tableaux de 50 nombres à virgule
vector<double> b(50, 0.);
vector<double> c(50, 0.);
//À partir d'ici les cases de 'c' contiennent la somme des cases de 'a' et 'b'
return 0;
}
Les itérateurs sur les flux ont donc la propriété de ne pouvoir qu'avancer: ils ne
possèdent que l'opérateur++ et pas le--
#include <iostream>
#include <iterator>
using namespace std;
int main()
{
ostream_iterator<double> it(cout);
*it = 3.14;
*it = 2.71;
return 0;
}
vous devriez obtenir ceci :
3.142.71
Le seul problème, c'est que nous n'avons pas inséré d'espace entre eux
SOLUTION: Delimiteur
On peut spécifier ce qu'on appelle un délimiteur, c'est-à-dire le ou les symboles qui
seront insérés entre chaque écriture faite via l'opérateur*
#include <iostream>
#include <iterator>
using namespace std;
int main()
{
ostream_iterator<double> it(cout, ", ");
*it = 3.14;
*it = 2.71;
return 0;
}
Ce qui donne :
3.14, 2.71,
Pour obtenir un retour à la ligne entre chaque écriture, il faut spécifier le délimiteur ''\n''.
#include <fstream>
#include <iterator>
#include <iostream>
using namespace std;
int main()
{
ifstream fichier("data.txt");
istream_iterator<double> it(fichier); //Un itérateur sur le fichier
istream_iterator<double> end; //Le signal de fin
#include <algorithm>
#include <vector>
#include <iterator>
#include <fstream>
using namespace std;
int main()
{
vector<int> tab(100,0);
ifstream fichier("C:/Nanoc/data.txt");
istream_iterator<int> it(fichier);
istream_iterator<int> fin;
copy(it, fin, tab.begin()); //On copie le contenu du fichier du debut à la
fin dans le vector
return 0;
}
Il faut absolument que votre vectorsoit assez grand pour contenir tous les nombres lus
On peut bien sûr utiliser copy()pour écrire dans un fichier ou dans la console
:
int main()
{
srand(time(0));
vector<int> tab(100,-1); //Un tableau de 100 cases
Le problème de la taille
Lorsqu'on lit des données dans un fichier pour les insérer dans un tableau, il y a un problème qui
survient assez souvent : celui de la taille à donner au tableau.
SOLUTION: BACK_INSERTER
#include <algorithm>
#include <vector>
#include <iterator>
#include <fstream>
using namespace std;
int main()
{
vector<int> tab; //Un tableau vide
ifstream fichier("C:/Nanoc/data.txt");
istream_iterator<int> it(fichier);
istream_iterator<int> fin;
back_insert_iterator<vector<int> > it2(tab);
//L'algorithme ajoute les cases nécessaires au tableau
copy(it, fin, it2);
return 0;
}
Voici par exemple les lignes permettant de trouver le minimum des valeurs dans un
fichier :
ifstream fichier("C:/Nanoc/data.txt");
#include <string>
#include <sstream>
#include <iostream>
using namespace std;
int main()
{
ostringstream flux; //Un flux permettant d'écrire dans une chaîne
flux << "Salut les"; //On écrit dans le flux grâce à l'opérateur <<
flux << " zeros";
flux << " !";
string const chaine = flux.str(); //On récupère la chaine
cout << chaine << endl; //Affiche 'Salut les zeros !'
return 0;
}
Allez plus loin avec la SL
Plus loin avec les strings
string chaine("Salut les zeros!"); //Une chaîne
#include <iostream>
#include <string>
#include <algorithm>
#include <cctype>
using namespace std;
class Convertir
{
public:
char operator()(char c) const
{
return toupper(c);
}
};
int main()
{
string chaine("Salut les zeros !");
transform(chaine.begin(), chaine.end(), chaine.begin(), Convertir());
cout << chaine << endl;
return 0;
}
OU PLUTOT
L'itérateur de fin
Faire du sorting sur tableau normal
#include <algorithm>
using namespace std;
int main()
{
int const taille(1000);
double tableau[taille]; //On déclare un tableau
//Remplissage du tableau…
double* debut(tableau); //Les deux itérateurs
double* fin(tableau+taille);
sort(debut, fin); //Et on trie
return 0;
}
int main()
{
complex<double> c(2,3);
cout << c << endl;
return 0;
}
Ce code produit le résultat suivant :
(2., 3.)
a = cos(c/b) + sin(b/c);
Bref, tout ce qui est nécessaire pour faire des maths un peu poussées. Enfin, il
existe des fonctions spécifiques aux nombres complexes comme la norme ou le
conjugué.
complex<double> a(3,4);
cout << norm(conj(a)) << endl; //Affiche '5'
Les valarray
Ils sont plus restrictifs que les vectordans le sens où l'on ne peut pas facilement ajouter
des cases à la fin mais, comme ce n'est pas une opération très courante, ce n'est pas un
problème.
La grande force des valarrayest la possibilité d'effectuer des opérations
mathématiques directement avec l'ensemble du tableau.
#include<valarray>
using namespace std;
int main()
{
valarray<int> a(10, 5); //5 éléments valant 10
valarray<int> b(8, 5); //5 éléments valant 8
valarray<int> c = a + b; //Chaque élément de c vaut 18
return 0;
}
#include<valarray>
#include<cmath>
using namespace std;
int main()
{
valarray<double> a(10); //10 éléments
//Remplissage du tableau…
a.apply(Cosinus);
return 0;
}
Le mieux à faire serait alors de lancer l'exception dans la fonction et de récupérer l'erreur, si elle
se produit, dans lemain. De cette manière, celui qui appelle la fonction a conscience qu'une
erreur s'est produit
int main()
{
int a,b;
cout << "Valeur pour a : ";
cin >> a;
cout << "Valeur pour b : ";
cin >> b;
try
{
cout << a << " / " << b << " = " << division(a,b) << endl;
}
catch(string const& chaine)
{
cerr << chaine << endl;
}
return 0;
}
#include <exception>
using namespace std;
private:
int m_numero; //Numéro de l'erreur
string m_phrase; //Description de l'erreur
int m_niveau; //Niveau de l'erreur
};
On pourrait alors réécrire la fonction de division de 2 entiers de la manière suivante :
int division(int a,int b) // Calcule a divisé par b.
{
if(b==0)
throw Erreur(1,"Division par zéro",2);
else
return a/b;
}
int main()
{
int a,b;
cout << "Valeur pour a : ";
cin >> a;
cout << "Valeur pour b : ";
cin >> b;
try
{
cout << a << " / " << b << " = " << division(a,b) << endl;
}
catch(std::exception const& e)
{
cerr << "ERREUR : " << e.what() << endl;
}
return 0;
}
Cela donne à l'exécution :
Valeur pour a : 3
Valeur pour b : 0
ERREUR : Division par zéro
int main()
{
try
{
vector<int> a(1000000000,1); //Un tableau bien trop grand
}
catch(exception const& e) //On rattrape les exceptions standard de tous types
{
cerr << "ERREUR : " << e.what() << endl; //On affiche la description de
l'erreur
}
return 0;
}
Cela donne le résultat suivant dans la console :
ERREUR : std::bad_alloc
Si l'on avait attrapé l'exception par valeur et pas par référence (c'est-à-dire sans le &), le
message aurait étéstd::exceptioncar le polymorphisme n'est pas conservé. C'est pour cela
que l'on attrape toujours les exceptions par référence. Il est fort quand même ce
polymorphisme !
C'est pour cela que lesvector(et lesdeque) proposent une méthode appeléeat()qui
fait exactement la même chose que les crochets mais qui lance une exception en
cas d'indice erroné.
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector<double> tab(5, 3.14); //Un tableau de 5 nombres à virgule
try
{
tab.at(8) = 4.; //On essaye de modifier la 8ème case
}
catch(exception const& e)
{
cerr << "ERREUR : " << e.what() << endl;
}
return 0;
}
Cela nous donne :
ERREUR : vector::_M_range_check
int main()
{
int a(5);
int b(5);
//reste du programme
return 0;
return 0;
}
Le résultat de ce petit programme est :
La spécialisation
La spécialisation emploie la syntaxe suivante :
template <>
string maximum<string>(const string& a, const string& b)
{
if(a.size()>b.size())
return a;
else
return b;
}
Vous remarquerez deux choses:
return 0;
}
qui donne :
1. la fonction générique ;
2. les fonctions spécialisées.