La notation polonaise inverse I. Exemple introductif
Transcription
La notation polonaise inverse I. Exemple introductif
5 La notation polonaise inverse La notation polonaise inverse (NPI, ou RPN en anglais pour reverse polish notation) permet de représenter des formules sans utiliser de parenthèses. Elle est inspirée de la notation polonaise inventée par le mathématicien Jan Łukasiewicz. Ce problème a pour but d’étudier cette notation et de montrer comment on peut ensuite effectuer les calculs à l’aide d’une pile. Remarque préliminaire. Une pile d’entiers est une structure de données permettant de stocker des entiers et d’effectuer les opérations suivantes en temps constant (indépendant de la taille de la pile) : — créer une nouvelle pile vide, — déterminer si la pile est vide, — insérer un entier au sommet de la pile, — déterminer la valeur de l’entier au sommet de la pile, — retirer l’entier au sommet de la pile. Nous supposerons fournies les fonctions suivantes, qui réalisent les opérations ci-dessus et s’exécutent chacune en temps constant : — pile_vide(), qui ne prend pas d’argument et renvoie une pile vide, — est_vide(P), qui prend une pile P en argument et renvoie True ou False suivant que P est vide ou non, — empiler(P,x), qui prend une pile P et un entier x en argument, insère x au sommet de P (c’est-à-dire à la fin de la liste), et ne renvoie rien, — depiler(P), qui prend une pile P (supposée non vide) en argument, supprime l’entier au sommet de P (c’est-à-dire à la fin de la liste) et renvoie sa valeur. Dans la suite il est demandé de manipuler les piles uniquement au travers de ces fonctions, sans aucune hypothèse sur la représentation effective des piles en mémoire. I. Exemple introductif Une machine réalise des mesures et fournit les résultats obtenus sous la forme d’une liste. Les mesures sont fournies sous la forme de nombres entiers, cependant il arrive que les quantités mesurées soient des nombres négatifs ou non entiers. La machine procède alors de la manière suivante : http://alexandre.boisseau.free.fr/Prive/WWW/InfoPC/pb_piles.pdf — pour signifier que le résultat de la mesure est −4 par exemple, la machine va placer dans la liste le nombre 4 suivi de la chaîne de caractères "MOINS" ; — pour signifier que le résultat de la mesure est 0.4, la machine va placer dans la liste le nombre 4 suivi de la chaîne de caractères "DIV10". Ces deux opérations sont les seules mais il est possible de les combiner. Par exemple la liste : [5,"DIV10",4,8,"DIV10","MOINS",3,"MOINS"] représente la liste de mesures successives [0.5,4,-0.8,-3]. Question 1. Écrire la liste correspondant à la liste de mesures [-2,-4,0.5]. Question 2. Expliquer à quelle liste de mesures correspond à la liste : [2,"MOINS","DIV10","DIV10",4,"MOINS","MOINS",12] Question 3. Écrire une fonction mesures(L) qui retrouve et renvoie les mesures à partir de la liste L fournie par la machine. On procèdera de la manière suivante : • On définit une pile P, initialement vide ; • On parcourt tous les éléments de la liste L et : — lorsque l’on rencontre un nombre, on le place sur la pile P, — lorsque l’on rencontre la chaîne "MOINS", on dépile le sommet de P et on empile son opposé, — lorsque l’on rencontre la chaîne "DIV10", on dépile le sommet de P et on empile sa valeur divisée par 10. On notera que la liste L commence nécessairement par un nombre. II. Expressions NPI Dans les expressions mathématiques usuelles, les opérateurs binaires (comme +, ×, etc.) sont placés entre les opérandes (les nombres sur lesquels ils agissent). On écrit donc 3 + 4 pour désigner la somme du nombre 3 et du nombre 4. Pour réaliser plusieurs opérations, on utilise habituellement des parenthèses pour dire dans quel ordre elles doivent être effectuées (ou alors on utilise les priorités habituelles des opérations). Par exemple : — (3 + 4) × 5 : on effectue d’abord 3 + 4 puis on multiplie par 5 ; — 3+4×5 : faut il comprendre cette formule comme (3+4)×5 ou 3+(4×5) ? La multiplication est prioritaire sur l’addition, la bonne interprétation est donc 3 + (4 × 5) ; — 3+4+5 : faut-il comprendre cette formule comme (3+4)+5 ou 3+(4+5) ? Peu importe ici car les deux donnent le même résultat ; — 3 − 4 − 5 : faut-il comprendre cette formule comme (3 − 4) − 5 ou 3 − (4 − 5) ? Ici c’est important car les deux ne donnent pas le même résultat. La convention est d’effectuer les opérations à partir de la gauche, la bonne interprétation est donc (3 − 4) − 5. Une possibilité pour éviter les parenthèse est d’écrire les formules sous forme d’arbres. Les opérateurs sont situés aux nœuds de l’arbre et de chaque opérateur partent deux branches désignant les opérandes : (3 + 4) × 5 3 + (4 × 5) × 3 + 5 + 3+4+5 4 3 + × 4 5 3 + 5 + 4 3−4−5 3 − + ou 3 − (4 − 5) 4 5 − 5 3 − 3 4 − 4 5 Question 4. Représenter la formule (3 + 4) ∗ 5 − 6 + 2 par un arbre. Quel est le résultat du calcul décrit par l’arbre suivant : × − + − −4 −1 3 × 2 12 × 4 6 On peut représenter les arbres sur ordinateur mais ce n’est pas le plus simple. La NPI combine deux avantages : les formules peuvent s’écrire sur une seule ligne et sans parenthèses. En NPI, les opérateurs suivent les opérandes : — (3 + 4) × 5 s’écrit 3 4 + 5 ×; — 3 + (4 × 5) s’écrit 3 4 5 × +; — 3 + 4 + 5 s’écrit 3 4 + 5 +; — 3 − 4 − 5 s’écrit 3 4 − 5 −; — 3 − (4 − 5) s’écrit 3 4 5 − −. Question 5. Écrire la formule 10×(3+4)−5 en NPI. Déterminer le résultat du calcul décrit par la formule 12 4 + 12 4 − ×. Dans la suite, une expression en NPI est représentée en P YTHON par une liste dont les éléments sont des chaînes de caractères qui représentent soit des opérations ("+", "-", "*" et "/") soit des nombres. Par exemple la liste ["3","4","+","5","*"] représente l’expression 3 4 + 5 ×. III. Évaluation d’une expression en NPI L’évaluation d’une expression NPI se fait à l’aide d’une pile. Le principe est le suivant : on parcourt les éléments de la liste de la gauche vers la droite. Pour chaque chaîne de caractères que l’on rencontre, on a deux possibilités : — soit cette chaîne représente une opération. Dans ce cas, on dépile deux éléments, on applique l’opération et on empile le résultat ; — soit cette chaîne représente un nombre. Dans ce cas, on empile sa valeur. Question 6. Écrire la fonction evaluer_npi(L) qui calcule le résultat de l’expression NPI représentée par la liste L. Remarque : comme le montre l’exemple ci-dessous, il est très facile de transformer une chaîne de caractères en un nombre. >>> s = "3.2" >>> print(type(s)) <class ’str’> >>> x = float(s) >>> print(x) 3.2 >>> print(type(x)) <class ’float’> Question 7. Modifier la fonction précédente afin que l’expression NPI puisse également contenir les chaînes de caractères "exp" et "ln" correspondant aux fonctions exp et ln (attention : elle n’ont qu’un seul argument) et l’opérateur "^" (calcul de puissance, avec deux arguments). IV. Travaux pratiques Question 8. Programmer la fonction evaluer_npi(L) et la tester sur les exemples qui ont été vus au cours de ce problème (ou sur les exemples que vous voulez). Question 9. Écrire une fonction evaluer_npi_chaine(s) qui prend en paramètre une chaîne de caractères s qui représente une expression NPI (par exemple s peut être la chaîne de caractères "3 4 + 5 *") et renvoie le résultat du calcul. On pourra pour cela utiliser la fonction présentée dans l’annexe 1. Question 10. Écrire une fonction calculatrice_npi() qui demande une chaîne de caractères à l’utilisateur représentant une expression NPI, calcule le résultat de cette expression, l’affiche et recommence (la fonction s’arrête lorsque la chaîne de caractères entrée est vide). Question 11. Modifier la fonction evaluer_npi pour qu’elle puisse prendre en compte d’autres fonctions (trigonométrie,...) et d’autres opérateurs (division euclidienne,...). Faire des tests pour vérifier le fonctionnement. Question 12. Modifier la fonction evaluer_npi pour qu’elle accepte les opérations "dup" qui duplique l’élément situé en tête de pile et "swap" qui échange les deux éléments situés au sommet de la pile. Expliquer comment calculer sin(π/12)/(π/12) à l’aide de cette fonction. Question 13. On veut maintenant ajouter la possibilité d’utiliser des mémoires. Pour simplifier, on considère uniquement deux emplacements mémoire M 1 et M 2 avec les instructions ">M1" qui retire l’élément situé au sommet de la pile et l’enregistre dans la mémoire M 1 et "M1" qui place au sommet de la pile l’élément contenu dans la mémoire M 1 . On aura de manière analogue les commandes ">M2" et "M2". Programmer ces nouvelles instructions et expliquer comment utiliser les mémoires pour calculer sin(π/12)/(π/12). Question 14. Modifier le programme pour gérer un nombre quelconque d’emplacements mémoire. Il faudra donc prendre en compte les commandes ">Mi" et "Mi" quel que soit l’entier i . Annexe 1 : la fonction split Cette fonction permet de découper une chaînes de caractères en utilisant un séparateur. Exemple en utilisant la console P YTHON : >>> s = "a b c 112 -3" >>> L = s.split() >>> print(L) [’a’, ’b’, ’c’, ’112’, ’-3’] >>> s = "a,b,c,112,-3" >>> L = s.split(",") >>> print(L) [’a’, ’b’, ’c’, ’112’, ’-3’] >>> x=int(L[3]) >>> y=int(L[4]) >>> print(x+y) 109 L’aide de P YTHON pour la commande split : string.split = split(s, sep=None, maxsplit=-1) split(s [,sep [,maxsplit]]) -> list of strings Return a list of the words in the string s, using sep as the delimiter string. If maxsplit is given, splits at no more than maxsplit places (resulting in at most maxsplit+1 words). If sep is not specified or is None, any whitespace string is a separator. (split and splitfields are synonymous) Corrections Q 3. On suit l’algorithme donné dans l’énoncé : exec(open(’/home/ba/Enseignement/PC/Python/piles.py’).read()) def mesures(L): P = pile_vide() for x in L: if x=="MOINS": m = depiler(P) empiler(P,-m) elif x=="DIV10": m = depiler(P) empiler(P,m/10) else: empiler(P,x) return P print(mesures([5,"DIV10",4,8,"DIV10","MOINS",3,"MOINS"])) [0.5, 4, -0.8, -3] print(mesures([2,"MOINS","DIV10","DIV10",4,"MOINS","MOINS",12])) [-0.02, 4, 12] Q 6. On applique directement l’algorithme donné dans l’énoncé. def evaluer_npi(L): P = pile_vide() for s in L: if s=="+": x = depiler(P) y = depiler(P) empiler(P,x+y) elif s=="-": x = depiler(P) y = depiler(P) empiler(P,y-x) elif s=="*": x = depiler(P) y = depiler(P) empiler(P,x*y) elif s=="/": x = depiler(P) y = depiler(P) empiler(P,y/x) else: # s représente un nombre empiler(P,float(s)) return depiler(P) On a supposé que l’expresion donnée est correcte et qu’il n’y aura donc pas de problème lors du calcul. Exemple : print(evaluer_npi(["3","4","+","5","*"])) 35.0 Q 7. On adapte la fonction précédente (on indique simplement ce qui est ajouté). import math def evaluer_npi(L): ... elif s=="^": x = depiler(P) y = depiler(P) empiler(P,y**x) elif s=="exp": x = depiler(P) empiler(P,math.exp(x)) elif s=="ln": x = depiler(P) empiler(P,math.log(x)) print(evaluer_npi(["0","exp","1","ln","+"])) 1.0 Q 9. def evaluer_npi_chaine(s): L = s.split(" ") return evaluer_npi(L) print(evaluer_npi_chaine("3 4 + 5 *")) 35.0 print(evaluer_npi_chaine("3 4 5 * +")) 23.0 print(evaluer_npi_chaine("3 4 + 5 +")) 12.0 print(evaluer_npi_chaine("3 4 - 5 -")) -6.0 print(evaluer_npi_chaine("3 4 5 - -")) 4.0 print(evaluer_npi_chaine("12 4 + 12 4 - *")) 128.0 Q 10. def calculatrice_npi(): s = input("Expression NPI : ") while s!="": print(evaluer_npi_chaine(s)) s = input("Expression NPI : ") Q 12. À nouveau on indique uniquement les différences. def evaluer_npi(L): ... elif s=="dup": x = depiler(P) empiler(P,x) empiler(P,x) elif s=="swap": x = depiler(P) y = depiler(P) empiler(P,x) empiler(P,y) elif s=="pi": empiler(P,math.pi) ... print(evaluer_npi_chaine("pi 12 / dup sin swap /")) 0.9886159294653692 Q 13. def evaluer_npi(L): ... elif s==">M1": m1 = depiler(P) elif s=="M1": empiler(P,m1) elif s==">M2": m2 = depiler(P) elif s=="M2": empiler(P,m2) ... print(evaluer_npi_chaine("pi 12 / >M1 M1 sin M1 /")) 0.9886159294653692 Q 14. Voici une méthode simple mais un peu couteuse en mémoire. Les valeurs contenues dans les espaces mémoire sont enregistrées dans une liste M, l’espace mémoire Mi étant enregistré dans M[i]. Il faudra prendre garde à ce que la liste soit assez grande, en l’agrandissant au besoin. Bien entendu cette méthode n’est pas optimale car si on utilise uniquement la variable M1000 par exemple, il faudra une liste de taille 1001. def evaluer_npi(L): P = pile_vide() M = [] for s in L: ... elif s[0:2]==">M": i = int(s[2:]) while i>=len(M): M.append(0) M[i] = depiler(P) elif s[0]=="M": i = int(s[1:]) empiler(P,M[i]) ... print(evaluer_npi_chaine("pi 12 / >M10 M10 sin M10 /")) 0.9886159294653692 Une méthode plus efficace serait d’enregistrer dans la liste M des couples (i , m), la présence d’un tel couple signifiant que l’espace mémoire Mi contient la valeur m.