Syllabus 2023-2024
Syllabus 2023-2024
Syllabus 2023-2024
Algorithmique
2023-2024
Bachelor en Informatique
ALG2
2 Algorithmes de tris 19
2.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.2 Tri par insertion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.3 Tri par sélection des minima successifs . . . . . . . . . . . . . . . . . . . . . . . 22
2.4 Tri bulle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.5 Cas particuliers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.6 Références . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4 La liste 41
4.1 L’idée de liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.2 Comment implémenter une liste en mémoire ? . . . . . . . . . . . . . . . . . . . 42
4.3 Les ArrayList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
4.5 L’interface d’une liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.6 Implémenter une ArrayList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
4.7 Liste chaînées . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
4.8 Implémenter une linked list . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.9 Les listes : un exemple d’application . . . . . . . . . . . . . . . . . . . . . . . . 57
4.10 Les listes : exercices récapitulatifs . . . . . . . . . . . . . . . . . . . . . . . . . . 58
5 La pile 61
5.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
5.2 Implémentation orienté-objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.3 Exemple d’utilisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
5.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
6 La file 65
6.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
6.2 Implémentation orienté-objet . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
6.3 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
7 La récursivité 69
7.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
7.2 Définitions récursives . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
3
TABLE DES MATIÈRES
8 Mises en pratique 75
8.1 Se poser les bonnes questions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
8.2 Les structures de données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
8.3 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
Compléments 85
.1 Énumération . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
.2 Gestion des erreurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
Notre pseudo-code 87
.3 Exemple : un mot dans un miroir . . . . . . . . . . . . . . . . . . . . . . . . . . 88
.4 Exemple : Position des minimums. . . . . . . . . . . . . . . . . . . . . . . . . . 89
Aide mémoire 91
.5 Manipuler le hasard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
.6 La liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
.7 ArrayList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
.8 SinglyLinkedList . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
4
Introduction
Dans les cours de ALG1 et DEV1, vous avez appris les bases de la programmation et du
développement logiciel. L’une des difficultés auxquelles vous avez été confrontées était celle
de résoudre des problèmes, plus ou moins difficiles : jouer au Mastermind, afficher une suite
d’entiers déterminée, afficher un calendrier, etc.
Penser l’algorithme de résolution d’un problème et décrire cet algorithme, sont deux étapes
qui s’entremêlent : en réfléchissant au problème, on commence à écrire une solution, on change
d’avis, on ré-écrit, etc. Durant cette phase, il est nécessaire de pouvoir décrire sa pensée de
façon simple mais suffisamment précise. Ceci nous amène à écrire en langage naturel ou en
pseudo-code. Cela ressemble à du code, mais on ne se force pas à avoir une syntaxe parfaite.
Sur un document tel que celui-ci, l’utilisation de pseudo code se justifie pour :
. montrer à l’apprenant·e comment il ou elle peut écrire son pseudo-code, et
. gagner en place et en lisibilité pour des algorithmes plus complexes.
Voyez l’annexe .2 pour une explication du pseudo-code que nous utilisons.
5
Chapitre
Algorithmes d’insertions et de re-
1 cherches
Souvent lorsque les applications stockent des données, ces données changent. Il est nécessaire
d’en ajouter, de modifier certaines, d’en rechercher, d’en supprimer.
Exemple Imaginons qu’une école organise un événement libre et gratuit pour les étudiants et
les étudiantes. Mais pour y assister, il est nécessaire de s’inscrire. Nous voulons fournir ce qu’il
faut pour :
Nous allons, pour l’exemple, stocker les numéros d’étudiants et d’étudiantes dans un tableau 1 .
Comme le tableau ne sera généralement pas rempli, nous allons créer un tableau « trop grand »
et gérer sa taille logique. Nous avons deux variables :
Il y a pour le moment quatre personnes inscrites dont les numéros sont ceux repris dans les
quatre premières cases du tableau.
1.1.1 Inscription
Pour inscrire un étudiant ou une étudiante, il suffit de l’ajouter à la suite dans le tableau. Par
exemple, en partant de la situation décrite ci-dessus, l’inscription de l’étudiant numéro 42123
aboutit à la situation suivante :
registered = 42010 41390 43342 42424 42123 ? ? ? ...
nRegistered = 5
1. Ce n’est bien sûr pas la bonne manière de faire pour une application réelle pour laquelle nous utiliserions
probablement une base de données. . . mais c’est une autre histoire. . . et un autre cours.
1
1.1. INSERTION DANS UN TABLEAU NON TRIÉ
En langage Java, il n’est pas possible de passer nRegistered en entrée sortie puisque le passage
de paramètre se fait par valeur et que int est un type primitif. Il faudra que ce soit en valeur
de retour. Nous pourrions écrire :
public static int register(int[] registered, int nRegistered, int number){
// On peut imaginer vérifier que l'étudiant n'est pas déjà inscrit
// On peut imaginer vérifier qu'il reste de la place
// (c-à-d que le tableau n'est pas plein)
registered[nRegistered] = number;
return nRegistered + 1;
}
Dans ce cas, c’est bien le code appelant qui devra maintenir la valeur de la taille logique à
jour.
En Java :
public static int check(int[] registered, int nRegistered, int number){
int i = 0;
while (i < nRegistered && registered[i] != number){
i++;
}
if (i<nRegistered){
return i;
} else {
return -1;
}
}
1.1.3 Désinscription
Pour désinscrire un étudiant, il faut le trouver dans le tableau et l’enlever. Attention, il ne
peut pas y avoir de trou dans un tableau (il n’y a pas de case vide). Le plus simple est d’y
déplacer le dernier élément. Par exemple, reprenons la situation après inscription de l’étudiant
numéro 42123.
registered = 42010 41390 43342 42424 42123 ? ? ? ...
nRegistered = 5
2
1.2. GESTION DES TABLEAUX NON TRIÉS
L’algorithme est assez simple à écrire en utilisant la recherche écrite juste avant :
En Java :
public static int unsubscribe(int[] registered, int nRegistered, int number){
int pos = check(registered, nRegistered, number);
registered[pos] = registered[nRegistered - 1];
return nRegistered - 1;
}
1.1.4 Exercices
1. Éviter la double inscription.
Comment modifier l’algorithme d’inscription pour s’assurer qu’un étudiant ne s’inscrive
pas deux fois ?
2. Limiter le nombre d’inscriptions.
Comment modifier l’algorithme d’inscription pour refuser une inscription si le nombre
maximal de participant est atteint en supposant que ce maximum est égal à la taille
physique du tableau ?
3. Vérifier la désinscription.
Que se passerait-il avec l’algorithme de désinscription tel qu’il est si on demande à
désinscrire un étudiant non inscrit ? Que suggérez-vous comme changement ?
4. Optimiser la désinscription.
Dans l’algorithme de désinscription tel qu’il est, voyez-vous un cas où on effectue une
opération inutile ? Avez-vous une proposition pour l’éviter ?
3
1.2. GESTION DES TABLEAUX NON TRIÉS
1.2.1 Rechercher
Spécification
Retourner l’indice d’une donnée trouvée dans un tableau non trié ou -1 si elle n’est pas trouvée.
Nous supposons que le tableau n’est pas rempli, l’algorithme recevra donc une valeur repré-
sentant le nombre d’éléments du tableau. Cette valeur est inférieure à la taille physique du
tableau bien sûr.
Données : le tableau à analyser, le nombre d’éléments dans ce tableau (taille logique), la
valeur à rechercher
Résultat : la position de l’élément si il est dans le tableau et -1 sinon.
Solution
Il suffit de parcourir le tableau jusqu’à ce que l’on trouve la valeur ou bien que l’on arrive à la
fin de sa taille logique.
tant que l’on n’est pas à la fin et la val n’est pas trouvée
incrémenter l’indice
fin tant que
si on n’est pas à la fin alors
on a trouvé
sinon
on n’a pas trouvé et on retourne -1
fin si
langage naturel
1.2.2 Ajouter
Spécification
Ajouter une donnée non encore présente dans le tableau de données non triées.
Données : le tableau à modifier, le nombre d’éléments dans ce tableau, la valeur à ajouter
Résultat : le tableau reçu est modifié en lui ajoutant la valeur si elle n’y était pas déjà
4
1.2. GESTION DES TABLEAUX NON TRIÉS
Solution
Si l’algorithme reçoit le tableau, le nombre d’éléments et la valeur à ajouter, il suffit d’insérer
la valeur dans le tableau à la première place disponible. . . après avoir vérifié qu’elle n’est pas
présente. Cette vérification peut se faire simplement en utilisant l’algorithme indexValue. Si
celui-ci retourne -1, c’est que la valeur n’est pas présente.
Remarque En langage Java, le passage de paramètre se faisant par valeur (cfr. Chapitre
1 page 1), la taille logique du tableau doit être retournée et maintenue à jour par le code
appelant. En langage Java, cet algorithme aurait l’allure suivante :
public static int add(int[] is, int nValues, int value){
if (nValues < is.length && indexValue(is, nValues, value) == -1){
is[nValues] = value;
nValues = nValues + 1;
}
return nValues;
}
1.2.3 Supprimer
Spécification
Supprimer une donnée d’un tableau de données non triées.
Données : le tableau à modifier, le nombre d’éléments dans ce tableau, la valeur à supprimer
Résultat : le tableau reçu est modifié en lui supprimant la valeur
Solution
Comme le tableau n’est pas trié, pour supprimer une valeur, il suffit de trouver l’index de cette
valeur et de placer la dernière valeur à sa place.
langage naturel
5
1.2. GESTION DES TABLEAUX NON TRIÉS
is[index] = is[nValues];
nValues = nValues - 1;
}
return nValues;
}
6
1.3. INSERTION DANS UN TABLEAU TRIÉ
En Java :
/∗
∗ Recherche d'un étudiant ou d'une étudiante par son numéro d'id.
∗ return: la position où a été trouvé le numéro
∗ ou -1 si non présent
∗/
public static int find( int[] registered, int nRegistered, int number){
int pos = findPosition(registered, nRegistered, number);
if (registered[pos] == number){
return pos;
} else {
return -1;
}
}
/∗
∗ Recherche d'une position
∗ return: la position à laquelle est ou devrait être le numéro
∗/
public static int findPosition( int[] registered, int nRegistered, int number){
int pos = 0,
while (pos < nRegistered && registered[pos] < number){
pos++;
}
7
1.3. INSERTION DANS UN TABLEAU TRIÉ
return pos;
}
Comprenez-vous pourquoi :
. registered[pos] < number contient un ’<’ dans la condition du tant que et pas un ’6=’ ?
. la condition du if est insuffisante. Que se passe-t-il si la valeur recherchée est absente du
tableau et strictement supérieure à toutes celles figurant dans le tableau ?
Le if tel que présenté dans l’algorithme peut se réécrire grâce à l’opérateur ternaire de Java.
Profitons en pour le présenter.
ConditionalExpression:
Condition ? Expression : Expression
Si la condition est vraie, l’expression prend la première valeur et si la condition est fausse, ce
sera la seconde valeur. Par exemple : heure < 12 ? “matin” : “soir” vaut “soir” si heure vaut 15.
/∗∗
∗ Recherche une inscription dans le tableau.
∗
∗ @param registered le tableau d'inscrits
∗ @param nRegistered le nombre d'inscrits
∗ @param number le numéro à chercher
∗ @return la position dans le tableau, -1 si non présent
∗/
public static int find(int[] registered, int nRegistered, int number){
int pos = findPosition(registered, nRegistered, number);
return registered[pos] == number ? pos : -1;
}
/∗∗
∗ Recherche la position que le nombre soit présent ou pas.
∗ @param registered le tableau d'inscrits
∗ @param nRegistered le nombre d'inscrits
∗ @param number le numéro à chercher
∗ @return la position à laquelle est ou devrait être le numéro.
∗/
public static int findPosition{int[] registered, int nRegistered, int number){
int pos = 0;
while (pos < nRegistered && registered[pos] < number){
pos = pos + 1;
}
return pos;
}
8
1.3. INSERTION DANS UN TABLEAU TRIÉ
En Java :
/∗∗
∗ Inscrit un ou une étudiant.e.
∗
∗ @param registered le tableau d'inscrits
∗ @param nRegistered le nombre d'inscrits
∗ @param number le numéro à enregistrer
∗/
public static int subscribe(int[] registred, int nRegistered, int number){
int pos = findPosition(registered, nRegistered, number);
shiftRight(registred, pos, nRegistered-1);
registered[pos] = number;
return nRegistered + 1;
}
/∗∗
∗ Décale les éléments du tableau de begin à end un à droite.
∗ @param tab le tableau
∗ @param begin l'indice de départ
∗ @param end l'indice de fin
∗/
public static void shiftRight(int[] tab, int begin, int end){
for (int i = end; i >= begin; i = i - 1){
tab[i + 1] = tab[i];
}
}
1.3.3 Désinscription
Ici, il va falloir décaler vers la gauche les éléments qui suivent celui à supprimer.
En Java :
/∗∗
∗ Désinscrit un ou une étudiant.e.
∗
∗ @param registered le tableau d'inscrits
∗ @param nRegistered le nombre d'inscrits
∗ @param number le numéro à supprimer
∗/
public static int unsubscribe(int[] registred, int nRegistered, int number){
int pos = findPosition(registered, nRegistered, number);
shiftLeft(registred,pos+1, nRegistered);
return nRegistered - 1;
}
9
1.4. GESTION DES TABLEAUX TRIÉS
/∗∗
∗ Décale les éléments du tableau de begin à end un à gauche
∗ (et écrase donc l'élément en position begin-1).
∗ @param tab le tableau
∗ @param begin l'indice de départ
∗ @param end l'indice de fin
∗/
public static void shiftLeft(int[] tab, int begin, int end){
for (int i = begin; i <= end; i = i + 1){
tab[i - 1] = tab[i];
}
}
1.3.4 Exercices
1. Éviter la double inscription.
Comment modifier l’algorithme d’inscription pour s’assurer qu’un étudiant ne s’inscrive
pas deux fois ?
2. Limiter le nombre d’inscriptions.
Comment modifier l’algorithme d’inscription pour refuser une inscription si le nombre
maximal de participants est atteint en supposant que ce maximum est égal à la taille
physique du tableau ?
3. Vérifier la désinscription.
Que se passerait-il avec l’algorithme de désinscription tel qu’il est si on demande à
désinscrire un étudiant non inscrit ? Que suggérez-vous comme changement ?
1.4.1 Rechercher
Spécification
Rechercher la position où a été trouvé l’élément ou la position où il aurait dû être.
Données : le tableau à analyser, le nombre d’éléments dans ce tableau, la valeur à rechercher
Résultat : la position où a été trouvée la valeur ou la position où elle aurait dû être
Solution
L’algorithme retourne la position où a été trouvée la valeur ou celle où elle devrait être. En
parcourant le tableau, il suffit de s’arrêter dès que l’élément du tableau est plus grand ou égal
à la valeur recherchée.
10
1.4. GESTION DES TABLEAUX TRIÉS
}
1.4.2 Ajouter
Spécification
Ajouter une donnée non encore présente dans le tableau de données triées.
Données : le tableau à modifier, le nombre d’éléments dans ce tableau, la valeur à ajouter
Résultat : le tableau reçu est modifié en lui ajoutant la valeur si elle n’y était pas déjà
Solution
Pour faire un ajout sans doublon dans un tableau trié, il faut :
/∗∗
∗ Ajoute un nombre s'il n'est pas présent.
∗
∗ @param is le tableau d'éléments
∗ @param nValues le nombre d'éléments du tableau (taille logique)
∗ @param value la valeur à insérer
∗/
public static int add(int[] is, int nValues, int value){
int index;
if (verify(is, nValues, value) == -1){
index = findIndex(is, nValues, value);
shiftRight(is, index, nValues);
is[index] = value;
nValues++;
}
return nValues;
}
/∗∗
∗ Vérifie si la valeur est dans le tableau.
11
1.4. GESTION DES TABLEAUX TRIÉS
∗
∗ @param is le tableau d'éléments
∗ @param nValues le nombre d'éléments du tableau (taille logique)
∗ @param value la valeur à insérer
∗/
public static int verify(int[] is, int nValues, int value){
int index = findIndex(is, nValues, value);
if (is[index] == value) {
return index;
} else {
return -1;
}
}
/∗∗
∗ Décale d'une position vers la droite les éléments
∗ entre position début et fin.
∗
∗ @param is le tableau d'éléments
∗ @param begin la position de début
∗ @pasam end la position de fin
∗/
public static void shiftRight(int[] is, int begin, int end){
for (int i = end; i >= begin; i--){
is[i+1] = is[i];
}
}
1.4.3 Supprimer
Spécification
Supprimer une donnée d’un tableau de données triées.
Données : le tableau à modifier, le nombre d’éléments dans ce tableau, la valeur à supprimer
Résultat : le tableau reçu est modifié en lui supprimant la valeur
Solution
Pour supprimer un élément, dès lors qu’il est trouvé, il suffit de décaler tous les éléments à sa
droite vers la gauche et adapter la taille logique.
public static int delete(int[] is, int nValues, int value){
int index;
if (verify(is, nValues, value) != -1){
index = findIndex(is, nValues, value);
shiftLeft(is, index+1, nValues);
nValues = nValues - 1;
}
return nValues;
}
12
1.5. LA RECHERCHE DICHOTOMIQUE
2. Cet élément médian n’est pas tout à fait au milieu dans le cas d’une zone contenant un nombre pair
d’éléments.
13
1.6. INTRODUCTION À LA COMPLEXITÉ
Troisième étape : medianIndex = (10 + 13) DIV 2, c’est-à-dire 11 où se trouve l’élément 20. La
zone de recherche devient leftIndex vaut 12 et rightIndex vaut 13.
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
1 3 5 7 7 9 9 10 10 15 16 20 23 23 25 28 28 28 29 29
1 3 5 7 7 9 9 10 10 15 16 20 23 23 25 28 28 28 29 29
1 3 5 7 7 9 9 10 10 15 16 20 23 23 25 28 28 28 29 29
1 3 5 7 7 9 9 10 10 15 16 20 23 23 25 28 28 28 29 29
Arrivé à ce stade, la zone de recherche s’est réduite à un seul élément. Ce n’est pas celui
qu’on recherche mais c’est à cet endroit qu’il se serait trouvé ; c’est donc là qu’on pourra
éventuellement l’insérer. Le paramètre de sortie prend la valeur 5.
Algorithme
14
1.6. INTRODUCTION À LA COMPLEXITÉ
lisible
quand on lit l’algorithme, on doit pouvoir se convaincre qu’il est correct ;
optimisé en temps
il doit éviter les instructions inutiles (utilisation du processeur) ;
optimisé en espace
il doit utiliser le moins de mémoire possible (allocations mémoire).
Pour comparer des algorithmes en terme d’utilisation de temps et d’espace, un outil incon-
tournable est la complexité. Cette notion permet de quantifier l’utilisation processeur et/ou
mémoire d’un algorithme donné, aux fins de comparaison avec d’autres algorithmes.
Instructions élémentaires
Dans le cadre de ce cours, nous nous intéressons exclusivement à la complexité temporelle. Une
des façons les plus simples pour quantifier la complexité temporelle est de compter le nombre
d’instructions élémentaires utilisées par un algorithme.
Par exemple si nous reprenons l’algorithme suivant
15
1.6. INTRODUCTION À LA COMPLEXITÉ
Grand O
Pour s’affranchir d’un calcul précis du nombre d’instructions, nous allons nous intéresser à la
complexité en « grand O ». Dans ce qui suit, n représente un entier qui estime la taille des
données d’un algorithme. Il s’agit souvent de la taille d’un tableau donné en paramètre, ou
d’un entier donné en paramètre. La signification de la notation « grand O » est la suivante :
. On dit qu’un algorithme (qui dépend a priori d’un entier n) a une complexité en O(1) si le
nombre d’instructions exécutées ne dépend pas de n. On dit qu’il est en temps constant.
. On dit qu’un algorithme (qui dépend a priori d’un entier n) a une complexité en O(n) si
le nombre d’instructions exécutées est toujours inférieur à une quantité proportionnelle
à n. (Notons qu’un algorithme en O(1) est également en O(n).)
. Similairement un algorithme aura une complexité en O(n2 ) si le nombre d’instructions
exécutées est toujours inférieur à une quantité proportionnelle à n2 .
Appel de méthode Un appel à une méthode compte pour une instruction, dès lors seule
la complexité de la méthode elle-même rentre en ligne de compte 5 .
if A3
A1
else
A2
16
1.6. INTRODUCTION À LA COMPLEXITÉ
Boucle L’algorithme
while A3
A1
Comparons les complexités les plus courantes Pour la recherche dichotomique, la com-
plexité est logarithmique, et on le note O(log2 (n)) ce qui veut dire que si on double la taille
du problème, on a juste une itération en plus dans la boucle.
n 10 100 1000 104 105 106 109
O(1) 1 1 1 1 1 1 1
O(log2 (n)) 4 7 10 14 17 20 30
O(n) 10 100 1000 104 105 106 109
O(n2 ) 100 104 106 108 1010 1012 1018
O(n3 ) 1000 106 109 1012 1015 1018 1027
O(2n ) 1024 1030 10301 103010 1030102 10301029 10301029995
On voit ainsi que si on trouve un algorithme correct pour résoudre un problème mais que cet
algorithme est de complexité exponentielle alors cet algorithme ne sert à rien en pratique. Par
exemple un algorithme de recherche de complexité exponentielle, exécuté sur une machine pou-
vant effectuer un milliard de comparaisons par secondes, prendrait plus de dix mille milliards
d’années pour trouver une valeur dans un tableau de 100 éléments.
1.6.1 Exercices
Exercice 3 Réflexion
L’algorithme de recherche dichotomique est-il toujours à préférer à l’algorithme de recherche
linéaire ?
17
Chapitre
2 Algorithmes de tris
Dans ce chapitre nous voyons quelques algorithmes simples pour trier un ensemble d’informa-
tions : recherche des maxima, tri par insertion et tri bulle dans un tableau. Des algorithmes
plus efficaces seront vus en deuxième année.
2.1 Motivation
Dans le chapitre précédent, nous avons vu que la recherche d’information est beaucoup plus
rapide dans un tableau trié grâce à l’algorithme de recherche dichotomique. Nous avons vu
comment garder un ensemble trié en insérant toute nouvelle valeur au bon endroit.
Dans certaines situations, nous serons amené à devoir trier toute une collection de valeurs. Par
exemple un tableau.
1. D’abord les situations impliquant le classement total d’un ensemble de données « brutes »,
c’est-à-dire complètement désordonnées.
Prenons pour exemple les feuilles récoltées en vrac à l’issue d’un examen ; il y a peu de
chances que celles-ci soient remises à l’examinateur de manière ordonnée ; celui-ci devra
donc procéder au tri de l’ensemble des copies, par exemple par ordre alphabétique des
noms des étudiants ou des étudiantes, ou par numéro de groupe, etc.
2. Enfin, les situations qui consistent à devoir retrier des données préalablement ordonnées
sur un autre critère.
Prenons l’exemple d’un paquet de copies d’examen déjà triées sur l’ordre alphabétique
des noms des étudiants et des étudiantes, et qu’il faut retrier cette fois-ci sur les numéros
de groupe. Il est clair qu’une méthode efficace veillera à conserver l’ordre alphabétique
déjà présent dans la première situation afin que les copies apparaissent dans cet ordre
dans chacun des groupes.
Le dernier cas illustre un classement sur une clé complexe (ou composée) impliquant la
comparaison de plusieurs champs d’une même structure : le premier classement se fait sur le
numéro de groupe, et à numéro de groupe égal, l’ordre se départage sur le nom de la personne.
Nous dirons de cet ensemble qu’il est classé en majeur sur le numéro de groupe et en mineur
sur le nom d’étudiant.
Notons que certains tris sont dits stables parce qu’en cas de tri sur une nouvelle clé, l’ordre de
la clé précédente est préservé pour des valeurs identiques de la nouvelle clé, ce qui évite de faire
des comparaisons sur les deux champs à la fois. Les méthodes nommées tri par insertion,
tri bulle et tri par recherche de minima successifs (que nous allons aborder dans ce
chapitre) sont stables.
Certains tris sont évidemment plus performants que d’autres. Le choix d’un tri particulier
dépend de la taille de l’ensemble à trier et de la manière dont il se présente, c’est-à-dire déjà
plus ou moins ordonné. La performance quant à elle se juge sur deux critères :
. le nombre de tests effectués (comparaisons de valeurs) et ;
. le nombre de transferts de valeurs réalisés.
19
2.1. MOTIVATION
Les algorithmes classiques de tri présentés dans ce chapitre le sont surtout à titre pédagogique.
Ils ont tous une « complexité en n2 » (on dit qu’ils sont « en O(n2 ) »), ce qui veut dire que si n
est le nombre d’éléments à trier, le nombre d’opérations élémentaires (tests et transferts de va-
leurs) est proportionnel à n2 . Ils conviennent donc pour des ensembles de taille « raisonnable »,
mais peuvent devenir extrêmement lents à l’exécution pour le tri de grands ensembles, comme
par exemple les données de l’annuaire téléphonique. Plusieurs solutions existent, comme la
méthode de tri Quicksort. Cet algorithme très efficace faisant appel à la récursivité et qui
sera étudié en bloc deux (Q3) a une complexité en O(n log(n)) en général et dans de rares cas
en O(n2 ).
Dans ce chapitre, les algorithmes traiteront du tri dans un tableau d’entiers à une dimen-
sion. Toute autre situation peut bien entendu se ramener à celle-ci moyennant la définition de
la relation d’ordre propre au type de données utilisé. Ce sera par exemple l’ordre alphabétique
pour les chaines de caractères, l’ordre chronologique pour des objets Date ou Moment (que nous
verrons plus tard), etc. De plus, le seul ordre envisagé sera l’ordre croissant des données. Plus
loin, nous envisagerons le tri d’autres structures de données.
Enfin, dans toutes les méthodes de tri abordées, nous supposerons la taille physique du tableau
à trier égale à sa taille logique, celle-ci n’étant pas modifiée par l’action de tri. Nous supposons
trier tout le tableau.
20
2.2. TRI PAR INSERTION
L’étape suivante consiste à insérer tab[1], qui vaut 12, dans ce sous-tableau de taille 2 :
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
12 20 18 17 15 14 15 16 18 17 12 14 16 18 15 15 19 11 11 13
algorithme insertionSort(myArray )
pour i de 1 à taille(myArray) -1
mémoriser myArray[i] dans value
décaler les éléments myArray[0]..myArray[i-1] qui sont plus grands que value,
en partant de myArray[i-1]
placer value dans le "trou" laissé par le décalage
langage naturel
1 /∗∗
2 ∗ Tri par insertion.
3 ∗
4 ∗ @param myArray tableau à trier
5 ∗/
6 public static void insertionSort(int[] myArray) {
7 for (int i = 1; i < myArray.length; i++) {
8 int value = myArray[i];
9 int pos = i - 1;
10 while (pos >= 0 && value < myArray[pos]) {
11 myArray[pos + 1] = myArray[pos];
12 pos = pos - 1;
13 }
14 myArray[pos + 1] = value;
15 }
16 }
21
2.3. TRI PAR SÉLECTION DES MINIMA SUCCESSIFS
Celui-ci devrait donc apparaitre en 1re position du tableau. Or cette position est occupée par
la valeur 20. Comment procéder dès lors pour ne perdre aucune valeur et sans faire appel à
un second tableau ? La solution est simple, il suffit d’échanger le contenu des deux éléments
d’indices 0 et 9 :
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
17 52 61 47 82 64 95 66 84 20 32 24 46 48 75 55 19 61 21 30
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
17 19 61 47 82 64 95 66 84 20 32 24 46 48 75 55 52 61 21 30
Le sous-tableau trié est maintenant formé des deux premiers éléments, et le sous-tableau non
trié par les 18 éléments suivants.
Et ainsi de suite, jusqu’à obtenir un sous-tableau trié comprenant tout le tableau. En pratique
l’arrêt se fait une étape avant car lorsque le sous-tableau non trié n’est plus que de taille 1, il
contient nécessairement le maximum de l’ensemble.
algorithme selectionSort(myArray )
pour i de 0 à taille(myArray) -2
rechercher la position du minimum de myArray[i] à la fin du tableau
échanger les valeurs myArray[i] et le minimum trouvé
langage naturel
22
2.4. TRI BULLE
1 /∗∗
2 ∗ Trie le tableau reçu en paramètre via un tri par sélection
3 ∗ des minima successifs
4 ∗
5 ∗ @param myArray le tableau à trier
6 ∗/
7 public static void selectionSort(int[] myArray) {
8 for (int i = 0; i < myArray.length - 1; i++) {
9 int minIndex = searchMinIndex(myArray, i, myArray.length - 1);
10 if (minIndex != i) {
11 swap(myArray, i, minIndex);
12 }
13 }
14 }
15
16 /∗∗
17 ∗ Retourne l'indice du minimum entre les indices début et fin
18 ∗ du tableau reçu.
19 ∗
20 ∗ En cas d'égalité, c'est l'indice le plus petit qui est retourné.
21 ∗
22 ∗ @param myArray le tableau d'entiers
23 ∗ @param begin l'indice de début
24 ∗ @param end l'indice de fin
25 ∗/
26 public static int searchMinIndex(int[] myArray, int begin, int end) {
27 int minIndex = begin;
28 for (int i = begin + 1; i <= end; i++) {
29 if (myArray[i] < myArray[minIndex]) {
30 minIndex = i;
31 }
32 }
33 return minIndex;
34 }
35
36 /∗∗
37 ∗ Échange deux valeurs dans un tableau
38 ∗
39 ∗ @param myArray un tableau
40 ∗ @param pos1 une position
41 ∗ @param pos2 une (autre) position
42 ∗/
43 private static void swap(int[] myArray, int pos1, int pos2) {
44 int value = myArray[pos1];
45 myArray[pos1] = myArray[pos2];
46 myArray[pos2] = value;
47 }
23
2.5. CAS PARTICULIERS
10 5 12 15 4 1 8 3 7 12 11 4 6 5
10 5 12 15 1 4 8 3 7 12 11 4 6 5
10 5 12 1 15 4 8 3 7 12 11 4 6 5
10 5 1 12 15 4 8 3 7 12 11 4 6 5
10 1 5 12 15 4 8 3 7 12 11 4 6 5
1 10 5 12 15 4 8 3 7 12 11 4 6 5
Ceci achève le premier parcours. On recommence à présent le même processus dans le sous-
tableau débutant au deuxième élément, ce qui donnera le contenu suivant :
1 3 10 5 12 15 4 8 4 7 12 11 5 6
algorithme bubbleSort(myArray )
pour i de de 0 à taille(myArray)-2
pour j de taille(myArray)-1 à i
si myArray[j] < myArray[j-1]
échanger les valeurs myArray[j] et myArray[j-1]
langage naturel
Algorithme.
1 /∗∗
2 ∗ Tri bulle d'un tableau d'entiers.
3 ∗
4 ∗ @param myArray
5 ∗/
6 public static void bubbleSort(int[] myArray) {
7 // La bulle part de la droite,
8 // en faisant remonter l'élément le plus petit,
9 // et éclate à gauche.
10 for (int burstPos = 0; burstPos < myArray.length - 1; burstPos++) {
11 for (int bubblePos = myArray.length - 1; bubblePos > burstPos; bubblePos--) {
12 if (myArray[bubblePos] < myArray[bubblePos - 1]) {
13 swap(myArray, bubblePos, bubblePos - 1);
14 }
15 }
16 }
17 }
18
19 /∗∗
20 ∗ Échange deux valeurs dans un tableau
21 ∗
22 ∗ @param myArray un tableau
23 ∗ @param pos1 une position
24 ∗ @param pos2 une (autre) position
25 ∗/
26 private static void swap(int[] myArray, int pos1, int pos2) {
27 int value = myArray[pos1];
28 myArray[pos1] = myArray[pos2];
29 myArray[pos2] = value;
30 }
24
2.6. RÉFÉRENCES
Une autre situation courante est la suivante : un ensemble de valeurs sont traitées (lues,
calculées. . .) et il ne faut garder que les k plus petites (ou plus grandes).
L’algorithme est plus simple à écrire si on utilise une position supplémentaire en fin de tableau.
Notons aussi qu’il faut tenir compte du cas où il y a moins de valeurs que le nombre de valeurs
voulues ; c’est pourquoi on ajoute une variable indiquant le nombre exact de valeurs dans le
tableau.
Exemple : on lit un ensemble de valeurs strictement positives (un 0 indique la fin de la lecture)
et on ne garde que les k plus petites valeurs.
public static int bests(int[] smallers){
Scanner keyboard = new Scanner(System.in);
int val;
nbValues = 0;
val = keyboard.nextInt();
while (val >= 0){
nbValues = insert(val, smallers, nbValues);
val = keyboard.nextInt();
}
}
2.6 Références
. http://interstices.info/jcms/c_6973/les-algorithmes-de-tri
Ce site trivisuels permet de visualiser les méthodes de tris en fonctionnement. Il présente
tous les tris vus en bloc 1 plus quelques autres comme le « tri rapide » (« quicksort »)
que vous verrez en bloc 2.
25
Chapitre
3.1 Définition
La dimension d’un tableau est le nombre d’indices qu’on utilise pour faire référence à un de
ses éléments.
Attention
Ne pas confondre la dimension avec la taille !
En dev1 , nous avons introduit les tableaux à une dimension. Un seul indice suffisait à localiser
un de ses éléments. Pour le dire autrement, chaque case possédait un numéro. De nombreuses
situations nécessitent cependant l’usage de tableaux à deux dimensions. Ils vous sont déjà fami-
liers par leur présence dans beaucoup de situations courantes : calendrier, grille horaire, grille
de mots croisés, sudoku, jeux se déroulant sur un quadrillage (damier, échiquier, scrabble. . .).
Dans ces situations, chaque case est désignée par deux numéros.
3.2 Notations
3.2.1 Déclarer
Pour déclarer un tableau à 2 dimensions, on écrira :
ou en Java :
TypeElément[][] nomTableau = new TypeElément[nbLignes][nbColonnes];
où nbLignes et nbColonnes sont des expressions entières quelconques.
Exemple :
int[5×10] tab
ou en Java :
int[][] tab = new int[5][10];
déclare un tableau de 5 lignes par 10 colonnes dont chaque case contient un entier.
27
3.2. NOTATIONS
3.2.2 Utiliser
Pour accéder à une case du tableau on donnera les deux indices entre crochets. On considère
que la première ligne et la première colonne portent le numéro (l’indice) 0.
Exemple :
et en Java :
System.out.println(tab[2][4]);
3.2.3 Visualiser
Notez que la vue sous forme de tableau avec des lignes et des colonnes est une vision humaine.
Il n’y a pas de lignes ni de colonnes en mémoire.
Exemple : Soit le tableau déclaré ainsi :
int[4×5] nombres
Dans cette représentation, le tableau nombres est d’abord décomposé à un premier niveau en
quatre éléments auxquels on accède par le premier indice. Ensuite, chaque élément de premier
niveau est décomposé en cinq éléments de deuxième niveau accessibles par le deuxième indice.
Convention
Pour être précis, on devrait juste parler de première dimension et de deuxième dimension
mais la notion de ligne et de colonne est un abus de langage qui simplifie le discours.
28
3.2. NOTATIONS
3.2.4 Exemples
Exemple 1 – Remplir les coins. Dans ce petit exemple, on a un tableau de chaines et on
donne des valeurs aux coins.
"NO" "NE"
"SO" "SE"
La version Java :
Syntaxe Java
On vous renvoie à votre cours de Java pour les précisions sur la syntaxe Java.
Notez juste ici :
. Comment est déclaré/créé le tableau.
. Le double [] pour accéder à un élément.
Exemple 2 – Gestion des stocks. Prenons l’exemple d’un stock de 10 produits pour
chaque jour de la semaine.
article0 article1 article2 ... article7 article8 article9
lundi cpt[0,0] cpt[0,1] cpt[0,2] ... cpt[0,7] cpt[0,8] cpt[0,9]
mardi cpt[1,0] cpt[1,1] cpt[1,2] ... cpt[1,7] cpt[1,8] cpt[1,9]
mercredi cpt[2,0] cpt[2,1] cpt[2,2] ... cpt[2,7] cpt[2,8] cpt[2,9]
jeudi cpt[3,0] cpt[3,1] cpt[3,2] ... cpt[3,7] cpt[3,8] cpt[3,9]
vendredi cpt[4,0] cpt[4,1] cpt[4,2] ... cpt[4,7] cpt[4,8] cpt[4,9]
samedi cpt[5,0] cpt[5,1] cpt[5,2] ... cpt[5,7] cpt[5,8] cpt[5,9]
dimanche cpt[6,0] cpt[6,1] cpt[6,2] ... cpt[6,7] cpt[6,8] cpt[6,9]
29
3.2. NOTATIONS
En Java :
3.2.5 Complexité
Regardons un peu la complexité avec les tableaux 2D.
Dans l’algorithme suivant
30
3.3. PARCOURS
3.2.6 Exercices
Ces exercices ne nécessitent pas encore de parcours du tableau (pas de boucle). Nous verrons
les algorithmes de parcours juste après.
Pour chaque exercice, donnez la complexité « grand O » de votre solution.
3.3 Parcours
Comme nous l’avons fait pour les tableaux à une dimension, envisageons le parcours des
tableaux à deux dimensions (n lignes et m colonnes). Nos algorithmes sont valables quel que
soit le type des éléments. Utilisons un type T 1 , pour désigner un type quelconque.
T[n × m] tab
1. Nos versions Java seront écrites avec des tableau d’entiers. Il est possible de les écrire avec un type
générique T mais ça requiert des notions avancées de Java.
31
3.3. PARCOURS
Commençons par des cas plus simples où on ne parcourt qu’une seule des dimensions puis
attaquons le cas général.
Une ligne. On peut vouloir ne parcourir qu’une seule ligne du tableau. Si on parcourt la
ligne l, on visite les cases (l, 0), (l, 1), . . ., (l, m − 1). L’indice de ligne est constant et c’est
l’indice de colonne qui varie.
l 1 2 3 4 5
langage naturel
Retenons
Pour parcourir une ligne, on utilise une boucle sur les colonnes.
Une colonne. Symétriquement, on pourrait considérer le parcours d’une colonne avec l’al-
gorithme suivant.
32
3.3. PARCOURS
1
2
3
Une seule boucle suffit comme le montre l’algorithme suivant.
La diagonale montante. Pour la diagonale montante 2 d’un tableau carré (n = m), repré-
sentée ci-dessous, on peut envisager deux solutions :
. avec deux indices, ou
. avec un seul indice, en se basant sur le fait que
lg + col = n − 1 ⇒ lg = n − 1 − col.
3
2
1
2. On l’appelle montante car elle semble monter, si on la parcourt de gauche à droite. On l’appellera encore
montante même si on la parcourt de droite à gauche.
33
3.3. PARCOURS
34
3.3. PARCOURS
Parcours par lignes et par colonnes. Les deux parcours les plus courants sont les parcours
« ligne par ligne » et « colonne par colonne ». Les tableaux suivants montrent dans quel ordre
chaque case est visitée dans ces deux parcours.
Parcours ligne par ligne Parcours colonne par colonne
1 2 3 4 5 1 4 7 10 13
6 7 8 9 10 2 5 8 11 14
11 12 13 14 15 3 6 9 12 15
Le plus simple est d’utiliser deux boucles imbriquées
Il suffit d’inverser les deux boucles pour effectuer un parcours par colonne.
Mais on peut obtenir le même résultat avec une seule boucle si l’indice sert juste à compter le
nombre de passages (n ∗ m) et que les indices de lignes et de colonnes sont gérés manuellement.
L’algorithme suivant montre ce que ça donne pour un parcours ligne par ligne. La solution
pour un parcours colonne par colonne est similaire et laissée en exercice.
35
3.3. PARCOURS
En plus détaillé :
L’avantage de cette solution apparaitra quand on verra des situations plus difficiles.
36
3.3. PARCOURS
Version avec 1 boucle. Illustrons-le au travers d’un algorithme qui cherche un élément
particulier et retourne un booléen indiquant si l’élément a été trouvé ou pas.
37
3.3. PARCOURS
Pour retenir le sens du déplacement, nous allons utiliser un entier (−1 pour gauche et 1 pour
droite) ce qui va faciliter le calcul du déplacement.
38
3.4. EXERCICES
3.4 Exercices
Pour chaque exercice, donnez la complexité « grand O » de votre solution.
Exercice 6 Affichage
Écrire un algorithme qui affiche tous les éléments d’un tableau (à n lignes et m colonnes) ligne
par ligne.
Écrivez un autre algorithme qui affiche cette fois les éléments colonne par colonne
39
3.4. EXERCICES
Notes
. Le zèbre doit toujours présenter des lignes obliques et parallèles, quelle que soit la taille.
. La spirale est un véritable défi et vous est donné comme exercice facultatif. Ne le faites
pas si vous êtes en retard.
40
Chapitre
4 La liste
Dans ce chapitre, nous allons étudier une nouvelle structure de donnée plus souple que le ta-
bleau : la liste. Nous verrons comment il est possible d’implémenter une telle liste en mémoire
en orienté objet, ce qui vous permettra de mettre en pratique une des notions fondamen-
tales vues au cours de Java : l’encapsulation (les autres piliers de l’orienté objet, héritage et
polymorphisme, ne seront pas vus dans ce cours d’algorithmique).
1. "fromage"
2. "pain"
3. "salami"
4. "eau"
1. "fromage"
2. "salami"
3. "eau"
On pourrait aussi insérer un élément dans la liste, par exemple une baguette, ce qui décale, de
facto, la position des suivants.
1. "fromage"
2. "salami"
41
4.2. COMMENT IMPLÉMENTER UNE LISTE EN MÉMOIRE ?
3. "baguette"
4. "eau"
List<String> liste = new List<String>() // bien entendu, ce constructeur n’existe pas en java.
liste.add("fromage")
liste.add("pain")
liste.add("salami")
liste.add("eau")
liste.remove("pain")
liste.add(2, "pain français")
liste.set(2, "baguette")
liste.clear() // Efface la liste
Dans cet exemple nous avons créé une liste de chaînes de caractères (String). De façon générale
on pourra imaginer une liste d’entiers, de booléens, ou d’autres choses encore. Voici par exemple
un morceau de code manipulant une liste d’entiers :
List<int> liste
liste = new List<int>()
liste.add(42)
liste.add(54)
liste.set(1,44)
liste.add(1,43)
liste.removePos(2)
liste.remove(42)
liste.clear()
Après sa création, la liste est vide. Ensuite, elle passe par les états suivants :
0. 42 0. 42 0. 42 0. 42 0. 42 0. 43
1. 54 1. 44 1. 43 1. 43
2. 44
Enfin, le dernier appel la vide complètement
1. au moyen d’un tableau géré dynamiquement par la classe. Le tableau est encapsulé par
la classe et remplacé par un tableau plus grand lorsque cela devient nécessaire (une
ArrayList),
2. en chaînant les éléments les uns à la suite des autres en mémoire (une LinkedList).
42
4.3. LES ARRAYLIST
Nous verrons également que choisir une de ces implémentations (ArrayList ou LinkedList)
impacte les performances : la complexité des algorithmes implémentés par les méthodes add
et get par exemples varie d’une classe à l’autre.
Le principe d’une ArrayList est alors d’implémenter une liste au moyen d’un tableau encapsulé
dans la classe, la taille logique du tableau étant géré par les méthodes de la classe.
Avant de voir l’implémentation d’une ArrayList, voyons la façon dont les choses se passent en
Java. Une classe ArrayList est déjà implémentée pour vous dans le package java.util , voici
un exemple de son utilisation :
43
4.4. EXERCICES
1 package dev2.algo.liste;
2
3 import java.util.ArrayList;
4
5 public class ArrayListesEnJava{
6
7 public static void main(String[] args) {
8 /∗ On crée une nouvelle ArrayList vide. On précise le type
9 de donnée à stocker entre les < > ∗/
10 ArrayList<String> liste = new ArrayList<>();
11
12 // Illustration des méthodes add, set et remove
13 liste.add("fromage");
14 liste.add("pain");
15 liste.add("salami");
16 liste.add("eau");
17
18 liste.remove("pain");
19
20 /∗ Je peux préciser à la méthode add l'endroit dans la liste
21 ou effectuer l'insertion. ∗/
22 liste.add(2, "pain français");
23
24 liste.set(2, "baguette");
25
26 // Récupère et affiche l'élément en position 1
27 System.out.println(liste.get(1));
28
29 // La méthode contains() permet de tester l'existence d'un élément
30 if(liste.contains("salami")){
31 System.out.println("La liste contient du salami !");
32 }
33
34 // La méthode size() renvoie le nombre d'éléments courant:
35 System.out.println("La liste contient " + liste.size() + " éléments.");
36
37 // Vide la liste
38 liste.clear();
39
40 }
41 }
Ce code illustre quelques méthodes de la classe ArrayList. Essayez de deviner d’autres mé-
thodes utiles de cette classe. Pour les découvrir, nous vous invitons à consulter la documenta-
tion en ligne : https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/
util/List.html
4.4 Exercices
Le but de ces exercices est simplement de vous familiariser avec la notion de liste. Comme
d’habitude, les algorithmes peuvent être écris en Java ou en pseudo-code (vous supposerez
dans ce dernier cas l’existence d’une classe ArrayList, classe dont l’implémentation sera donnée
dans la suite du chapitre).
44
4.5. L’INTERFACE D’UNE LISTE
Remarque : Nous ne précisons pas encore la façon dont ces méthodes sont implémentées,
vu que cela va dépendre de la représentation mémoire choisie pour notre liste (ArrayList ou
LinkedList). Nous donnerons ces implémentations dans les sections qui suivent. Les méthodes
fournies ci-dessus ne constituent donc qu’une interface (un contrat où il y a que les signatures
des méthodes).
Quelques précisions s’imposent :
45
4.6. IMPLÉMENTER UNE ARRAYLIST
Les attributs La classe a donc deux attributs privés : un tableau et sa taille logique.
class ArrayList
private :
String[] tab // Le tableau contenant les données.
int size // La taille logique du tableau.
Ces attributs sont accessibles par le constructeur et toutes les méthodes de la classe.
constructor ArrayList()
this(20) // Appel à l’autre constructeur, comme en Java.
Insertion La classe offrira notamment deux méthodes 4 pour insérer des valeurs :
1. Par exemple : trouver toutes les chaînes d’une liste de chaînes dont la dernière lettre est une voyelle.
2. Voir le mécanisme des génériques (generics), en Java
3. Pour rappel on ne s’intéresse ici qu’à la complexité temporelle. La complexité spatiale est ici évidem-
ment égale à la taille maximale demandée. Notons également que certains langages, et le Java est de ceux-là,
parcourent implicitement le tableau pour l’initialiser. La complexité temporelle devient alors O(n) dans ces
langages.
4. Les deux méthodes ont le même nom, mais pas la même signature, ce qui permet de les différencier.
Beaucoup de langages, y compris le Java, permettent cela.
46
4.6. IMPLÉMENTER UNE ARRAYLIST
public :
void add(String value) // ajoute un élément en fin de liste
void add(int pos, String value) // insère un élément en position pos
Notons que chacune de ces méthodes est responsable de modifier la taille logique. Il n’y a donc
plus d’utilité de retourner cette taille logique.
Une implémentation de la première méthode add serait :
La complexité est O(1). Notez que les deux dernières lignes pourraient se réduire à :
tab[size++] = value // On gardera plutôt les deux lignes par souci de lisibilité
Complexité amortie
Dans cette implémentation, si la taille maximale est atteinte, une exception est levée.
Une autre façon de gérer la limite est d’augmenter la taille physique : créer un second
tableau, plus grand (typiquement de taille double), et recopier les éléments du premier
vers le second.
Avec cette technique, la complexité de l’ajout n’est plus constante, mais linéaire (c’est-à-
dire O(n)). En effet, dans le pire des cas il faut recopier tout le tableau, ce qui implique
de le parcourir complètement. Cependant cet effet n’apparaît « pas souvent » ; à tel
point qu’en moyenne a , on peut parler de complexité amortie en temps constant.
a. cormen_2009.
Pour la seconde méthode, notons d’abord une conséquence de la simple existence de cette
méthode. En effet, cette méthode permettant d’insérer une valeur à une position précise donnée,
ceci implique que la position des valeurs a de l’importance. Dès lors, insérer une valeur à une
position doit décaler les autres valeurs, afin que l’ordre relatif des valeurs soit préservé. Muni
de cette information nous savons maintenant implémenter la méthode :
Dans la mesure où il faut potentiellement parcourir tout le tableau, cette méthode est en O(n).
Nous aborderons plus tard l’implémentation pour les méthodes de suppression (remove, remo-
vePos).
Taille (size) Comme c’est la classe qui est responsable de tenir à jour sa taille logique, il
doit y avoir une méthode pour récupérer la taille (logique) courante :
public :
int size() // donne le nombre actuel d’éléments
47
4.6. IMPLÉMENTER UNE ARRAYLIST
Accéder à un élément avec son indice (get, set) Dans un tableau, la syntaxe tab[2]
permet d’accéder au troisième élément (élément d’indice 2), mais cette syntaxe est réservée
aux tableaux.
Pour nos objets ArrayList, nous définissons deux méthodes :
public :
String get(int pos) // donne un élément en position pos
void set(int pos, String value) // modifie un élément en position pos
Trouver un élément Pour retrouver un élément, nous allons définir deux méthodes.
public :
int indexOf(String value) // donne la position d’un élément
boolean contains(String value) // indique si un élément est présent
(Attention, en Java un test d’égalité ou d’inégalité entre chaînes se réalise avec la méthode
equals .) Cette méthode parcourt la liste, et est donc en complexité O(n) (où n représente la
longueur de la liste).
La méthode contains effectue également une recherche mais retourne seulement un booléen :
48
4.6. IMPLÉMENTER UNE ARRAYLIST
Notons que cette méthode a également une complexité O(n), puisqu’elle appelle indexOf.
public :
void removePos(int pos) // supprime l’élément en position pos – O(1)
boolean remove(String value) // supprime l’élément de valeur donnée – O(n)
La méthode de suppression par valeur peut alors s’écrire simplement en combinant la recherche
de la valeur et la suppression par position :
Vider la liste Nous définissons également une méthode permettant de vider complètement
la liste.
public :
void clear() // vide la liste
La liste est-elle vide ? La dernière méthode que nous implémentons permet de déterminer
si notre objet ArrayList est vide :
public :
boolean isEmpty() // la liste est-elle vide ?
49
4.7. LISTE CHAÎNÉES
4.6.1 Exercices
Quelle est la complexité de cette version, et comment comparer les deux versions ?
Pour cette raison, "une liste" sera un objet (de type SinglyLinkedList) contenant simplement
une référence vers un Node.
Remarquez qu’il serait aussi possible de chaîner la liste dans les deux sens, comme illustré
ci-dessous :
Ce type de liste chaînée est appelée liste chaînée double. Lorsqu’on mentionne une liste chaînée,
il est donc important de savoir de quoi on parle (une liste simplement chaînée ? une liste
double ? 5 ).
L’implémentation que nous allons donner dans les sections qui suivent correspond à une liste
chaînée simple, ce qui explique le nom de la classe : "SinglyLinkedList".
Et en Java ? Avons-nous une classe LinkedList à notre disposition (de même que la classe
ArrayList) ? Oui, mais attention : la classe LinkedList de Java implémente en réalité une liste
chaînée double. Elle se manipule comme on s’y attend :
5. Et il existe encore d’autres variantes/généralisations, nous n’en parlerons pas dans ce cours
50
4.8. IMPLÉMENTER UNE LINKED LIST
1 package dev2.algo.liste;
2
3 import java.util.linkedList;
4
5 public class LinkedListesEnJava{
6
7 public static void main(String[] args) {
8 /∗ LinkedList est en réalité une liste double ∗/
9 LinkedList<String> liste = new LinkedList<>();
10
11 // Illustration des méthodes add, set et remove
12 liste.add("fromage");
13 liste.add("pain");
14 liste.add("salami");
15 liste.add("eau");
16
17 liste.remove("pain");
18
19 /∗ Je peux préciser à la méthode add l'endroit dans la liste
20 ou effectuer l'insertion. ∗/
21 liste.add(2, "pain français");
22
23 liste.set(2, "baguette");
24
25 // Récupère et affiche l'élément en position 1
26 System.out.println(liste.get(1));
27
28 // La méthode contains() permet de tester l'existence d'un élément
29 if(liste.contains("salami")){
30 System.out.println("La liste contient du salami !");
31 }
32
33 // La méthode size() renvoie le nombre d'éléments courant:
34 System.out.println("La liste contient " + liste.size() + " éléments.");
35
36 // Vide la liste
37 liste.clear();
38
39 }
40 }
Comme vous pouvez le constater, elle se manipule de la même façon qu’une ArrayList (forcé-
ment vu que ces deux classes implémentent la même interface). Nous verrons cependant que
les performances des différentes méthodes peuvent fortement varier.
class SinglyLinkedList
private :
Node head // Référence à la tête de liste.
class Node
private :
T value // La valeur du noeud
Node next // Référence à l’élément suivant. null sinon
Cohérence interne : il faut qu’il n’y ait pas de cycles (c’est-à-dire un noeud qui référence un
nœud qui référence un nœud . . . ultimement référence un noeud précédent). En particulier la
chaîne devra forcément se terminer par null .
Voyons comment implémenter certaines méthodes de notre interface selon ce principe.
Constructeur Le rôle du constructeur est d’initialiser les attributs de la classe. Pour rappel
le constructeur de nos listes crée une liste vide. Il n’y a donc aucune valeur, et donc aucun
51
4.8. IMPLÉMENTER UNE LINKED LIST
Node.
constructor SinglyLinkedList()
head = null
public :
void add(String value) // ajoute un élément en fin de liste
void add(int pos, String value) // insère un élément en position pos
Pour la première méthode, il faut trouver le dernier Node, et lui ajouter un successeur. Le cas
particulier d’une liste vide doit être traité à part.
Dans le pire des cas, il faut parcourir (avec findElemAtPos ) toute la liste pour trouver le point
d’insertion, mais l’insertion proprement dite est en temps constant. La complexité est donc
O(n) 6 .
6. Si on considère la position spécifique comme variable, on peut dire que la complexité est O(pos). Il faut
52
4.8. IMPLÉMENTER UNE LINKED LIST
Accéder à un élément avec son indice (get, set) avec findElemAtPos nous avons fait
l’essentiel du travail pour implémenter get et set .
La taille Contrairement au cas de ArrayList , la taille n’est pas un attribut de notre classe 7 .
Pour compter le nombre d’éléments, il faut donc parcourir la liste.
isEmpty Pour déterminer si une liste est vide, il est naturel de se demander si la taille vaut
0. Cependant, notre méthode size a une complexité linéaire. Une implémentation en temps
constant est simplement :
53
4.8. IMPLÉMENTER UNE LINKED LIST
Parcourir avec un Iterator Cependant la classe Node est privée. Dès lors tout algorithme
voulant parcourir une liste chaînée (faire une somme, trouver un maximum, etc.) devrait être
implémenté comme une méthode, afin d’accéder à ces objets Node, ce qui n’a aucun sens. La
solution standard à ce problème est de retourner un Iterator : c’est un objet permettant de
parcourir (efficacement, donc en O(n)) n’importe quelle liste.
54
4.8. IMPLÉMENTER UNE LINKED LIST
interface Iterator<T>
public :
boolean hasNext() // doit être en O(1)
T next() // doit être en O(1)
D’une part la performance réelle dépendra de l’utilisation qui est faite de la structure. À titre
d’exemple, une insertion dans une SinglyLinkedList en connaissant le Node qui précède le point
55
4.8. IMPLÉMENTER UNE LINKED LIST
d’insertion se fait en temps constant. C’est notamment le cas pour une insertion en début de
liste, qui est toujours en temps constant alors que c’est en temps linéaire pour une ArrayList.
D’autre part, le concept de liste chaînée peut être modifié selon le besoin :
. ajout d’un attribut size pour garder la taille en mémoire ;
. ajout d’un attribut tail pour garder le dernier Node de la liste en mémoire ;
. modifier la classe Node pour obtenir une version doublement chaînée, parcourable dans
les deux sens.
En Java, ces choix ont été implémentés dans la classe LinkedList .
4.8.3 Exercices
Pour chaque exercice, donnez la complexité de l’algorithme obtenu.
Exercice 6 indexOf
Écrivez la méthode de la classe SinglyLinkedList
qui retourne la position de la valeur donnée au sein de la liste chaînée. La valeur -1 est retournée
si la valeur ne s’y trouve pas.
Exercice 7 contains
Écrivez la méthode de la classe SinglyLinkedList
56
4.9. LES LISTES : UN EXEMPLE D’APPLICATION
Exercice 8 removePos
Écrivez la méthode de la classe SinglyLinkedList
Exercice 9 remove
On veut écrire la méthode de la classe SinglyLinkedList
qui supprime la première occurrence de la valeur donnée si elle existe. La méthode renvoie true
si une suppression a été effectuée, false sinon.
En Java ça donne :
57
4.10. LES LISTES : EXERCICES RÉCAPITULATIFS
Exercice 10 Anniversaires
(cet exercice requiert des notions d’Orienté Objet.)
Écrire un algorithme qui reçoit une liste d’objets Personne 9 et retourne une nouvelle liste de
ceux qui sont nés durant un mois passé en paramètre (donné sous la forme d’un entier entre 1
et 12).
Exercice 12 Le nettoyage
Écrire un algorithme qui reçoit une liste de chaines en paramètre et supprime de cette liste tous
les éléments de valeur donnée en paramètre. L’algorithme retournera le nombre de suppressions
effectuées.
a) Faites l’exercice en créant une nouvelle liste (la liste de départ reste inchangée)
9. comportant des attributs nom et prénom (des chaînes de caractères) et un attribut date, de type Date,
ayant lui-même des attributs jour, mois et année (des entiers).
58
4.10. LES LISTES : EXERCICES RÉCAPITULATIFS
Exercice 16 Rendez-vous
(cet exercice requiert des notions d’Orienté Objet.)
Soit la classe RendezVous ayant comme attribut une date (type Date) et un motif de rencontre
(type String).
Écrire un algorithme qui reçoit une liste de rendez-vous et la met à jour en supprimant tous
ceux qui sont désormais passés.
Pour résoudre cet exercice, vous pouvez utiliser sans l’écrire un algorithme aujourdhui() qui
retourne la date du jour.
59
Chapitre
5 La pile
5.1 Définition
Une pile est une collection d’éléments admettant les fonctionnalités suivantes :
La pile (en anglais stack) est donc une collection de données de type dernier entré, premier
sorti (en anglais on dit « LIFO », c’est-à-dire last in first out). L’analogie avec la pile de
dossiers sur un bureau est claire : les dossiers sont déposés et retirés du sommet et on ne peut
jamais ajouter, retirer ou consulter un dossier qui se trouve ailleurs dans la pile.
On ne peut donc pas parcourir une pile, ou consulter directement le n-ième élément. Les
opérations permises avec les piles sont donc peu nombreuses, mais c’est précisément là leur
spécificité : elles ne sont utilisées en informatique que dans des situations particulières où
seules ces opérations sont requises et utilisées. Paradoxalement, on implémentera une pile en
restreignant des structures plus riches aux seules opérations autorisées par les piles. Cette
restriction permet de n’utiliser que les fonctionnalités nécessaires de la pile, simplifiant ainsi
son utilisation.
61
5.2. IMPLÉMENTATION ORIENTÉ-OBJET
Des exemples d’utilisations sont la gestion de la mémoire par les micro-processeurs, l’évaluation
des expressions mathématiques en notation polonaise inverse, la fonction « ctrl-Z » dans un
traitement de texte qui permet d’annuler les frappes précédentes, la mémorisation des pages
web visitées par un navigateur, etc. Nous les utiliserons aussi en ALG3 pour parcourir les
arbres et les graphes.
5.2.2 Remarques :
. Théoriquement, et dans la majorité des utilisations, la pile est infinie, c’est-à-dire qu’on
peut y ajouter un nombre indéterminé d’éléments (comme c’était le cas pour la classe Liste
étudiée au chapitre précédant). Dans certaines situations, on peut cependant imposer une
capacité maximale à la pile (en pratique, c’est le cas de la pile de dossiers dans un bureau,
elle est forcément limitée par la hauteur du plafond !). Nous aborderons ce cas particulier
dans les exercices.
. Lors de l’implémentation de la classe, il faudra songer à envoyer un message d’erreur
lorsqu’on utilise les méthodes sommet et dépiler si la pile est vide. Si la pile possède une
taille maximale, alors c’est empiler qui doit générer une erreur lorsque la pile est pleine.
. Nous avons utilisé ici des noms de méthodes neutres indépendants de tout langage de
programmation. Dans la littérature anglaise, on trouvera souvent push, top et pop en lieu
et place de empiler, sommet et dépiler.
62
5.4. EXERCICES
5.4 Exercices
On remplira le tableau en ajoutant les éléments les uns à la suite des autres et en retenant la
position du dernier élément, qui correspondra à la valeur de l’attribut indSommet. Cet attribut
augmentera lorsqu’on ajoute un élément, et va décroître lorsqu’on en retire un.
Détaillez l’implémentation des méthodes de la classe PileTab correspondant à cette situation.
Il faudra aussi adapter le constructeur, en lui donnant comme paramètre la taille maximale de
la pile.
Un fichier aller contient la description d’un itinéraire. Chaque enregistrement du fichier est
une structure Étape contenant les champs ville (chaine) et km (réel). Écrire un algorithme
qui crée le fichier retour qui contiendra – au même format que aller – la description de
l’itinéraire retour.
Exemple :
63
5.4. EXERCICES
aller retour
Bruxelles 0 Amsterdam 0
Antwerpen 40 Utrecht 50
Breda 100 Breda 120
Utrecht 170 Antwerpen 180
Amsterdam 220 Bruxelles 220
La notation polonaise inverse (encore appelée notation postfixée) consiste à donner tous les
opérandes d’une opération avant de donner l’opérateur lui-même. Le résultat de l’opération
devient un opérande pouvant à son tour être utilisé comme nouvel opérande d’une opération.
L’intérêt de cette notation est qu’il ne nécessite pas de parenthèse à condition d’écrire les
opérandes dans le bon ordre.
Par exemple,
. 1 + 2 s’écrit « 1 2 + »
. (1 * 2) + 3 s’écrit « 1 2 * 3 + »
. 1 * (2 + 3) s’écrit « 1 2 3 + * »
. (4 + 3) * (3 + 2) s’écrit « 4 3 + 3 2 + * »
. 4 + (3 * 3) + 2 s’écrit « 4 3 3 * + 2 + »
. 32 - 2 * (12 - 3 * (7 - 2)) s’écrit « 32 2 12 3 7 2 - * - * - »
Reprenons l’exemple « 4 3 3 * + 2 + » : une pile contiendra successivement
3
3 3 9 2
4 4 4 4 13 13 15
À l’aide d’une Pile<int>, écrivez un algorithme qui interprète les opérations arithmétiques
simplifiées aux entiers en notation polonaise inverse.
Votre algorithme recevra en paramètre un tableau de chaines et retournera un entier, le résultat
de l’opération.
Vous pouvez utiliser les algorithmes
. estNombre(String c ) qui retourne vrai si c peut être converti en un nombre et faux
sinon.
. nombre(String c ) qui convertit c en l’entier correspondant
Nous supposerons le tableau entré comme ne comportant pas d’erreur et n’utilisant que des
opérateurs binaires +, -, * et / (pour la division entière).
64
Chapitre
6 La file
6.1 Définition
Une file est une collection d’éléments admettant les fonctionnalités suivantes :
La file (en anglais queue) est donc une collection de données de type premier entré, premier
sorti (en anglais on dit « FIFO », c’est-à-dire first in first out). L’analogie avec une file de
clients à un guichet (poste, caisse du supermarché, ...) est évidente : c’est le premier arrivé qui
est le premier servi, et il est très malvenu d’essayer de doubler une personne dans une file !
Noter qu’une fois entré dans une file – au sens informatique du terme – on ne peut pas en
sortir par l’arrière, le seul scénario possible pour en sortir est d’attendre patiemment son tour
et d’arriver en tête de la file.
De même que pour la pile, on ne peut donc pas non plus parcourir une file, ou consulter di-
rectement le n-ième élément. Les files sont très utiles en informatique, citons par exemple la
création de mémoire tampon (buffer) dans de nombreuses applications, les processeurs multi-
tâches qui doivent accorder du temps-machine à chaque tâche, la file d’attente des impressions
pour une imprimante, ...
65
6.3. EXERCICES
interface File<T>
// T est le type des éléments de la file
void enfiler(T élément) // ajoute un élément dans la file
T tête() // retourne la valeur de l’élément en tête de file, sans le retirer
T défiler() // enlève et retourne l’élément de tête
booléen estVide() // indique si la file est vide
6.2.2 Remarques :
. De même que dans le chapitre précédent, la file est supposée infinie, c’est-à-dire qu’on
peut y ajouter un nombre indéterminé d’éléments. Le cas de la file limitée à une capacité
maximale sera envisagé dans l’exercice 2.
. Dans l’implémentation, il faudra songer à envoyer un message d’erreur lorsqu’on utilise
les méthodes tête et défiler si la file est vide. Si la file possède une taille maximale, alors
c’est enfiler qui doit générer une erreur lorsque la file est pleine.
6.3 Exercices
Nous proposons ici d’offrir un constructeur qui reçoit le nombre maximum d’éléments de la file
et qui crée un tableau d’indices 0 à nbMaxÉlément (le tableau aura donc un élément de plus
que le nombre maximum d’éléments de la file, nous allons expliquer pourquoi). On remplit
le tableau en ajoutant les éléments les uns à la suite des autres en retenant la position du
premier et du dernier via deux indices. Lorsqu’on ajoute un élément, la position du dernier va
augmenter, et lorsqu’on enlève un élément, cela revient à augmenter la position du premier,
afin d’éviter de devoir retasser l’ensemble de la file en début de tableau, ce qui serait une perte
d’efficacité.
66
6.3. EXERCICES
Exemple :
Supposons qu’après la création de la file, les éléments 4, 3, 6 et 2 ont été ajoutés.
L’indice du premier est 0 et celui du dernier est 3.
4 3 6 2
On ajoute l’élément 7. L’indice du dernier est à présent 4.
4 3 6 2 7
On enlève un élément de la file. L’indice du premier vaut maintenant 1
3 6 2 7
Ce mécanisme peut se faire de façon circulaire : arrivés en fin de tableau, les indices premier
et dernier repassent à 0. Lorsque premier et dernier sont égaux, la file n’a qu’un seul élément.
Si cet élément est supprimé, l’incrémentation de premier aura pour conséquence que l’indice
premier sera d’une unité supérieur à l’indice dernier (en tenant compte de la rotation des
valeurs), et ce sera donc la condition correspondant à une file vide. Le tableau ne sera jamais
rempli au maximum pour pouvoir distinguer le cas d’une file vide et le cas d’une file pleine.
Nous placerons donc dans le tableau au maximum nbMaxÉlément éléments (en laissant un
élément « vide » entre les indices dernier et premier).
Exercice 3 Le monte-charge
Un monte-charge de chantier assure le transport de personnes et de matériel entre deux niveaux.
Lorsque le monte-charge arrive à un étage, tous ses passagers en sortent. La charge maximale
admise a la valeur MAX (donnée en paramètre). Étant donné les risques liés à l’utilisation du
monte-charge en surcharge, le poids de chacun des passagers et des charges qu’ils transportent
(outils, brouette de sable, ...) est vérifié avant l’entrée dans le monte-charge.
Pour gérer l’embarquement dans le monte-charge, on souhaite faire appel à un algorithme
« embarquement » qui détermine combien de personnes qui se trouvent actuellement en file
devant le monte-charge peuvent y entrer.
Cet algorithme respectera l’ordre d’arrivée des personnes et mettra à jour la file des personnes
en attente qu’il aura reçue en paramètre.
Exercice 4 À l’envers
Écrire un algorithme recevant en paramètre une file d’entiers. À l’issue de cet algorithme, les
valeurs de la file seront en ordre inverse et débarrassées des éléments pairs.
Exemple : Si le contenu de la file est le suivant :
1 → 0 → 4 → 25 → 20 → 11 → 3 → 7 → 8 → 73 → 2 → 4
son contenu sera après traitement :
73 → 7 → 3 → 11 → 25 → 1
67
Chapitre
7 La récursivité
7.1 Introduction
Une procédure est dite récursive lorsque dans sa description, une des étapes de cette procédure
est la procédure elle-même. Des termes synonymes de la récursivité sont la récurrence ou
l’auto-référence.
La récursivité est une démarche qui consiste à faire référence à ce qui fait l’objet de la démarche,
ainsi c’est le fait de décrire un processus dépendant de données en faisant appel à ce même
processus sur d’autres données plus « simples », de montrer une image contenant des images
similaires, de définir un concept en invoquant le même concept. (Wikipédia, août 2013)
Les illustrations ci-dessous sont des exemples célèbres de dessins récursifs :
La vache qui rit qui orne la fameuse boite de fromage porte des boucles d’oreilles en forme
de boites de fromage identiques au dessin entier. Il en est de même pour le dessin de la boite
de cacao Droste : sur le plateau porté par la dame apparait en plus petit la même boite de
cacao. Dans le troisième dessin, chaque carré contient un carré plus petit inscrit dans le carré
précédent de telle sorte que l’image formée à partir du 3e carré est identique au carré de départ,
mais réduite de 50%. Chacun de ces dessins s’auto-contient donc à une échelle plus petite. Il
en est de même pour le dessin fractal en haut de ce chapitre, il s’agit du triangle de Sierpinski
formé dans chaque coin de triangles plus petits mais pareils au triangle entier.
69
7.2. DÉFINITIONS RÉCURSIVES
n! = n ∗ (n − 1)!
0! = 1
. Parmi les autres exemples rencontrés au cours de mathématique, citons la formule récur-
sive des coefficients binomiaux :
p−1 p
Cnp = Cn−1 + Cn−1 si 0 < p < n
Cn0 = 1 et Cnn = 1
Cette définition permet de reconstruire facilement le triangle de Pascal (où n est l’indice
de ligne (on démarre avec n = 0) et p l’indice de l’élément à calculer dans cette ligne, le
premier élément de la ligne est d’indice p = 0). :
Noter que le calcul récursif est plus commode d’utilisation que la formule « directe »
faisant intervenir les factorielles :
n!
Cnp = (n−p)!p!
. Un troisième exemple est la formule récursive pour calculer les nombres de Fibonacci :
Fn = Fn−1 + Fn−2
70
7.3. ALGORITHME RÉCURSIF
Cette formule récursive est bien plus maniable que la formule donnant « directement » le
nombre d’indice n de la suite de Fibonnacci (avec l’indice n = 0 pour le premier élément) :
√ √
Fn = √1 ( 1+ 5 )n − √1 ( 1− 5 )n
5 2 5 2
une liste chainée est un ensemble soit vide, soit composé d’un élément auquel est attaché
une liste chainée.
Cette définition est intelligible par le fait qu’on sous-entend que la liste chainée à laquelle
la définition fait référence n’est plus identique à la première, mais plus petite d’un élément,
et éventuellement qu’elle peut se terminer (ensemble vide).
une chaine de caractères est un ensemble soit vide, soit composé d’un caractère concaténé
à une chaine de caractères.
void récursif(...)
// instructions
récursif(...)
// instructions
Si cette situation peut surprendre au premier abord, elle n’est pourtant pas très différente de
la situation suivante, appelée récursivité croisée :
void premier(...)
// instructions
second(...)
// instructions
void second(...)
// instructions
premier(...)
// instructions
71
7.4. UN GRAND EXEMPLE CLASSIQUE DE PROGRAMMATION RÉCURSIVE : LA
FACTORIELLE
Pour ne pas tomber dans ce processus sans fond, un algorithme récursif bien écrit doit forcément
posséder un (ou plusieurs) paramètre(s), et contenir une structure alternative qui conduit à une
clause d’évaluation immédiate de l’appel récursif pour une (ou plusieurs) valeur(s) initiale(s)
du(des) paramètre(s).
La forme générale est la suivante :
void récursif(paramètre)
if paramètre == valeur initiale
// il peut y avoir plusieurs cas initiaux
// instructions sans appel récursif.
else
// instructions menant à des valeurs différentes du paramètre,
// et après un nombre fini d’appels récursifs aux valeurs initiales.
int factorielle(int n)
if n == 0
return 1
else
return n * factorielle(n - 1)
7.5 Exercices
Dans les exercices suivants, il faudra parfois écrire un « algorithme façade », c’est-à-dire
un algorithme initial non récursif dans lequel les paramètres sont initialisés et qui appelle la
première fois l’algorithme récursif. Il est intéressant aussi de comparer chaque problème avec
sa solution itérative – lorsqu’elle existe – et de s’interroger sur la solution la plus efficace.
72
7.5. EXERCICES
. PGCD(a, 0) = a
. PGCD(a, b) = PGCD(b, a MOD b) si b 6= 0
Écrire un algorithme récursif pour le calcul du PGCD. Cet algorithme générera une erreur si
un des 2 nombres est négatif.
73
Chapitre
8 Mises en pratique
Nous voici arrivés au terme de ce cours d’algorithmique. Ce chapitre apporte une synthèse des
différentes notions vues tout au long de vos cours d’algorithmiques de 1re année et propose
quelques pistes de réflexion quant au choix d’une bonne représentation des données qui se pose
lors de la résolution de problèmes de programmation avancés.
Pour la plupart de ces exercices, la difficulté tient en partie dans le bon choix d’une repré-
sentation des données et de la démarche algorithmique la plus adéquate à mettre en œuvre
pour agir sur ces données en vue d’obtenir le résultat escompté. Noter que l’efficacité d’un
algorithme est liée étroitement au choix de la représentation.
75
8.2. LES STRUCTURES DE DONNÉES
8.3 Exercices
Après ces vérifications vous choisirez une des représentations pour écrire la solution (non OO
à ce stade) du jeu. Pensez à découper votre solution.
76
8.3. EXERCICES
Modifiez l’exercice précédent afin que le jeu puisse se jouer à n joueurs, où n est un entier
supérieur ou égal à 2, choisi au début du jeu.
class Joueur
private :
string nom // Le nom du joueur à cette position ("A", "B" ou " " si la case est vide)
int nbTours // Nb de tours qu’a fait le joueur qui est à cette position (0 si case vide)
public :
// Constructeurs et accesseurs
Un joueur a fait un tour complet quand il est de nouveau sur sa position de départ
ou la dépasse.
. joueurCourant : un entier donnant la position du joueur courant.
3. Dans cette proposition, le tableau change de signification.
77
8.3. EXERCICES
class Joueur
private :
int position // Donne la position du joueur sur le circuit (entier entre 1 et 50)
int nbTours // Le nombre de tours qu’a fait ce joueur
public :
// Constructeurs et accesseurs
class Joueur
private :
int position // Donne la position du joueur sur le circuit (entier entre 1 et 50)
int nbCasesParcourues // Nb de cases parcourues par le joueur depuis le départ
public :
// Constructeurs et accesseurs
78
8.3. EXERCICES
79
8.3. EXERCICES
Pour représenter l’occupation des chambres un jour donné, nous allons utiliser un tableau de
100 entiers. Un 0 indique que la chambre est libre, une autre valeur (positive) indique le numéro
du client qui occupe cette chambre ce jour-là.
Nous utiliserons une Liste de tels tableaux pour représenter l’occupation des chambres sur une
longue période ; les éléments se suivant correspondant à des jours successifs.
Nous vous imposons les attributs de la classe, à savoir :
. occupations : une Liste de tableaux de 100 entiers comme expliqué ci-dessus.
. premierJour : donne le jour concerné par le premier élément de la liste. Ainsi s’il vaut
10/9/2015 cela signifie que le premier élément de la liste « occupations » renseigne sur
l’occupation des chambres ce 10/9/2015 ; que le deuxième élément de la liste concerne le
11/9/2015 et ainsi de suite...
Écrire la méthode suivante :
. Elle retourne le numéro de la chambre qui a été choisie ou −1 si la réservation n’a pas
pu se faire.
. Si plusieurs chambres sont libres, on choisit celle avec le plus petit numéro.
. La demande de réservation peut couvrir une période qui n’est pas encore reprise dans la
liste ; il faudra alors l’agrandir.
. Vous pouvez supposer qu’il existe une méthode donnant le nombre de jours séparant deux
dates données.
class DemandeRéservation
private :
int numéroClient
Date débutRéservation
int nbNuitées
public :
// Constructeurs et accesseurs
Exercice 8 L’ensemble
La notion d’ensemble fini est une notion qui vous est déjà familière pour l’avoir rencontrée
dans plusieurs cours. Nous rappelons certaines de ses propriétés et opérations.
Étant donnés deux ensembles finis S et T ainsi qu’un élément x :
. x ∈ S signifie que l’élément x est un élément de l’ensemble S.
. L’ensemble vide, noté ∅ est l’ensemble qui n’a pas d’élément (x ∈ ∅ est faux quel que soit
x ).
. L’ordre des éléments dans un ensemble n’a aucune signification, l’ensemble {1,2} est
identique à {2,1}.
. Un élément x ne peut pas être plus d’une fois élément d’un même ensemble (pas de
répétition).
. L’union S ∪ T est l’ensemble contenant les éléments qui sont dans S ou (non exclusif)
dans T.
. L’intersection S ∩ T est l’ensemble des éléments qui sont à la fois dans S et dans T.
. La différence S \ T est l’ensemble des éléments qui sont dans S mais pas dans T.
Créer la classe Ensemble décrite ci-dessous (où E est le type des éléments de l’ensemble).
80
8.3. EXERCICES
class Ensemble de E
public :
void Ensemble de E() // construit un ensemble vide
void ajouter(E élt) // ajoute l’élément à l’ensemble
void enlever(E élt)) // enlève un élément de l’ensemble
boolean contient(E élt) // dit si l’élément est présent
boolean estVide() // dit si l’ensemble est vide
int taille() // donne la taille de l’ensemble
Ensemble de E union(Ensemble de E autreEnsemble)
Ensemble de E intersection(Ensemble de E autreEnsemble)
Ensemble de E moins(Ensemble de E autreEnsemble)
List of E éléments() // conversion en liste
Quelques remarques :
. La méthode d’ajout (resp. de suppression) n’a pas d’effet si l’élément est déjà (resp. n’est
pas) dans l’ensemble.
. Les méthodes union(), intersection() et moins() retournent un troisième ensemble, résultat
des 2 premiers sans toucher à ces 2 ensembles. On aurait pu envisager des méthodes
modifiant l’ensemble sur lequel on les appelle.
. La méthode éléments() est nécessaire si on veut parcourir les éléments de l’ensemble (par
exemple pour les afficher).
81
8.3. EXERCICES
Exercice 12 Casino
Pour cet exercice, on vous demande un petit programme qui simule un jeu de roulette très
simplifié dans un casino.
Dans ce jeu simplifié, vous pourrez miser une certaine somme et gagner ou perdre de l’argent
(telle est la fortune, au casino !). Quand vous n’avez plus d’argent, vous avez perdu.
Notre règle du jeu
Bon, la roulette, c’est très sympathique comme jeu, mais un peu trop compliqué pour un
exercice de première année. Alors, on va simplifier les règles et je vous présente tout de suite
ce que l’on obtient :
. Le joueur mise sur un numéro compris entre 0 et 49 (50 numéros en tout). En choisissant
son numéro, il y dépose la somme qu’il souhaite miser.
. La roulette est constituée de 50 cases allant naturellement de 0 à 49. Les numéros pairs
sont de couleur noire, les numéros impairs sont de couleur rouge. Le croupier lance la
roulette, lâche la bille et quand la roulette s’arrête, relève le numéro de la case dans
laquelle la bille s’est arrêtée. Dans notre programme, nous ne reprendrons pas tous ces
détails « matériels » mais ces explications sont aussi à l’intention de ceux qui ont eu la
chance d’éviter les salles de casino jusqu’ici. Le numéro sur lequel s’est arrêtée la bille
est, naturellement, le numéro gagnant.
. Si le numéro gagnant est celui sur lequel le joueur a misé (probabilité de 1/50, plutôt
faible), le croupier lui remet 3 fois la somme misée.
. Sinon, le croupier regarde si le numéro misé par le joueur est de la même couleur que le
numéro gagnant (s’ils sont tous les deux pairs ou tous les deux impairs). Si c’est le cas, le
croupier lui remet 50% de la somme misée. Si ce n’est pas le cas, le joueur perd sa mise.
Dans les deux scénarios gagnants vus ci-dessus (le numéro misé et le numéro gagnant sont
identiques ou ont la même couleur), le croupier remet au joueur la somme initialement misée
avant d’y ajouter ses gains. Cela veut dire que, dans ces deux scénarios, le joueur récupère de
l’argent. Il n’y a que dans le troisième cas qu’il perd la somme misée.
Comme vous pouvez le constater, on ne vous fait pas de proposition pour la représentation
des données. À vous de jouer !
82
8.3. EXERCICES
Exercice 14 Puissance 4
Le jeu de puissance 4 se déroule dans un tableau vertical comportant 6 rangées et 7 colonnes
dans lequel deux joueurs introduisent tour à tour des jetons (rouges pour l’un, jaunes pour
l’autre). Avec l’aide de la gravité, les jetons tombent toujours le plus bas possible dans les
colonnes où on les place. Le jeu s’achève lorsqu’un des joueurs a réussi à aligner 4 de ses jetons
horizontalement, verticalement ou en oblique, ou lorsque les deux joueurs ont disposé chacun
leur 21 jetons sans réaliser d’alignement (match nul).
On demande d’implémenter une classe Puissance4 qui permette de contrôler l’état des diffé-
rentes phases du jeu. Déterminez les attributs de cette classe et décrivez-les brièvement de
manière à justifier votre choix. Dotez ensuite la classe des méthodes permettant de :
N.B. : pour le contenu du tableau de jetons, vous pouvez adopter la convention suivante : 0
pour l’absence de jeton, 1 pour un jeton du 1er joueur et 2 un jeton du 2e joueur (on peut donc
faire abstraction de la couleur du jeton dans ce problème).
Exercice 15 Mastermind
Revenons sur le jeu Mastermind déjà vu en DEV2. Dans ce jeu, un joueur A doit trouver
une combinaison de k pions de couleur, choisie et tenue secrète par un autre joueur B. Cette
combinaison peut contenir éventuellement des pions de même couleur. À chaque proposition
du joueur A, le joueur B indique le nombre de pions de la proposition qui sont corrects et bien
placés et le nombre de pions corrects mais mal placés.
Exemple
Utilisons des lettres pour représenter les couleurs.
Combinaison secrète Proposition du joueur
R R V B J R V B B V
Il sera indiqué au joueur qu’il a :
. 2 pions bien placés : le R en 1re position et le second B en 4e position ;
. 1 pion mal placé : un des deux V (ils ne peuvent compter tous les deux).
Supposons une énumération Couleur (cf. la description d’une énumération en annexe) avec
toutes les couleurs possibles de pion.
a) Écrire une classe « Combinaison » pour représenter une combinaison de k pions. Elle
possède une méthode pour générer une combinaison aléatoire (que vous ne devez pas
écrire) et une méthode pour comparer une combinaison à la combinaison secrète (que
vous devez écrire)
b) Écrire ensuite une classe « MasterMind » qui représente le jeu et permet d’y jouer. La taille
de la combinaison et le nombre d’essais permis seront des paramètres du constructeur.
83
Compléments
Nous présentons ici quelques éléments qui pourront vous être utiles pour résoudre certains
exercices.
.1 Énumération
Parfois, une variable ne peut prendre qu’un ensemble fixe et fini de valeurs. Par exemple une
variable représentant une saison ne peut prendre que quatre valeurs (HIVER, PRINTEMPS,
ÉTÉ, AUTOMNE). On va l’indiquer grâce à l’énumération qui introduit un nouveau type
de donnée.
Il y a deux avantages à cela : une indication claire des possibilités de la variable lors de la
déclaration et une lisibilité du code grâce à l’utilisation des valeurs explicites.
Par exemple,
85
.2. GESTION DES ERREURS
86
Notre pseudo-code
Dans le cadre de ce cours, nous vous proposons d’écrire un pseudo-code très inspiré du langage
Java : à gauche le pseudo-code de ALG2 à droite celui de ALG1.
. Déclarer une variable se fait comme en Java, avec les types de données du langage Java 2 :
int age
age : entier
float prix prix : réel
nom : chaîne
String nom ok : booléen
ALG1
Boolean ok
. L’assignation et l’égalité :
age = 20 age <— 20
nom = "Dimitri" nom <— "Dimitri"
ok <— age = 20
ok = age == 20 ALG1
écrire "coucou"
print "coucou"
ALG1
ou une méthode :
method T get(int pos) algorithme get(pos : entier) -> T
/* La classe a un attribut tab de type tableau de T */
return tab[pos] retourner tab[pos]
fin algorithme
ALG1
La distinction entre méthode et algorithme est qu’une méthode a accès aux attributs de
classe de l’objet courant en plus des paramètres explicites.
. Déclarer, instancier, initialiser, passer en paramètre un tableau :
tab : tableau de 5 entiers
pour i de 0 à tab.taille - 1
int[] tab = {1,2,3,4,5} tab[i] <— i+1
fin pour
ALG1
87
.3. EXEMPLE : UN MOT DANS UN MIROIR
// int[10] compteurs
int[] tab = new int[10];
// int[] resultat
int[] resultat;
def afficherMiroir(mot) :
for i in range(len(mot)-1, -1,-1) :
print(mot[i])
88
.4. EXEMPLE : POSITION DES MINIMUMS.
int[n] indiceMin
int posiMin = 0
int derniereCase = 0
indiceMin[0] = 0
if monTab.length >= 2
indiceMin[1] = -1
for i from 1 to monTab.length - 1
if monTab[i] < monTab[posiMin]
posiMin = i
reinitTab(indiceMin)
indiceMin[0] = i
derniereCase = 0
else if monTab[i] == monTab[posiMin]
derniereCase = derniereCase + 1
indiceMin[derniereCase] = i
return indiceMin
void reinitTab(int[n] tab)
void principal()
89
Aide mémoire
Cet aide-mémoire peut vous accompagner lors d’une interrogation ou d’un examen. Il vous est
permis d’utiliser ces méthodes sans les développer. Par contre, si vous sentez le besoin d’utiliser
une méthode qui n’apparait pas ici, il faudra en écrire explicitement le contenu.
.5 Manipuler le hasard
.6 La liste
.7 ArrayList
class ArrayList<T>
private :
T[] tab // Le tableau contenant les données.
int size // La taille logique du tableau.
91
.8. SINGLYLINKEDLIST
.8 SinglyLinkedList
class SinglyLinkedList<T>
private :
Node head // Référence à la tête de liste.
92