Cours d’informatique du 17/11/2009 A. Rappels sur les boucles 1) Rappel sur les invariants de boucle : L’invariant de boucle est un outil pour concevoir cette boucle. Il peut exister plusieurs invariants de boucle pour un même problème. Tout dépend de la manière dont on résout le problème. Cet invariant fixe la signification des variables du problème. Reprenons le problème du tri étudié au cours du 10/11. 0 Zone non triée n Zone triée N-1 Dans ce cas, on dit qu’à partir de n, le tableau est trié. Autrement dit, n marque le début de la section triée. Pour trier complètement le tableau, il faut tout d’abord prendre n = N. Ensuite il faut diminuer n en ne violant pas l’invariant de la boucle. Ici, l’invariant de boucle est : P : ∀{j | n ≤ j < N} : ∀{i | 0 ≤ i ≤ j} : B[i] ≤ B[j] A chaque itération, il faut restituer l’invariant de boucle P. Reprenons l’exemple du programme « Bubble sort » import numpy as np def bubble(B, upto): iMaxChanged = 0 for i in range(1, upto): if B[i-1] > B[i]: # la condition de tri n’est pas respectée # pour B[i-1] et B[i] B[i], B[i-1] = B[i-1], B[i] # la condition du tri est restaurée iMaxChanged = i # Le tableau au dela de i est trié return iMaxChanged N = 10 B = np.random.randint(0, 10, N) print B n=N while n > 0: n = bubble(B, n) print B S0 On remarque que le rôle de la séquence d’instructions S 0 est de rétablir cet invariant de boucle. Avec ce code, on vérifié l’invariant à chaque itération. ! Chaque boucle a son invariant de boucle, même pour les boucles imbriquées ! 2) Fin d’une boucle : Lorsque l’on crée une boucle, il faut vérifier qu’elle se termine réellement à un moment. Pour vérifier cela, il faut vérifier que chaque instruction élémentaire et chaque boucle se termine réellement. Lorsqu’on utilise l’itérateur « for … in … », il n’y a aucun problème. A chaque itération, l’indice augmente. Tant que le problème qu’il faut traiter contient un nombre fini d’éléments, la boucle se termine toujours. En Python, il est donc plus sûr et plus simple d’utiliser un itérateur tel que « for … in … » plutôt que l’instruction « while ». Il est en effet plus clair de voir que la boucle s’arrêtera avec un itérateur. Reprenons comme exemple le problème du « bubble sort » écrit plus haut : Il y a deux boucles dans cet algorithme. • La première boucle avec l’itérateur « for … in … » se termine forcément puisque l’indice i augmente d’une unité à chaque itération. L’indice « upto » étant fini, l’indice i fini par atteindre la valeur « upto – 1 ». La boucle se termine alors. • Il est plus difficile de prouver que la deuxième boucle se termine. Pour le prouver, il suffit de montrer que « iMaxChanged » est toujours plus petit que « upto ». Si le tableau est trié, iMaxChanged = 0, et donc n = 0, ce qui implique que la boucle s’arrête. Tant que le tableau n’est pas totalement trié, on a 1 ≤ i < upto, et donc toute nouvelle valeur de i est inférieure à n-1. La valeur de n diminue donc à chaque itération et fini par être nulle, ce qui arrête la boucle puisque le gardien est falsifié. ! Quand on travaille avec une boucle, il est toujours plus simple de d’abord réfléchir à partir d’un dessin ! B. Problème des chaînes avec parenthèses Commençons à étudier ce problème par quelques exemples : • • • a(b[{c}d])[e] → est correct a(b[{c}d)[e] → est faux puisque le premier crochet n’est pas fermé a(b[{c}d)[e]] → est faux. Cette fois, toutes les parenthèses ouvertes sont fermées mais elles ne le sont pas dans le bon ordre. Seul le premier exemple a une forme correcte. Quel pourrait être l’algorithme qui vérifierait si l’ordre d’ouverture et de fermeture est correct ? Une première approche est de dire qu’on doit avoir des blocs imbriqués et enchaînés pour que l’expression soit correcte. Cela peut se représenter avec un dessin. Voici une idée d’algorithme : On pourrait tout d’abord créer une liste. Ensuite, en parcourant la chaîne de caractère, chaque fois que l’on rencontre une parenthèse ouverte, on l’ajoute à la liste. Et lorsque l’on trouve une parenthèse fermée, on la compare au dernier élément de la liste. S’il s’agit de l’élément correspondant ouvert ( ‘(‘ si on a ‘)’, ‘{‘ si on a ‘}’ et ‘[‘ si on a ‘]’), on supprime cet élément de la liste. S’il s’agit d’un autre élément, il y a une erreur dans l’ordre d’ouverture et de fermeture. Reprenons le premier exemple correct. Voici, étape par étape, l’évolution de la liste : 1:( 5:( 2:([ 6:/ 3:([{ 7:[ 4:([ 8:/ On observe donc qu’on retrouve une liste vide après avoir parcouru tout le tableau. Essayons avec le deuxième exemple : 1:( 2:([ 3:([{ 4:([ 5 : Il y a un problème : on trouve maintenant une parenthèse fermée. Or le crochet précédent n’a pas été fermé. Attention, on pourrait avoir une autre idée : à chaque fois que l’on rencontre une parenthèse ouvert, on incrémente un compteur d’une unité. Si on a une parenthèse fermée, on le décrémenterait. Cet algorithme déterminerait s’il y a autant de parenthèses ouvertes que fermées, mais ne saurait pas dire qi les parenthèses ont été fermées dans le bon ordre. Revenons donc à la première idée. Cet algorithme peut s’apparenter à une pile de livre. Il est facile de prendre ou d’ajouter un livre sur le haut de cette pile. Il est par contre plus difficile d’en insérer au milieu de cette pile. De même dans ce programme, on ne travaille qu’à la fin de la liste. Cette structure de donnée s’appelle donc structure de pile. Cette structure a trois opérations : - push(elem) → ajoute un élément à la liste - elem = pop() → regarde, supprime le dernier élément de la liste et rend celui qu’on regarde - elem = top() → consulte le dernier élément de la liste sans le supprimer Avec ces trois opérations, nous pouvons écrire en deux parties l’algorithme qui déterminera si la chaîne est correctement parenthèsée. stack.py # Stack def newStack(): return [] def push(stack, elem): stack.append(elem) # ajoute l’élément à la liste def pop(stack): # supprime le dernier élément return stack.pop() # pop existe déjà dans Python et supprime le dernier élément de la liste def top(stack): # doit regarder sans supprimer return stack[-1] # regarde le dernier élément parentheses.py import sys import stack # faut un dictionnaire des caractères parens = {'(':')', '[':']', '{':'}'} # crée une pile leftParens = stack.newStack() # appelle la fonction newStack qu'on a importé string = sys.argv[1] for char in string: if char in parens: # si c'est une parenthèse gauche stack.push(leftParens, char) elif char in parens.values(): # si c'est une parenthèse droite # Faut vérifier si pile n'est pas vide if len(leftParens) == 0: print "no left parenthesis corresponding to", char break elif parens[stack.top(leftParens)] == char: # compare au correspondant grace à dico stack.pop(leftParens) else: print stack.pop(leftParens), "does not match", char break # si ce n'est ni parenthèse gauche ni droite, ne fait rien else: # s'exécute si on ne termine pas de boucle prématurément par break p. ex. . Ce else permet donc d’éviter deux messages d’erreur. if len(leftParens): # donne une valeur booléenne, mais on peut écrire if len(leftParens) > 0: print "No right parenthese for : ", leftParens En exécutant le code parentheses.py, on sait si la chaîne est correctement parenthésée ou non.