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.