Cours Algorithme Avancé - HBB
Cours Algorithme Avancé - HBB
C HAP 1. L ES T ABLEAUX
« Si on ment à un compilateur, il prendra sa revanche. » - Henry Spencer.
Bonne nouvelle ! Je vous avais annoncé qu’il y a avait en tout et pour tout quatre
structures logiques dans la programmation. Eh bien, ça y est, on les a toutes passées en revue.
Mauvaise nouvelle, il vous reste tout de même quelques petites choses à apprendre…
1
Un tableau doit être déclaré comme tel, en précisant le nombre et le type de valeurs qu’il
contiendra (la déclaration des tableaux est susceptible de varier d'un langage à l'autre. Certains
langages réclament le nombre d'éléments, d'autre le plus grand indice... C'est donc là aussi
une affaire de conventions).
En nous calquant sur les choix les plus fréquents dans les langages de programmations,
nous déciderons ici arbitrairement et une bonne fois pour toutes que :
• les "cases" sont numérotées à partir de zéro, autrement dit que le plus petit
indice est zéro.
• lors de la déclaration d'un tableau, on précise la plus grande valeur de l'indice
(différente, donc, du nombre de cases du tableau, puisque si on veut 12 emplacements,
le plus grand indice sera 11). Au début, ça déroute, mais vous verrez, avec le temps,
on se fait à tout, même au pire.
2
langage C et en Visual Basic. Donc attention, Truc[6] est le septième élément du
tableau Truc !
• être un nombre entier Quel que soit le langage, l’élément Truc[3,1416]
n’existe jamais.
• être inférieure ou égale au nombre d’éléments du tableau (moins 1, si l’on
commence la numérotation à zéro). Si le tableau Bidule a été déclaré comme ayant 25
éléments, la présence dans une ligne, sous une forme ou sous une autre, de Bidule[32]
déclenchera automatiquement une erreur.
Je le re-re-répète, si l’on est dans un langage où les indices commencent à zéro, il faut en
tenir compte à la déclaration :
Tableau Note[13] en Numérique
...créera un tableau de 14 éléments, le plus petit indice étant 0 et le plus grand 13.
LE GAG DE LA JOURNEE
Il consiste à confondre, dans sa tête et / ou dans un algorithme, l’indice d’un élément d’un
tableau avec le contenu de cet élément. La troisième maison de la rue n’a pas forcément trois
habitants, et la vingtième vingt habitants. En notation algorithmique, il n’y a aucun rapport
entre i et truc[i].
Exercice 1
Que produit l’algorithme suivant ?
Tableau Nb[5] en Entier
Variable i en Entier
Début
Pour i ← 0 à 5
Nb[i] ← i * i
i suivant
Pour i ← 0 à 5
Ecrire Nb[i]
i suivant
Fin
Peut-on simplifier cet algorithme avec le même résultat ?
Exercice 2
Que produit l’algorithme suivant ?
Tableau N[6] en Entier
Variables i, k en Entier
Début
N[0] ← 1
3
Pour k ← 1 à 6
N[k] ← N[k-1] + 2
k Suivant
Pour i ← 0 à 6
Ecrire N[i]
i suivant
Fin
Peut-on simplifier cet algorithme avec le même résultat ?
--- Corrigé 1 :
Cet algorithme remplit un tableau avec six valeurs : 0, 1, 4, 9, 16, 25.
Il les écrit ensuite à l’écran. Simplification :
Variable i en Numérique
Début
Pour i ← 0 à 5
Ecrire i * i
i Suivant
Fin
--- Corrigé 2 :
Cet algorithme remplit un tableau avec les sept valeurs : 1, 3, 5, 7, 9, 11, 13.
Il les écrit ensuite à l’écran. Simplification :
Tableau N[6] en Numérique
Variables i, k en Numérique
Début
N[0] ← 1
Ecrire N[0]
Pour k ← 1 à 6
N[k] ← N[k-1] + 2
Ecrire N[k]
4
k Suivant
Fin
3. Tableaux dynamiques
Il arrive fréquemment que l’on ne connaisse pas à l’avance le nombre d’éléments que
devra comporter un tableau. Bien sûr, une solution consisterait à déclarer un tableau
gigantesque (10 000 éléments, pourquoi pas, au diable les varices) pour être sûr que « ça
rentre ». Mais d’une part, on n’en sera jamais parfaitement sûr, d’autre part, en raison de
l’immensité de la place mémoire réservée – et la plupart du temps non utilisée, c’est un gâchis
préjudiciable à la rapidité, voire à la viabilité, de notre algorithme.
Aussi, pour parer à ce genre de situation, a-t-on la possibilité de déclarer le tableau sans
préciser au départ son nombre d’éléments. Ce n’est que dans un second temps, au cours du
programme, que l’on va fixer ce nombre via une instruction de redimensionnement : Redim.
Notez que tant qu’on n’a pas précisé le nombre d’éléments d’un tableau, d’une
manière ou d’une autre, ce tableau est inutilisable.
Exemple : on veut faire saisir des notes pour un calcul de moyenne, mais on ne sait pas
combien il y aura de notes à saisir. Le début de l’algorithme sera quelque chose du genre :
Tableau Notes[] en Numérique
Variable nb en Numérique
Début
Ecrire "Combien y a-t-il de notes à saisir ?"
Lire nb
Redim Notes[nb-1]
…
Cette technique n’a rien de sorcier, mais elle fait partie de l’arsenal de base de la
programmation en gestion.
Exercice 1
Ecrivez un algorithme permettant à l’utilisateur de saisir un nombre quelconque de
valeurs, qui devront être stockées dans un tableau. L’utilisateur doit donc commencer par
entrer le nombre de valeurs qu’il compte saisir. Il effectuera ensuite cette saisie. Enfin, une
fois la saisie terminée, le programme affichera le nombre de valeurs négatives et le nombre
de valeurs positives.
Exercice 2
Ecrivez un algorithme calculant la somme des valeurs d’un tableau (on suppose que le
tableau a été préalablement saisi).
Exercice 3
5
Ecrivez un algorithme constituant un tableau, à partir de deux tableaux de même
longueur préalablement saisis. Le nouveau tableau sera la somme des éléments des deux
tableaux de départ.
Tableau 1 :
4 8 7 9 1 5 4 6
Tableau 2 :
7 6 5 2 1 3 7 4
Tableau à constituer :
11 14 12 11 2 8 11 10
--- Corrigé 1 :
Variables Nb, Nbpos, Nbneg en Numérique
Tableau T[] en Numérique
Variable i en Numérique
Debut
Ecrire "Entrez le nombre de valeurs :"
Lire Nb
Redim T[Nb-1]
Nbpos ← 0
Nbneg ← 0
Pour i ← 0 à Nb - 1
Ecrire "Entrez le nombre n° ", i + 1
Lire T[i]
Si T[i] > 0 alors
Nbpos ← Nbpos + 1
Sinon
Nbneg ← Nbneg + 1
Finsi
i Suivant
Ecrire "Nombre de valeurs positives : ", Nbpos
Ecrire "Nombre de valeurs négatives : ", Nbneg
Fin
--- Corrigé 2 :
Variables i, Som, N en Numérique
Tableau T[] en Numérique
Debut
… (on ne programme pas la saisie du tableau, dont on suppose qu’il compte N éléments]
6
Redim T[N-1]
…
Som ← 0
Pour i ← 0 à N - 1
Som ← Som + T[i]
i Suivant
Ecrire "Somme des éléments du tableau : ", Som
Fin
--- Corrigé 3 :
Variables i, N en Numérique
Tableaux T1[], T2[], T3[] en Numérique
Debut
… (on suppose que T1 et T2 comptent N éléments, et qu’ils sont déjà saisis)
Redim T3[N-1]
…
Pour i ← 0 à N - 1
T3[i] ← T1[i] + T2[i]
i Suivant
Fin
7
I-Techniques Rusees
« Informatique : alliance d'une science inexacte et d'une activité humaine faillible » - Luc
Fayard
Une fois n’est pas coutume, ce chapitre n’a pas pour but de présenter un nouveau type de
données, un nouveau jeu d’instructions, ou que sais-je encore.
Son propos est de détailler quelques techniques de programmation qui possèdent deux
grands points communs :
Et que valent quelques kilos d’aspirine, comparés à l’ineffable bonheur procuré par la
compréhension suprême des arcanes de l’algorithmique ? Hein ?
45 122 12 3 21 78 64 53 89 28 84 46
On commence par rechercher, parmi les 12 valeurs, quel est le plus petit élément , et où il
se trouve. On l’identifie en quatrième position (c’est le nombre 3), et on l’échange alors avec
le premier élément (le nombre 45). Le tableau devient ainsi :
3 122 12 45 21 78 64 53 89 28 84 46
On recommence à chercher le plus petit élément, mais cette fois, seulement à partir du
deuxième (puisque le premier est maintenant correct, on n’y touche plus). On le trouve en
troisième position (c’est le nombre 12). On échange donc le deuxième avec le troisième :
3 12 122 45 21 78 64 53 89 28 84 46
8
On recommence à chercher le plus petit élément à partir du troisième (puisque les deux
premiers sont maintenant bien placés), et on le place correctement, en l’échangeant, ce qui
donnera in fine :
3 12 21 45 122 78 64 53 89 28 84 46
9
Revenons (ouf) dans le monde du silence, et écrivons l'algorithme correspondant à cette
variante :
boucle principale : le point de départ se décale à chaque tour
Pour i ← 0 à 10
on examine tous les éléments suivants
Pour j ← i + 1 à 11
Si t[j] < t[i] Alors
on fait l'échange !
temp ← t[i]
t[i] ← t[j]
t[j] ← temp
Finsi
j suivant
On a placé correctement l'élément numéro i, on passe à présent au suivant.
i suivant
10
Il nous reste à combler les points d'interrogation de la boucle Pour. Évidemment, il va
falloir comparer N à chaque élément du tableau : si les deux valeurs sont égales, alors bingo,
N fait partie du tableau. Cela va se traduire, bien entendu, par un Si … Alors … Sinon. Et
voilà le programmeur raisonnant hâtivement qui se vautre en écrivant :
Tableau Tab[19] en Numérique
Variable N en Numérique
Début
Ecrire "Entrez la valeur à rechercher"
Lire N
Pour i ← 0 à 19
Si N = Tab[i] Alors
Ecrire N "fait partie du tableau"
Sinon
Ecrire N "ne fait pas partie du tableau"
FinSi
i suivant
Fin
Et patatras, cet algorithme est une véritable catastrophe.
Il suffit d'ailleurs de le faire tourner mentalement pour s'en rendre compte. De deux
choses l'une : ou bien la valeur N figure dans le tableau, ou bien elle n'y figure pas. Mais dans
tous les cas, l'algorithme ne doit produire qu'une seule réponse, quel que soit le nombre
d'éléments que compte le tableau. Or, l'algorithme ci-dessus envoie à l'écran autant de
messages qu'il y a de valeurs dans le tableau, en l'occurrence pas moins de 20 !
Il y a donc une erreur manifeste de conception : l'écriture du message ne peut se trouver à
l'intérieur de la boucle : elle doit figurer à l'extérieur. On sait si la valeur était dans le tableau
ou non uniquement lorsque le balayage du tableau est entièrement accompli.
Nous réécrivons donc cet algorithme en plaçant le test après la boucle. Faute de mieux,
on se contentera de faire dépendre pour le moment la réponse d'une variable booléenne que
nous appellerons Trouvé.
Tableau Tab[19] en Numérique
Variable N en Numérique
Début
Ecrire "Entrez la valeur à rechercher"
Lire N
Pour i ← 0 à 19
???
i suivant
Si Trouvé Alors
Ecrire "N fait partie du tableau"
Sinon
Ecrire "N ne fait pas partie du tableau"
FinSi
Fin
Il ne nous reste plus qu'à gérer la variable Trouvé. Ceci se fait en deux étapes.
11
• un test figurant dans la boucle, indiquant lorsque la variable Trouvé doit
devenir vraie (à savoir, lorsque la valeur N est rencontrée dans le tableau). Et
attention : le test est asymétrique. Il ne comporte pas de "sinon". On reviendra là
dessus dans un instant.
• last, but not least, l'affectation par défaut de la variable Trouvé, dont la valeur
de départ doit être évidemment Faux.
12
valeur cherchée... ou qu'on sera arrivés à la fin du tableau. Appliquons la transformation de
Morgan : Il faut donc poursuivre la recherche tant qu'on n'a pas trouvé la valeur et qu'on n'est
pas parvenu à la fin du tableau. Démonstration :
Tableau Tab[19] en Numérique
Variable N en Numérique
Début
Ecrire "Entrez la valeur à rechercher"
Lire N
i ← 0
TantQue N <> T[i] et i < 19
i ← i + 1
FinTantQue
Si N = Tab[i] Alors
Ecrire "N fait partie du tableau"
Sinon
Ecrire "N ne fait pas partie du tableau"
FinSi
Fin
13
Nous baptiserons le flag Yapermute, car cette variable booléenne va nous indiquer si
nous avons ou non procédé à une permutation au cours du dernier balayage du tableau (dans
le cas contraire, c’est signe que le tableau est trié, et donc qu’on peut arrêter la machine à
bulles). La boucle principale sera alors :
Variable Yapermute en Booléen
Début
…
TantQue Yapermute
…
FinTantQue
Fin
Que va-t-on faire à l’intérieur de la boucle ? Prendre les éléments du tableau, du premier
jusqu’à l’avant-dernier, et procéder à un échange si nécessaire. C’est parti :
Variable Yapermute en Booléen
Début
…
TantQue Yapermute
Pour i ← 0 à 10
Si t[i] > t[i+1] Alors
temp ← t[i]
t[i] ← t[i+1]
t[i+1] ← temp
Finsi
i suivant
Fin
Mais il ne faut pas oublier un détail capital : la gestion de notre flag. L’idée, c’est que
cette variable va nous signaler le fait qu’il y a eu au moins une permutation effectuée. Il faut
donc :
• lui attribuer la valeur Vrai dès qu’une seule permutation a été faite (il suffit
qu’il y en ait eu une seule pour qu’on doive tout recommencer encore une fois).
• la remettre à Faux à chaque tour de la boucle principale (quand on recommence
un nouveau tour général de bulles, il n’y a pas encore eu d’éléments échangés),
• dernier point, il ne faut pas oublier de lancer la boucle principale, et pour cela
de donner la valeur Vrai au flag au tout départ de l’algorithme.
14
temp ← t[i]
t[i] ← t[i+1]
t[i+1] ← temp
Yapermut ← Vrai
Finsi
i suivant
FinTantQue
Fin
Au risque de me répéter, la compréhension et la maîtrise du principe du flag font partie
de l’arsenal du programmeur bien armé.
4. La recherche dichotomique
Je ne sais pas si on progresse vraiment en algorithmique, mais en tout cas, qu'est-ce qu'on
apprend comme vocabulaire !
Blague dans le coin, nous allons terminer ce chapitre migraineux par une technique
célèbre de recherche, qui révèle toute son utilité lorsque le nombre d'éléments est très élevé.
Par exemple, imaginons que nous ayons un programme qui doive vérifier si un mot existe
dans le dictionnaire. Nous pouvons supposer que le dictionnaire a été préalablement entré
dans un tableau (à raison d'un mot par emplacement). Ceci peut nous mener à, disons à la
louche, 40 000 mots.
Une première manière de vérifier si un mot se trouve dans le dictionnaire consiste à
examiner successivement tous les mots du dictionnaire, du premier au dernier, et à les
comparer avec le mot à vérifier. Ca marche, mais cela risque d'être long : si le mot ne se
trouve pas dans le dictionnaire, le programme ne le saura qu'après 40 000 tours de boucle ! Et
même si le mot figure dans le dictionnaire, la réponse exigera tout de même en moyenne 20
000 tours de boucle. C'est beaucoup, même pour un ordinateur.
Or, il y a une autre manière de chercher, bien plus intelligente pourrait-on dire, et qui met
à profit le fait que dans un dictionnaire, les mots sont triés par ordre alphabétique. D'ailleurs,
un être humain qui cherche un mot dans le dictionnaire ne lit jamais tous les mots, du premier
au dernier : il utilise lui aussi le fait que les mots sont triés.
Pour une machine, quelle est la manière la plus rationnelle de chercher dans un
dictionnaire ? C'est de comparer le mot à vérifier avec le mot qui se trouve pile poil au milieu
du dictionnaire. Si le mot à vérifier est antérieur dans l'ordre alphabétique, on sait qu'on devra
le chercher dorénavant dans le première moitié du dico. Sinon, on sait maintenant qu'on devra
le chercher dans la deuxième moitié.
A partir de là, on prend la moitié de dictionnaire qui nous reste, et on recommence : on
compare le mot à chercher avec celui qui se trouve au milieu du morceau de dictionnaire
restant. On écarte la mauvaise moitié, et on recommence, et ainsi de suite.
A force de couper notre dictionnaire en deux, puis encore en deux, etc. on va finir par se
retrouver avec des morceaux qui ne contiennent plus qu'un seul mot. Et si on n'est pas tombé
sur le bon mot à un moment ou à un autre, c'est que le mot à vérifier ne fait pas partie du
dictionnaire.
Regardons ce que cela donne en terme de nombre d'opérations à effectuer, en choisissant
le pire cas : celui où le mot est absent du dictionnaire.
15
• Au départ, on cherche le mot parmi 40 000.
• Après le test n°1, on ne le cherche plus que parmi 20 000.
• Après le test n°2, on ne le cherche plus que parmi 10 000.
• Après le test n°3, on ne le cherche plus que parmi 5 000.
• etc.
• Après le test n°15, on ne le cherche plus que parmi 2.
• Après le test n°16, on ne le cherche plus que parmi 1.
Et là, on sait que le mot n'existe pas. Moralité : on a obtenu notre réponse en 16
opérations contre 40 000 précédemment ! Il n'y a pas photo sur l'écart de performances entre
la technique barbare et la technique futée. Attention, toutefois, même si c'est évident, je le
répète avec force : la recherche dichotomique ne peut s'effectuer que sur des éléments
préalablement triés.
Eh bien maintenant qu'on a vu la méthode, il n'y a plus qu'à traduire en pseudo-code !
On suppose qu'on dispose d'un tableau T[N-1] et qu'on y recherche une
valeur numérique X
On déclare trois numériques (début, milieu et fin) et un booléen (Trouvé)
...
Début
…
Trouvé ← Faux
Debut ← 0
Fin ← N-1
TantQue Non Trouvé ET Debut <= Fin
milieu ← (début + fin)/2
Si T[milieu]=X Alors
Trouvé ← Vrai
SinonSi T[milieu] < X Alors
Debut ← milieu + 1
Sinon
Fin ← milieu - 1
FinSi
FinTantQue
À l'issue de la boucle, la variable Trouvé contient le résultat
...
Fin
Au risque de me répéter, la compréhension et la maîtrise du principe du flag, du tri, et de
la recherche (dichotomique ou non) font partie du bagage indispensable du programmeur bien
outillé.
Exercice 1
Ecrivez un algorithme qui permette de saisir un nombre quelconque de valeurs, et qui
les range au fur et à mesure dans un tableau. Le programme, une fois la saisie terminée, doit
dire si les éléments du tableau sont tous consécutifs ou non.
16
Par exemple, si le tableau est :
12 13 14 15 16 17 18
9 10 11 15 16 17 18
Exercice 2
Ecrivez un algorithme qui trie un tableau dans l’ordre décroissant.
Vous écrirez bien entendu deux versions de cet algorithme, l'une employant le tri par
sélection, l'autre le tri à bulles.
Exercice 3
Ecrivez un algorithme qui inverse l’ordre des éléments d’un tableau dont on suppose
qu'il a été préalablement saisi (« les premiers seront les derniers… »)
--- corrigé 1 :
17
Sinon
Ecrire "Les nombres ne sont pas consécutifs"
FinSi
Fin
Cette programmation est sans doute la plus spontanée, mais elle présente le défaut d'examiner
la totalité du tableau, même lorsqu'on découvre dès le départ deux éléments non consécutifs. Aussi,
dans le cas d'un grand tableau, est-elle dispendieuse en temps de traitement. Une autre manière de
procéder serait de sortir de la boucle dès que deux éléments non consécutifs sont détectés. La
deuxième partie de l'algorithme deviendrait donc :
i ← 1
TantQue T[i] = T[i – 1] + 1 et i < Nb - 1
i ← i + 1
FinTantQue
Si T[i] = T[i – 1] + 1 Alors
Ecrire "Les nombres sont consécutifs"
Sinon
Ecrire "Les nombres ne sont pas consécutifs"
FinSi
--- corrigé 2 :
On suppose que N est le nombre d’éléments du tableau. Tri par insertion :
…
Pour i ← 0 à N - 2
posmaxi = i
Pour j ← i + 1 à N - 1
Si t[j] > t[posmaxi] alors
posmaxi ← j
Finsi
j suivant
temp ← t[posmaxi]
t[posmaxi] ← t[i]
t[i] ← temp
i suivant
Fin
Tri à bulles :
…
Yapermut ← Vrai
TantQue Yapermut
Yapermut ← Faux
Pour i ← 0 à N - 2
Si t[i] < t[i + 1] Alors
18
temp ← t[i]
t[i] ← t[i + 1]
t[i + 1] ← temp
Yapermut ← Vrai
Finsi
i suivant
FinTantQue
Fin
--- corrigé 3 :
…
Pour i ← 0 à (N-1)/2
Temp ← T[i]
T[i] ← T[N-1-i]
T[N-1-i] ← Temp
i suivant
Fin
19
II-Tableaux Multidimensionnels
« Le vrai problème n’est pas de savoir si les machines pensent, mais de savoir si les hommes
pensent » - B.F. Skinner
« La question de savoir si un ordinateur peut penser n'est pas plus intéressante que celle de
savoir si un sous-marin peut nager » - Edgar W. Dijkstra
Ceci n’est pas un dérèglement de votre téléviseur. Nous contrôlons tout, nous savons
tout, et les phénomènes paranormaux que vous constatez sont dus au fait que vous êtes
passés dans… la quatrième dimension (musique angoissante : « tintintin… »).
Oui, enfin bon, avant d’attaquer la quatrième, on va déjà se coltiner la deuxième.
1. Pourquoi plusieurs dimensions ?
Une seule ne suffisait-elle pas déjà amplement à notre bonheur, me demanderez-vous ?
Certes, répondrai-je, mais vous allez voir qu’avec deux (et davantage encore) c’est
carrément le nirvana.
Prenons le cas de la modélisation d’un jeu de dames, et du déplacement des pions sur
le damier. Je rappelle qu’un pion qui est sur une case blanche peut se déplacer (pour
simplifier) sur les quatre cases blanches adjacentes.
Avec les outils que nous avons abordés jusque là, le plus simple serait évidemment de
modéliser le damier sous la forme d’un tableau. Chaque case est un emplacement du
tableau, qui contient par exemple 0 si elle est vide, et 1 s’il y a un pion. On attribue comme
indices aux cases les numéros 1 à 8 pour la première ligne, 9 à 16 pour la deuxième ligne, et
ainsi de suite jusqu’à 64.
Arrivés à ce stade, les fines mouches du genre de Cyprien L. m'écriront pour faire
remarquer qu'un damier, cela possède 100 cases et non 64, et qu'entre les damiers et les
échiquiers, je me suis joyeusement emmêlé les pédales. A ces fines mouches, je ferai une
double réponse de prof :
2. Si le prof décide contre toute évidence que les damiers font 64 cases, c'est le
prof qui a raison et l'évidence qui a tort. Rompez.
Reprenons. Un pion placé dans la case numéro i, autrement dit la valeur 1 de Cases[i],
peut bouger vers les cases contiguës en diagonale. Cela va nous obliger à de petites
acrobaties intellectuelles : la case située juste au-dessus de la case numéro i ayant comme
indice i-8, les cases valables sont celles d’indice i-7 et i-9. De même, la case située juste en
dessous ayant comme indice i+8, les cases valables sont celles d’indice i+7 et i+9.
Bien sûr, on peut fabriquer tout un programme comme cela, mais le moins qu’on puisse
dire est que cela ne facilite pas la clarté de l’algorithme.
Il serait évidemment plus simple de modéliser un damier par… un damier !
20
L’informatique nous offre la possibilité de déclarer des tableaux dans lesquels les
valeurs ne sont pas repérées par une seule, mais par deux coordonnées.
Un tel tableau se déclare ainsi :
Tableau Cases[7, 7] en Numérique
Cela veut dire : réserve moi un espace de mémoire pour 8 x 8 entiers, et quand j’aurai
besoin de l’une de ces valeurs, je les repèrerai par deux indices (comme à la bataille navale,
ou Excel, la seule différence étant que pour les coordonnées, on n’utilise pas de lettres, juste
des chiffres).
Pour notre problème de dames, les choses vont sérieusement s’éclaircir. La case qui
contient le pion est dorénavant Cases[i, j]. Et les quatre cases disponibles sont Cases[i-1, j-
1], Cases[i-1, j+1], Cases[i+1, j-1] et Cases[i+1, j+1].
REMARQUE ESSENTIELLE :
Il n’y a aucune différence qualitative entre un tableau à deux dimensions [ i, j ] et un tableau
à une dimension [ i * j ]. De même que le jeu de dames qu’on vient d’évoquer, tout problème
qui peut être modélisé d’une manière peut aussi être modélisé de l’autre. Simplement, l’une
ou l’autre de ces techniques correspond plus spontanément à tel ou tel problème, et facilite
donc (ou complique, si on a choisi la mauvaise option) l’écriture et la lisibilité de
l’algorithme.
Une autre remarque : une question classique à propos des tableaux à deux dimensions
est de savoir si le premier indice représente les lignes ou le deuxième les colonnes, ou
l’inverse. Je ne répondrai pas à cette question non parce que j’ai décidé de bouder,
mais parce qu’elle n’a aucun sens. « Lignes » et « Colonnes » sont des concepts graphiques,
visuels, qui s’appliquent à des objets du monde réel ; les indices des tableaux ne sont que
des coordonnées logiques, pointant sur des adresses de mémoire vive. Si cela ne vous
convainc pas, pensez à un jeu de bataille navale classique : les lettres doivent-elles désigner
les lignes et les chiffres les colonnes ? Aucune importance ! Chaque joueur peut même
choisir une convention différente, aucune importance ! L’essentiel est qu’une fois une
convention choisie, un joueur conserve la même tout au long de la partie, bien entendu.
Exercice 1
Écrivez un algorithme remplissant un tableau de 6 sur 13, avec des zéros.
Exercice 2
Quel résultat produira cet algorithme ?
Tableau X[1, 2] en Entier
Variables i, j, val en Entier
Début
Val ← 1
Pour i ← 0 à 1
Pour j ← 0 à 2
X[i, j] ← Val
21
Val ← Val + 1
j Suivant
i Suivant
Pour i ← 0 à 1
Pour j ← 0 à 2
Ecrire X[i, j]
j Suivant
i Suivant
Fin
Exercice 3
Soit un tableau T à deux dimensions [12, 8] préalablement rempli de valeurs
numériques.
Écrire un algorithme qui recherche la plus grande valeur au sein de ce tableau.
--- corrigé 1 :
--- corrigé 2 :
Cet algorithme remplit un tableau de la manière suivante:
X[0, 0] = 1
X[0, 1] = 2
X[0, 2] = 3
X[1, 0] = 4
X[1, 1] = 5
X[1, 2] = 6
Il écrit ensuite ces valeurs à l’écran, dans cet ordre.
--- corrigé 3 :
22
Variables i, j, iMax, jMax en Numérique
Tableau T[12, 8] en Numérique
Le principe de la recherche dans un tableau à deux dimensions est strictement le même
que dans un tableau à une dimension, ce qui ne doit pas nous étonner. La seule chose qui
change, c'est qu'ici le balayage requiert deux boucles imbriquées, au lieu d'une seule.
Debut
...
iMax ← 0
jMax ← 0
Pour i ← 0 à 12
Pour j ← 0 à 8
Si T[i,j] > T[iMax,jMax] Alors
iMax ← i
jMax ← j
FinSi
j Suivant
i Suivant
Ecrire "Le plus grand élément est ", T[iMax, jMax]
Ecrire "Il se trouve aux indices ", iMax, "; ", jMax
Fin
3. Tableaux à n dimensions
Si vous avez compris le principe des tableaux à deux dimensions, sur le fond, il n’y a
aucun problème à passer au maniement de tableaux à trois, quatre, ou pourquoi pas neuf
dimensions. C’est exactement la même chose. Si je déclare un tableau Titi[2, 4, 3, 3], il s’agit
d’un espace mémoire contenant 3 x 5 x 4 x 4 = 240 valeurs. Chaque valeur y est repérée par
quatre coordonnées.
Le principal obstacle au maniement systématique de ces tableaux à plus de trois
dimensions est que le programmeur, quand il conçoit son algorithme, aime bien faire des
petits gribouillis, des dessins immondes, imaginer les boucles dans sa tête, etc. Or, autant il
est facile d’imaginer concrètement un tableau à une dimension, autant cela reste faisable
pour deux dimensions, autant cela devient l’apanage d’une minorité privilégiée pour les
tableaux à trois dimensions (je n’en fais malheureusement pas partie) et hors de portée de
tout mortel au-delà. C’est comme ça, l’esprit humain a du mal à se représenter les choses
dans l’espace, et crie grâce dès qu’il saute dans l’hyperespace (oui, c’est comme ça que ça
s’appelle au delà de trois dimensions).
Donc, pour des raisons uniquement pratiques, les tableaux à plus de trois dimensions
sont rarement utilisés par des programmeurs non matheux (car les matheux, de par leur
formation, ont une fâcheuse propension à manier des espaces à n dimensions comme qui
rigole, mais ce sont bien les seuls, et laissons les dans leur coin, c’est pas des gens comme
nous).
23
C HAP 2. C ALCULEZ LA COMPLEXITE
ALGORITHMIQUE
Vous avez pu voir qu’un algorithme peut être créé de mille et une façons. Mais
comment pouvez-vous savoir si votre algorithme est plus efficace qu’un autre, ou
même savoir s’il est performant ?
Pour cela, vous allez devoir analyser la complexité de votre algorithme. Ainsi, vous
pourrez conclure que cette complexité est convenable, ou la comparer avec d'autres
algorithmes.
Avant d’entrer dans le détail de son calcul, laissez-moi vous conter une petite
histoire.
Vous soulevez le coffre et tombez nez à nez avec un cadenas à trois chiffres, en
fonte, bien décidé à ne pas vous laisser accéder au trésor tant escompté. Quand
bien même ! Nous décidons de relever le défi et de tester rapidement, une à une,
toutes les combinaisons.
Plutôt que de les tester dans le désordre, car il serait impossible de se souvenir de
toutes les tentatives, nous réfléchissons à différentes stratégies. L’une d’elles
consiste à essayer le nombre le plus petit (000) puis le nombre suivant (001) et
ainsi de suite jusqu’à atteindre 999. En effet, nous calculons que cette stratégie nous
prendra, dans le pire des cas, 30 minutes.
24
Effectivement, quelques minutes plus tard, le cadenas s’ouvre. Hourra ! Le code était
123. Facile ! Notre stratégie était efficace et nous nous félicitons d’être si intelligents.
Algorithme cadenas_3_chiffres
Début
Pour i allant de 0 jusqu’à 9 :
Pour j allant de 0 jusqu’à 9 :
Pour k allant de 0 jusqu’à 9 :
Si code == combinaison :
Arrêter les boucles
Fin Si
Fin Pour
Fin Pour
Fin Pour
Fin
Forts de notre premier succès, nous nous basons sur le même algorithme pour
trouver le code. Au bout d’une heure et demie, las, nous abandonnons l’idée. C’est
le plus raisonnable : tester toutes les combinaisons possibles aurait pris plus de
5 heures.
Nous reprenons notre marche, le petit coffre sous le bras, et partons dans un grand
débat : comment en est-on arrivés là ? Comment le temps de calcul peut-il passer de
30 minutes à 5 heures en ajoutant un simple chiffre ? Le secret, c’est la complexité
de notre algorithme.
Au risque de ne pas vous surprendre, plus il y a de chiffres dans le code, plus il sera
long à trouver. Mais plus long comment ?
Si le code a 3 chiffres, il faut tester 1 000 combinaisons (eh oui, tous les nombres
entre 000 et 999). En revanche, s’il en a 4, il faut en tester 10 000. S’il en avait eu 5,
il aurait fallu en tester 100 000, et ainsi de suite. Nous nous apercevons donc
que notre algorithme d’ouverture de coffre dépend du nombre de chiffres du
code.
Plus précisément, le temps mis à l’ouvrir est multiplié par 10 à chaque fois que l’on
ajoute un chiffre : 10 pour un code à 1 chiffre, 100 (10 x 10) pour un code à 2
chiffres, 1 000 (10 x 10 x 10) pour un code à trois chiffres, et ainsi de suite.
25
Si l’on note n le nombre de chiffres de notre cadenas, le temps de calcul est
donc 10n ; ainsi, à l’aide de la notion big O, nous pouvons noter que la complexité
temporelle de notre algorithme est de O( 10n).
On dit alors que la complexité est exponentielle. Très vite, nous nous sommes
rendu compte que notre algorithme était impossible à réaliser, car il devenait trop
long.
Après avoir disserté sur ce sujet, nous avons soudain une idée : et si nous arrêtions
de calculer et faisions appel à Bill, notre cousin, celui qui trempe dans des affaires
louches ? Il nous a raconté un jour qu’il savait "écouter les cadenas". Sa technique
est simple : il tourne la molette du premier chiffre jusqu’à entendre un "clic". Il
sait alors que le chiffre est bon et passe au suivant. Il peut donc trouver les bons
chiffres un par un, sans avoir à se soucier des autres.
Évidemment, le jour où il vous a exposé sa théorie vous avez bien ri. À présent, vous
êtes dubitatif : vous avez passé deux heures à essayer d’ouvrir des coffres de
pirates, alors bon, pourquoi ne pas tenter ?
Au bout de deux minutes, le cadenas à 4 chiffres est ouvert. Youpi ! À présent, nous
ouvrons le coffre et en découvrons le contenu. Il est à la hauteur de nos efforts !
Mais comment peut-on aller si vite, alors que nous avons exactement le même
nombre de chiffres sur le cadenas ?
C’est très simple ! Afin de trouver le premier chiffre du code, Bill tente
10 combinaisons. Quand il l’a trouvé, il passe au suivant et teste de nouveau
10 combinaisons. Et ainsi de suite pour chaque chiffre !
26
Fin Si
Fin Pour
Si l’on y regarde de plus près, Bill teste 10 nouvelles combinaisons pour chaque
nouveau chiffre du cadenas. Autrement dit, si n est le nombre de chiffres, il teste
10 x n combinaisons. Ainsi avec la notation big O, nous pouvons dire que cet
algorithme a une complexité de O(10xn). La "complexité" de son algorithme était
bien meilleure que la nôtre.
Nous appelons cela une complexité linéaire. Pourquoi ? Car elle
augmente proportionnellement au nombre de chiffres.
Tout contents, nous allons rendre visite à Bill pour lui montrer le cadenas et crâner
un peu. Pour sûr, il sera impressionné !
Bill vous adresse un regard circonspect. "Vous voulez vous amuser ? Voilà de quoi
faire !", vous dit-il, sortant d’un placard un cadenas à 500 chiffres qui manque de
faire écrouler la table de la salle à manger. "Il faut tester 5 000 combinaisons, ce qui
prendra environ 3 heures. Enfin... Si vous êtes rapides."
Mince, nous nous retrouvons dans le même problème que tout à l’heure. Aussi
efficace que soit sa technique, le nombre de chiffres du cadenas est trop important.
Autrement dit, la taille des données du problème excède la capacité de son
algorithme.
27
C’est là que passe Jack, votre neveu. Il rigole, prend une pierre et tape sur le
cadenas. Paf ! Ce dernier s’ouvre d’un coup sec. Effectivement... Cela paraît si
simple, maintenant que l’on y pense…
Si l’on s’y attarde de plus près, son algorithme est sacrément efficace ! Quel que
soit le nombre de chiffres, il prend toujours le même temps. Balèze !
Si nous comparons nos différents algorithmes, nous nous rendons compte que nous
avons surtout pris en compte le facteur temps : notre première solution nous a pris
30 minutes quand celle de Jack ne demande qu’une seconde.
Il en sera de même pour chaque algorithme, que ce soit le tri d’une liste, la
décomposition en facteurs premiers ou encore la recherche du cours le plus suivi sur
Coursera. Nous parlons alors de complexité temporelle.
Si nous avions utilisé notre algorithme naïf pour ouvrir le cadenas à 500 chiffres, un
milliard d’années ne nous auraient pas suffi à trouver la bonne combinaison. Inutile
de dire que nous serions morts, réincarnés et re-morts avant de pouvoir profiter du
contenu derrière le cadenas.
Internet nous offre une autre application très importante de ce concept. Imaginons un
site de restauration à emporter. Que se passe-t-il lorsque vous cherchez tous les
restaurants asiatiques à 1 km de chez vous, ouverts jusqu’à 23 heures et qui livrent à
domicile ? L’algorithme va tout faire pour trouver le résultat de votre recherche.
Malgré cela, vous n’avez pas envie d’attendre 3 heures. Vous souhaitez que le site
affiche la page de résultats en une seconde ou deux, pas plus. Il est donc primordial
de trouver l’algorithme le plus efficace qui soit.
28
On parle ici de complexité spatiale (en espace). Les notations big O sont
exactement les mêmes que pour la complexité temporelle.
À vous de jouer
Contexte
Tout au long de ce cours, vous avez pu créer des algorithmes pour le labyrinthe.
Nous allons en prendre quelques-uns et calculer la complexité temporelle et spatiale
de ceux-ci.
Consigne
Nous allons calculé la complexité temporelle et spatiale de 3 algorithmes.
29
• Nous avons ensuite l’algorithme suivant qui permet de ramasser un nombre n
de clés dans le labyrinthe :
Algorithme ramasser
Variable
joueur_position_x ← 0 : ENTIER
joueur_position_y ← 0 : ENTIER
arrivée_position_x ← 5 : ENTIER
arrivée_position_y ← 5 : ENTIER
clés[n] : TABLEAU CHAÎNE DE CARACTÈRES
déplacement ← 0 : ENTIER
max_déplacement ← 30n : ENTIER
Début
Tant Que joueur_position_x != arrivée_position_x ET joueur_position_y != arrivée_position_y ET
clés.taille() != n ET déplacement < max_déplacement :
déplacement(joueur_position_x, joueur_position_y)
ramasser(clés, joueur_position_x, joueur_position_y)
Fin Tant Que
Fin
Votre objectif :
• Mesurer la complexité temporelle et spatiale des 3 algorithmes :
o déplacement ;
o ramassage ;
o tri.
• La fonction déplacement :
o complexité temporelle : O(1) .
La complexité est constante, il n’y a aucune boucle ni aucune fonction
récursive.
o complexité spatiale : O(1).
La mémoire n’est pas affectée par cet algorithme.
30
• La fonction ramasser :
o complexité temporelle : O(30n).
Au pire des cas, la boucle va itérer 30 x n fois.
o complexité spatiale : O(n).
La complexité est linéaire, le tableau aura au maximum n cases.
• La fonction tri :
o complexité temporelle : O(n2).
Il y a deux boucles imbriquées qui itèrent n fois, donc nous avons au
maximum n x n itérations.
o complexité spatiale : O(1).
La mémoire n’est pas affectée par cet algorithme, car la taille du
tableau ne change pas.
En résumé
• La complexité d'un algorithme est une mesure de la quantité de temps et/ou
d'espace requise par un algorithme.
• La complexité temporelle est le temps nécessaire à l'exécution d'un
algorithme, en fonction de la longueur des données en entrée.
• La complexité spatiale mesure la quantité totale de mémoire dont un
algorithme ou une opération a besoin pour s'exécuter en fonction de la
longueur de données en entrée.
• La notation big O est une notation standard pour décrire la complexité d’un
algorithme.
C’est bientôt terminé, il ne reste plus qu’un chapitre. Je vous propose de voir un
concept assez avancé de l’informatique, qu’on appelle la récursivité. Je vous attends
avec impatience dans ce dernier chapitre.
31
C HAP 3. V OYEZ LE MONDE AUTREMENT
AVEC LA RECURSIVITE
En programmation, nous avons vu que nous pouvions utiliser des boucles pour
répéter une opération. Nous les appelons des boucles itératives. Laissez-moi vous
présenter un nouveau concept : la récursivité.
Il s’agit d’un concept un peu spécial. Avez-vous déjà rêvé que vous rêviez ? Vous y
êtes ! Nous allons parler de concepts qui s’appellent eux-mêmes. C’est parti !
Les poupées russes sont une excellente métaphore de la récursivité. Si vous n'avez
jamais joué avec des poupées russes, elles sont exactement ce à quoi elles
ressemblent. Chaque poupée est la même sauf pour sa taille ; elles s'ouvrent
chacune, et la poupée à l'intérieur est de plus en plus petite jusqu'à ce que vous
arriviez au plus petit enfant, qui ne s'ouvre pas. Lorsque vous atteignez le plus petit
enfant, vous inversez le processus en fermant chaque poupée une par une dans
l'ordre inverse.
Par ailleurs, nous utilisons tous les jours la récursivité lorsque nous définissons des
mots ! En effet, nous utilisons des mots pour en définir d’autres, eux-mêmes étant
définis par d’autres mots !
La récursivité en programmation
En programmation, il s’agit d’une fonction qui fait référence à elle-même. Deux
fonctions peuvent s’appeler l’une l’autre, on parle alors de récursivité croisée.
Nous devons tout d’abord créer la fonction qui prend en paramètre une poupée,
qu’elle contienne ou non une autre poupée :
Algorithme nombre_de_poupées(poupée)
Début
Fin
Nous pouvons maintenant construire le corps de la fonction. Si la poupée en
paramètre contient une autre poupée, nous retournons 1 pour compter cette poupée,
32
et nous appelons de manière récursive la même fonction pour vérifier si l’autre
poupée contient aussi une autre poupée.
Algorithme nombre_de_poupées(poupée)
Début
Si poupée contient une autre poupée :
Retourner 1 + nombre_de_poupées(poupée à l’intérieur)
Fin Si
Fin
Pour terminer, nous devons trouver un moyen d’arrêter cette fonction récursive,
sinon nous aurons une boucle infinie. Ainsi, si la poupée ne contient pas de
poupée, nous allons retourner 1, simplement pour comptabiliser cette poupée.
Comme pour les boucles, il faut toujours penser à une condition qui
permette d’arrêter les appels récursifs.
La suite de Fibonacci
Avez-vous déjà passé un entretien d’embauche technique pour travailler en tant que
développeur·se, par exemple ? Si oui, la suite de Fibonacci vous parlera
certainement ! Il s’agit d’un des problèmes les plus couramment posés en entretien.
La suite de Fibonacci est une liste de nombres entiers. Elle commence généralement
par les nombres 0 et 1 (parfois 1 et 1). On appelle un nombre de cette liste un terme.
Chaque terme est la somme des deux termes qui le précèdent.
33
Pour résoudre ce problème, nous allons utiliser une méthode récursive et une
méthode itérative, afin que vous puissiez comparer les deux possibilités !
Quand nous calculons la valeur d’un nombre, nous réalisons les trois opérations
suivantes :
34
Peut-on trouver une manière moins consommatrice de le calculer ?
Nous avons sans doute une solution toute trouvée avec un algorithme linéaire !
À présent, chaque nouveau nombre n’a plus besoin de deux niveaux d’opération
pour le générer, mais d’un seul. Il est donc linéaire et sa complexité est de O(n).
Nous voyons ici que, dans bien des cas, nous préférerons la version itérative, car elle
est moins consommatrice en mémoire. La récursivité n’est pas la solution à tous les
problèmes.
Vous vous rappelez quand nous avons décomposé l’appel de chaque fonction
récursivement pour le cas de la suite de Fibonacci ? Nous avons pu identifier l'ordre
d’appel de chaque fonction pour identifier le résultat final de ces appels récursifs. En
fait, l’ordinateur va automatiquement empiler chaque appel de fonctions afin de
connaître l’ordre des appels, et avoir des informations au sujet des
fonctions actives. C’est ce qu’on appelle la pile d’exécution.
35
L’ordinateur doit retenir le résultat de tous les calculs récursifs avant de finir sa
boucle. Il utilise alors ce que nous appelons une "pile d’exécution" (en
anglais, stack). Celle-ci est gérée automatiquement par le système.
Algorithme main()
Variable
joueur_position_x ← 0 : ENTIER
joueur_position_y ← 0 : ENTIER
arrivée_position_x ← 5 : ENTIER
arrivée_position_y ← 5 : ENTIER
clés[3] : TABLEAU CHAÎNE DE CARACTÈRES
Début
Tant Que joueur_position_x != arrivée_position_x ET joueur_position_y != arrivée_position_y ET
clés.taille() != 3:
déplacement(joueur_position_x, joueur_position_y)
ramasser(clés, joueur_position_x, joueur_position_y)
Fin Tant Que
Fin
Lorsque la fonction main est appelée, on la place dans la pile d’exécution :
main()
et elle y est tant qu’elle est active, c’est-à-dire qu’elle n’est pas terminée.
main()
Et la fonction ramasser , de la même manière, sera dépilée lorsqu’elle sera
terminée, et ainsi de suite tant que la boucle est active. Lorsqu’elle se termine, la
fonction main se termine aussi. Et donc on retire aussi la fonction main de la pile
d’exécution. Quand la pile est vide, le programme est terminé !
La pile d’exécution peut aussi se noter "pile d’appels".
Dans le cas d’un appel récursif, c’est exactement ce qui se passe !
36
Algorithme compteur(n)
Si n > 0 :
compteur(n - 1)
Fin Si
Afficher n
Fin
empile
Lorsque nous appelons la fonction compteur avec n=2 , nous avons la pile
d’appels suivante :
empile compteur(2) empile compteur(1) empile compteur(0)
compteur(2) compteur(1)
compteur(2)
Tant qu’il y a des appels récursifs de la fonction compteur , on les empile dans la
pile d’exécution.
dépile
Lorsque la dernière fonction récursive est appelée, l’ordinateur "dépile" l’ensemble
des fonctions présentes dans la pile d’exécution. Autrement dit, il va chercher dans la
pile le dernier élément enregistré, et ainsi de suite jusqu’à arriver en bas de la pile.
Bien pratique !
compteur(0) compteur(1)
compteur(1) compteur(2)
compteur(2)
37
À vous de jouer !
Contexte
Pour finaliser notre labyrinthe, nous souhaitons ajouter une dernière fonctionnalité.
Nous allons créer des cases pièges, qui bloqueront le joueur pendant 10 secondes à
chaque fois que le joueur passera dessus.
Consigne
Vous allez pour cela créer une fonction récursive qui bloquera le joueur pendant n
secondes, et qui affichera un compte à rebours du temps restant pour être débloqué.
Vous appellerez cette fonction freeze .
Considérez que la fonction attendre permet de mettre en pause le jeu pendant un
certain nombre de secondes. Le nombre de secondes est passé en paramètre de la
fonction.
Votre objectif :
• Définir une fonction freeze qui prend en paramètre le nombre n de
secondes à bloquer.
• Afficher le nombre de secondes restantes.
• Mettre en pause le jeu 1 seconde.
• Appeler récursivement la fonction freeze .
• Spécifier la condition d’arrêt de la fonction récursive.
Afficher n
attendre(1)
freeze(n - 1)
Fin
En résumé
38
• Une fonction récursive est une fonction qui s'appelle elle-même pendant son
exécution.
• Les étapes pour construire une fonction récursive :
o Décomposer le problème en un ou plusieurs sous-problèmes du même
type. On résout les sous-problèmes par des appels récursifs.
o Les sous-problèmes doivent être de taille plus petite que le problème
initial.
o Enfin, la décomposition doit en fin de compte conduire à un élémentaire
qui, lui, n’est pas décomposé en sous-problèmes (c’est la condition
d’arrêt).
• La pile d'exécution sert à enregistrer des informations au sujet des fonctions
actives dans un programme informatique.
C’est bon, c’est promis, cette fois c’est vraiment terminé ! Bravo pour l'ensemble de
votre travail tout au long du cours. Et je vous dis à tout de suite dans le dernier
dernier quiz de ce cours pour valider vos connaissances. Je sais que vous adorez les
quiz !
39