Structure de pile I. Définition........................................... 2 I.1 Introduction..................................... 2 I.2 Opérations caractérisant une structure de pile................... 2 II. Utilisation d une pile..................................... 2 III. Implantation......................................... 3 III.1 Utilisation d un tableau............................... 3 III.2 Utilisation d une liste................................ 4 III.3 Un peu de programmation objet (en xième lecture...)............... 4 2015-2016 1/6
I. Définition I.1 Introduction Dans ce chapitre, on présente la strucure de pile, qui correspond exactement à l image traditionnelle d une pile d assiettes posée sur une table ; en particulier, on ne peut accéder qu au dernier élément ajouté (appelé le sommet de la pile). L image associée à une pile est donc «dernier arrivé, premier sorti»(en anglais last in, first out, parfois abrégé en LIFO). On peut bien sûr aussi imaginer une structure où, comme dans une file d attente, les éléments sortent dans leur ordre d arrivée, le premier élément sorti correspondant à l élément le plus anciennement arrivé (en anglais first in, first out) : on parle alors de structure de file, non au programme! I.2 Opérations caractérisant une structure de pile Une pile est une structure de données munie de quatre opérations, dont l exécution se fait en temps constant : crée_pile() qui crée une pile vide ; empile(p, x) qui empile x sur la pile p ; dépile(p) qui supprime de la pile p le dernier élément empilé, et renvoie sa valeur ; pile_vide(p) qui renvoie un booléen indiquant si la pile est vide ou non. On peut illustrer la structure de pile par l image suivante : pile (stack) empile (push) dépile (pop) Remarque. L exécution de chacune de ces 4 fonctions se fait en temps constant (donc si on appelle au total N fois ces fonctions au cours d un algorithme, la compexité de notre algorithme sera en O(N)). II. Utilisation d une pile Lorsque l on utilise une pile, on ne se préoccupe pas de son implantation, et on n utilise que les quatre fonctions de la définition. Exemple. Définir un fonction echange(p) qui échange l ordre des deux éléments les plus hauts dans la pile p. On peut modifier la fonction précédente pour qu elle déclenche une erreur si p n a pas deux éléments. 2/6 2015-2016
Exemple. Pour les problèmes suivants, la structure de pile est-elle adaptée : gérer un répertoire téléphonique ; sortir d un labyrinthe ; stocker l historique des actions effectuées dans un logiciel, et disposer de la commande undo ; gérer la file d attente d une imprimante connectée à un réseau ; vérifier le parenthésage d une expression algébrique. La pile est une structure de données appropriée quand : on veut stocker des éléments dont le nombre est variable, a fortiori dont le cardinal maximum est inconnu à l avance on peut ou on doit se contenter d accéder au dernier élément stocké. Réciproquement, si on veut pouvoir accéder à un élément quelconque à tout moment, il faudra utiliser un tableau. III. Implantation Plusieurs implantations peuvent être proposées. III.1 Utilisation d un tableau Une première idée peut être d utiliser un tableau, dont la première case contient l indice de la première case disponible dans le tableau. # utilisation de tableau def pile_vide(nbmax=10000): """initialisation d'une pile vide""" pile = np.zeros(nbmax, np.int64) pile[0] = 1 return pile def empile(p, e): """fonction pour empiler l'élément e sur la pile p""" p[p[0]] = e p[0] += 1 def depile(p): """fonction pour dépiler un élément de p""" p[0] -= 1 return p[p[0]] def est_vide(p): """fonction pour tester si une pile est vide ou non""" return p[0] == 1 Remarque. Les opérations dépile et empile modifient le contenu de la pile passée en argument, mais ce n est pas le contenu des «nouvelles» piles qui est renvoyé par ces fonctions ; en particulier, la fonction empile ne renvoie rien. Remarque. Il n est pas proposé ici de gestion des erreurs. Quels sont les avantages et les inconvénients de cette implantation? 2015-2016 3/6
III.2 Utilisation d une liste Une autre implantation naturelle de la pile est celle utilisant une liste, et s appuyant sur les méthodes de listes pop et append. def nouvelle_pile (): """renvoie une pile vide""" return [] def pile_vide (p): """teste si la pile <p> est vide""" return len(p) == 0 def empile (p, x): """empile <x> sur la pile <p>""" p.append(x) def depile (p): """Supprime de la pile <p> le dernier élement empile, renvoie la valeur de cet élement""" return p.pop() Remarque. Il n est pas proposé ici de gestion des erreurs. Quels sont les avantages et les inconvénients de cette implantation? III.3 Un peu de programmation objet (en xième lecture...) On peut définir une nouvelle classe dont les instances seront les piles. class Pile(object): """définition d'une classe d'objets de type pile : dépilage du dernier empilé""" def init (self, nbmax=1000): """création d'une instance de classe""" self.pile = np.zeros(nbmax) self.hauteur = 0 def empile(self, e): """méthode pour empiler l'élément e sur l'instance de classe""" self.pile[self.hauteur] = e self.hauteur += 1 def depile(self): """méthode pour dépiler un élément de l'instance de classe""" self.hauteur -= 1 return self.pile[self.hauteur+1] def est_vide(self): """méthode pour tester si une pile est vide ou non""" return self.hauteur == 0 Remarque. Il n est pas proposé ici de gestion des erreurs. Quels sont les avantages et les inconvénients de cette implantation? On peut proposer d enrichir la classe en ajoutant des méthodes. 4/6 2015-2016
2015-2016 5/6 Exo 1 Un labyrinthe est représenté par un tableau numpy que l on peut charger directement avec l instruction : lab = np.load('pile_3_tableau_labyrinthe.npy') print(lab.shape) # (59, 59) Le tableau est constitué des valeurs 0.0 (représentant un mur) et 1.0 (représentant un couloir). On souhaite savoir s il existe un parcours par les couloirs joignant deux points donnés. On décrit l algorithme à mettre en œuvre de la façon suivante, où une pile est utilisée pour stocker les cases encore à explorer : Partant de la case (i 0, j 0 ) supposée blanche, on grise celle-ci et on l empile. La sortie n étant pas trouvée, on dépile une case. Parmi ses quatre cases voisines potentielles, on grise et on empile celles correspondant à un couloir. (1) Écrire une fonction sortiepossible(lab,i0,j0,i1,j1), qui prend en argument un tableau et deux cases, et qui renvoie le booléen indiquant si on peut rejoindre la case (i 1, j 1 ) depuis la case (i 0, j 0 ). (2) Cet algorithme est-il adapté au tracé d un chemin joignant deux cases d un labyrinthe? Exo 2 Évaluation d une expression algébrique postfixée La notation habituelle, pour une expression algébrique, s appelle la notation infixe : sin(3.14/4) + 1 Cette notation nécessite l utilisation de parenthèses ou de règles de priorités. Une autre notation est la notation postfixe : 3.14 4 / sin 1 + Il s agit simplement d une autre représentation de la même expression algébrique. Définition : On appelle expression postfixée toute expression qui est : un nombre ; une expression postfixée suivie d une fonction ; deux expressions postfixées suivies d un opérateur binaire. Remarque : cette définition est récursive, car on utilise les expressions postfixées pour définir les expressions postfixées. (1) Proposer un algorithme utilisant une pile permettant d évaluer une expression postfixée. (2) On suppose que l expression algébrique est donnée par une chaîne de caractères, les nombres, fonctions et opérateurs étant séparés par des espaces. Comment transformer cette chaîne en la liste des nombres, fonctions, opérateurs? (3) Implanter l algorithme proposée à la première question, en privilégiant une approche modulaire du problème. (4) Vérifier avec l expression postfixée proposée en exemple l ordre de grandeur du résultat obtenu. 2016
Comment gérer des erreurs? assert test, message Ne pas l utiliser à tord et à travers, ça ne remplace pas un if... Si la condition test n est pas satisfaite, l exécution est interrompue, une exception est levée et le texte message s affiche. On préférera utiliser assert à des conditionnelles if lorsque l on voudra vérifier que les arguments d une fonction respectent certaines conditions. Pour comprendre, essayons un parallèle avec un exo de math! Imaginez l exo de math suivant : f = ln, montrer que x 0, f(x) =qqc de compliqué : vous n écrirez pas : si x 0, f(x) n existe pas, mais sinon (i.e. si x > 0) j essaie de faire l exo! Vous direz : il y a une erreur d énoncé, donc je ne cherche pas à faire l exo. Pour assert, c est pareil : si les arguments d une fonction python ne vérifient pas certaines conditions, pas besoin d exécuter cette fonction, ni tout le programme qui en découle (c est voué à l échec dès le départ), donc on interrompt l exécution à la source via le assert. Un exemple concret : on définit 4 fonctions, puis on les teste : def f1(): assert 1 < 2, "erreur f1" return "valeur 1" def f2(): assert 1 > 2, "erreur f2" return "valeur 2" def f3(): if 1 < 2: print ("le test 3 passe") else: print ("le test 3 ne passe pas") return "valeur 3" def f4(): if 1 > 2: print ("le test 4 passe") return "valeur 4" 1er test a1 = f1() print(a1) #valeur 1 a3 = f3() print(a3) #(avec le if, toute f3 #le test 3 passe #valeur 3 exécutée) a4 = f4() print(a4) #(et fonctions suivantes aussi exécutées) #valeur 4 a5 = "coucou" print(a5) #coucou 2ème test : a1 = f1() print(a1) #valeur 1 a3 = f3() print(a3) #(avec le if, toute f3 exécutée) #le test 3 passe #valeur 3 a4 = f4() print(a4) #(et fonctions suivantes aussi exécutées) #valeur 4 a2=f2() print(a2) #(exécution arrêtée) #Traceback (most recent call last): # File "assert.py", line 34, in <module> # a2 = f2() # File "assert.py", line 9, in f2 # assert 1 > 2, "erreur f2" #AssertionError: erreur f2 a5 = "coucou" print(a5) #(fonction suivante pas exécutée) 6/6 2015-2016