Algorithmique Elementaire
Algorithmique Elementaire
Algorithmique élémentaire
Dans ce chapitre, nous introduisons les concepts élémentaires de l’algorithmique. Nous voyons tout
d’abord les structures élémentaires à partir desquelles sont construits tous les algorithmes écrits dans
un langage impératif. Ces structures ont déjà été introduites au cours du chapitre 1 dans le cadre de Py-
thon. Nous donnons ensuite quelques algorithmes simples intervenant très souvent comme brique d’une
construction plus imposante. Ces petits algorithmes sont à bien maîtriser : il faut être capable de les
implémenter rapidement lorsque le besoin se fait sentir, éventuellement en les adaptant à la situation.
Le développement de l’informatique a marqué l’essor de l’algorithmique, mais cette discipline n’est pas
l’apanage de l’informatique. La notion d’algorithme est liée mathématiquement à la possibilité de réso-
lution systématique d’un problème, donc à la notion de méthode de calcul. On trouve dans le domaine
purement mathématique de nombreux algorithmes :
• tous les algorithmes de calcul des opérations élémentaires (addition posée, multiplication posée...)
• l’algorithme de la division euclidenne par différences successives
• l’algorithme d’Euclide du calcul du pgcd
• l’algorithme de résolution des équations de degré 2
• l’algorithme du pivot de Gauss pour résoudre les systèmes d’équations linéaires, et répondre à
d’autres questions d’algèbre linéaire.
• l’algorithme de Hörner pour l’évaluation d’un polynôme
• etc.
Les questions qu’on peut se poser sont alors les suivantes :
46 CHAPITRE 3. ALGORITHMIQUE ÉLÉMENTAIRE
1. Quelles sont les structures élémentaires à partir desquelles sont construits les algorithmes.
2. L’algorithme s’arrête-t-il ? (problème de la terminaison)
3. L’algorithme renvoie-t-il le résultat attendu ? (problème de la correction)
4. Combien de temps dure l’exécution de l’algorithme, notamment lorsqu’on le lance sur de grandes
données ? (problème de la complexité).
Nous nous intéressons dans ce chapitre à la première question, les 3 autres faisant l’objet d’un chapitre
ultérieur (analyse des algorithmes)
I.2 Le langage
L’étude algorithmique formelle nécessite de se dégager de toute contrainte idiomatique relevant des spé-
cificités de tel ou tel langage. Pour cela, nous utiliserons un pseudo-code, indépendant de toute implé-
mentation. Les traductions ultérieures dans un langage de programmation spécifique se font alors sans
difficulté, sachant que certains langages offrent parfois certaines possibilités supplémentaires (qui sont
essentiellement du confort, mais n’ajoutent rien à la description formelle des algorithmes, comme par
exemple le fait de pouvoir construire une boucle for sur un objet itérable queconque en Python).
Nous décrirons systématiquement un algorithme en :
1. lui donnant un nom ;
2. définissant les données initiales (variables d’entrée, préciser leur type) ;
3. définissant la sortie (variables de résultat, préciser leur type) ;
4. donnant le bloc d’instructions définissant l’algorithme.
La structure générale est donc la suivante :
Certains langages autorisent le branchement multiple, soit via une autre instruction (par exemple
case of en Pascal), soit comme surcouche de l’instruction basique. Nous utiliserons cette possi-
bilité en pseudo-code (algorithme 3.5).
Évidemment, d’un point de vue de la complétion du langage, cette possibilité n’apporte rien de
neuf, puisqu’elle est équivalente à un emboîtement de structures conditionnelles (algorithme 3.6).
48 CHAPITRE 3. ALGORITHMIQUE ÉLÉMENTAIRE
3. Les boucles
Une boucle est une succession d’instructions, répétée un certain nombre de fois. Le nombre de
passages dans la boucle peut être déterminé à l’avance, ou peut dépendre d’une condition vérifiée
en cours d’exécution. Cela permet de distinguer plusieurs types de boucles :
(a) Boucles conditionnelles, avec condition de continuation.
On passe dans la boucle tant qu’une certaine condition est réalisée. Le test de la condition est
réalisé avant le passage dans la boucle. On utilise pour cela une boucle while ou tant que en
français. L’algorithme 3.7 en est un exemple.
Ici, on calcule les termes d’une suite définie par la récurrence un+1 = sin(un ). On peut montrer
facilement que cette suite tend vers 0. On répète l’itération de la suite (donc le calcul des termes
successifs) tant que les valeurs restent supérieures à ε.
Comme dans l’exemple ci-dessus, une boucle while s’utilise le plus souvent lorsqu’on ne connaît
pas à l’avance le nombre de passages dans la boucle. Souvent d’ailleurs, c’est le nombre de
passages dans la boucle qui nous intéresse (afin, dans l’exemple ci-dessus, d’estimer la vitesse
de convergence de la suite). Dans ce cas, il faut rajouter un compteur de passages dans la
boucle, c’est-à-dire une variable qui s’incrémente à chaque passage dans la boucle. C’est ce que
nous avons fait dans l’algorithme 3.8.
La valeur finale de i nous dit maintenant jusqu’à quel rang de la suite il faut aller pour obtenir
la première valeur inférieure à ε (et par décroissance, facile à montrer, toutes les suivantes
vérifieront la même inégalité).
(b) Boucles conditionnelles, avec condition d’arrêt
Il s’agit essentiellement de la même chose, mais exprimé de façon légèrement différente. Ici,
on répète la série d’instructions jusqu’à la réalisation d’une certaine condition. Il s’agit de la
boucle repeat... until..., ou répéter... jusqu’à ce que..., en français. L’algorithme
précédent peut se réécrire de la façon suivante, de façon quasi-équivalente, à l’aide d’une boucle
repeat... until... (algorithme 3.9).
La différence essentielle avec une boucle while est que, contrairement à une boucle while,
I Qu’est-ce qu’un algorithme 49
on passe nécessairement au moins une fois dans la boucle. À part ce détail, on a équivalence
entre les deux structures, en remplaçant la condition de continuation par une condition d’arrêt
(par négation). Ainsi, une boucle repeat instructions until condition est équivalente à
la structure donnée en algorithme 3.10.
Remarquez ici le passage forcé une première fois dans la succession d’instructions (bloc isolé
avant la structure).
On peut éviter la répétition de la succession d’instructions en forçant le premier passage à
l’aide d’une variable booléenne (algorithme 3.11)
autoriseront à décrire les algorithmes en utilisant cette structure, plus naturelle dans certaines
situations. Il faut cependant garder à l’esprit que dans le cadre d’une définition formelle d’un
algorithme, cela crée une redondance avec la structure while.
(c) Boucles inconditionnelles
Il s’agit de boucles dont l’arrêt ne va pas dépendre d’une condition d’arrêt testée à chaque ité-
ration. Dans cette structure, on connaît par avance le nombre de passages dans la boucle. Ainsi,
on compte le nombre de passages, et on s’arrête au bout du nombre souhaité de passages. Le
compteur est donné par une variable incrémentée automatiquement, comme dans l’algorithme
Une fonction est similaire à une procédure, mais renvoie en plus une valeur de sortie. En général, il est
conseillé de faire en sorte que la seule action d’une fonction soit ce retour d’une valeur. En particulier, on
ne fait aucune interface avec l’utilisateur : pas d’affichage, ni de lecture de valeur (les valeurs à utiliser
pour la fonction étant alors passées en paramètres).
Par exemple, la fonction 3.15 retourne une valeur entière n. À quoi correspond-elle ?
La fonction 3.16 est une fonction récursive pour le calcul la suite définie par une récurrence simple, en
l’occurrence un+1 = sin(un ), initialisée par 1.
La fonction 3.17 est une très mauvaise façon de calculer le n-ième terme de la suite de Fibonacci par
récursivité. En pratique, vous calculerez plus vite à la main F100 que l’ordinateur par cette fonction.
Pourquoi cet algorithme récursif est-il si mauvais ?
52 CHAPITRE 3. ALGORITHMIQUE ÉLÉMENTAIRE
L’étude de la récursivité est du ressort du programme de Spé (ou de Sup en cours d’option). Nous nous
contenterons donc cette année d’une utilisation naïve de la récursivité, tout en restant conscient des
dangers d’explosion de complexité que cela peut amener.
Problème : Étant donné une liste de réels, trouver le maximum de cette liste (ou de façon similaire, son
minimum).
Idée de résolution : Progresser dans le tableau en mémorisant le plus grand élément rencontré jusque-
là. On peut aussi mémoriser sa place.
II Étude de quelques algorithmes de recherche 53
On utilise la convention usuelle de Python pour les indexations : un tableau de taille n est supposé indexé
de 0 à n − 1.
Problème : Étant donné une liste, trouver un terme particulier dans cette liste, s’il y est.
Idée de résolution : Parcourir la liste jusqu’à ce qu’on trouve l’élément souhaité. Variantes : si on veut
toutes les occurrences, on parcourt la liste en entier ; si on veut la dernière occurrence, on parcourt le
tableau à partir de la fin.
Remarque 3.2.1
Si le tableau T est utilisé essentiellement pour des tests d’appartenance, il faut se demander si la
structure de tableau est vraiment adaptée : il est peut-être plus intéressant de considérer une structure
d’ensemble, le test d’appartenance s’y faisant plus rapidement.
Problème : Étant donné un tableau T trié et un élément a comparable aux éléments du tableau, trouver
la première occurrence de a (donc pouvoir dire si a est dans le tableau ou non), ou, de façon équivalente,
trouver le plus petit indice i tel que a 6 T [i] (dans ce cas, a est dans le tableau, de plus petit occurrence
i, si et seulement si a = T [i]). Ce dernier problème étant un peu plus général, c’est lui qu’on étudie.
54 CHAPITRE 3. ALGORITHMIQUE ÉLÉMENTAIRE
Idée 1 : parcourir le tableau dans l’ordre pour repérer i. Cela ne change pas grand chose à la méthode
de recherche dans un tableau non trié. En particulier, en moyenne, le coût sera linéaire.
Idée 2 : faire une dichotomie, en coupant à chaque étape le tableau en 2, afin de n’en garder que la
moitié pertinente. Intuitivement, cela va beaucoup plus vite, puisqu’à chaque étape, on réduit le nombre
de possibilités de moitié.
Exercice 1
1. Expliquer comment améliorer l’algorithme si on ne recherche pas nécessairement la première
occurrence.
2. Expliquer comment récupérer la dernière occurrence.
3. Comment récupérer l’ensemble de toutes les occurrences ?
4. Proposer une variante de cet algorithme pour obtenir un test d’appartenance à une liste triée.
Remarque 3.2.2
On prouvera rigoureusement un peu plus tard que cet algorithme s’arrête toujours et fournit bien ce
qu’on veut, à l’aide de variants et d’invariants de boucles.
Moyennant un prétraitement du texte (confection d’une table triée des suffixes, pouvant se faire de façon
naïve en Θ(n ln(n)) : pour une donnée de taille n, le temps de réponse est de l’ordre de n ln(n)), on peut
améliorer les performances de la recherche en descendant à O(ln(n)) (en considérant k comme constante),
sans compter le prétraitement. Avec le prétraitement naïf, on y perd (mais on peut faire ce prétraitement
II Étude de quelques algorithmes de recherche 55
plus rapidement). Ainsi, cela s’avère intéressant si le prétraitement est fait une fois pour toutes (dans un
texte finalisé qui n’est pas trop amené à être modifié ; des mises à jours restent possibles), et est antérieur
à la demande de recherche. C’est ce type d’algorithmes qui est à la base des recherches dans les pages
internet.
On verra en TP quelques algorithmes de tri de tableau. Ces algorithmes sont utiles dans la vie pratique
tous les jours (par exemple lorsqu’on ordonne des données dans un tableur ou une base de données).
Ils sont aussi à la base de nombreux algorithmes plus sophistiqués. Il est donc important de pouvoir
trouver des tris de complexité minimale. Nous verrons plusieurs algorithmes en Θ(n2 ) (tri à bulles, tri
par sélection, tri par insertion), et quelques algorithmes en Θ(n ln(n)) (en moyenne) (tri rapide, tri fusion).
On peut montrer que dans une situation générale, c’est le plus rapide qu’on puisse faire. Sous certaines
hypothèses supplémentaires (par exemple travailler sur un ensemble fini de données), on peut descendre
encore.