Partage
  • Partager sur Facebook
  • Partager sur Twitter

[Exercice][Intermédiaire] Calcul en notation polonaise

Arbre binaire et récursivité

29 août 2010 à 5:43:16

Bonjour,
Je vous propose un exercice que j'ai eu à l'université et que j'ai trouvé assez intéressant.
Il s'agit de lire un calcul en notation polonaise (notation préfixée), de créer un arbre binaire et d'afficher différents résultats à partir de celui-ci.

Les connaissances requises sont :

Le tutoriel sur les arbres binaires est explicité en OCaml et celui sur la récursivité en PHP et OCaml, mais le raisonnement reste le même.

Le programme à créer devra lire un calcul du type:

<math>\(-\quad \times\quad /\quad 15\quad -\quad 7\quad +\quad 1\quad 1\quad 3\quad +\quad 2\quad +\quad 1\quad 1\)</math>

et afficher un résultat comprenant :

  • La pronfondeur maximale de l'arbre (en prenant 1 pour la profondeur du sommet)
  • La valeur maximale dans l'opération (ici 15 donc)
  • Le nombre de noeuds dans l'arbre
  • Le résultat du calcul
  • Le calcul en notation polonaise avec toutes les parenthèses
  • Le calcul en notation infixée avec toutes les parenthèses
  • Le calcul en notation infixée avec seulement les parenthèses nécessaires
  • Un diagramme de l'arbre utilisé
  • Toutes les étapes du calcul


Le résultat devra donc ressembler à ça :

Enter a calculation in prefix notation
- x / 15 - 7 + 1 1 3 + 2 + 1 1

Maximum depth:  6
Number of nodes:  15
Maximum value:  15

Result =  5

Prefix notation with brackets: 
(- (x (/ 15 (- 7 (+ 1 1))) 3) (+ 2 (+ 1 1)))

Infix notation with all brackets: 
(((15 / (7 - (1 + 1))) x 3) - (2 + (1 + 1)))
Infix notation with only necessary brackets: 
15 / (7 - (1 + 1)) x 3 - (2 + 1 + 1)

Diagram:
+-(-)
   +-(x)
   |  +-(/)
   |  |  +- 15
   |  |  +-(-)
   |  |     +- 7
   |  |     +-(+)
   |  |        +- 1
   |  |        +- 1
   |  +- 3
   +-(+)
      +- 2
      +-(+)
         +- 1
         +- 1


Order of operations:
   15 / (7 - (1 + 1)) x 3 - (2 + 1 + 1)
 = 15 / (7 - 2) x 3 - (2 + 1 + 1)
 = 15 / 5 x 3 - (2 + 1 + 1)
 = 3 x 3 - (2 + 1 + 1)
 = 9 - (2 + 1 + 1)
 = 9 - (2 + 2)
 = 9 - 4
 = 5


Le but de l'exercice étant en particulier de s'entrainer sur l'utilisation des arbres binaires et de la récursivité pour les parcourir, je ne pense pas que la gestion des exceptions et erreurs, ainsi que la précision du calcul soient une priorité ici.

Je posterai ma solution dans quelques jours, bien que ça ne soit à mon avis vraiment pas la meilleure, pour laisser les personnes qui le souhaitent la chercher par eux-mêmes.

Si vous avez des questions, n'hésitez pas, je ferai de mon mieux pour y répondre.
Je suis aussi ouvert à toutes remarques, tant que celles-ci sont un tant soit peu constructives.
  • Partager sur Facebook
  • Partager sur Twitter
14 septembre 2010 à 21:07:00

Bon, comme personne n'a l'air très emballé par cet exercice, je vous propose ma solution en Python 3.1, bien que je me doute qu'il doit y avoir beaucoup mieux.
Par ailleurs, étant donné que ça n'était pas pour moi le but de l'exercice, je n'ai fait aucune vérification sur les données entrées par l'utilisateur, donc si l'expression rentrée est fausse, ça donnera au mieux un faux résultat, au pire une erreur.


#!/usr/bin/python

from decimal import Decimal,getcontext

getcontext().prec = 5

#Classe utilisée pour stocker les noeuds de l'arbre
class Node:
    maxValue = 0
    maxDepth = 0
    nodeNumber = 0
    def __init__(self,data,par):
        self.data = data
        self.par = par
        self.right = None
        self.left = None
        if data:
            Node.nodeNumber += 1
        self.isnb = str(data).isdigit()
        if data and self.isnb and self.data > Node.maxValue:
            Node.maxValue = self.data

#Fonction (récursive) pour créer l'arbre
#Paramètres : objet Node, int (parcours de chaine), int (calcul de la profondeur max)
#Renvoie : tuple(Node,int) avec le noeud traité et l'index en cours
def makeTree(n, ind, depth):
    if depth > Node.maxDepth:
        Node.maxDepth = depth
    if ind < len(datas) and not datas[ind].isdigit() and not n.left:
        n.left,ind = makeTree(Node(datas[ind],n),ind + 1,depth + 1)
    if ind < len(datas) and datas[ind].isdigit() and not n.left:
        n.left = Node(int(datas[ind]),n)
        ind += 1
    if ind < len(datas) and datas[ind].isdigit() and not n.right:
        n.right = Node(int(datas[ind]),n)
        ind += 1
    if ind < len(datas) and not datas[ind].isdigit() and not n.right:
        n.right,ind = makeTree(Node(datas[ind],n),ind + 1,depth + 1)
    return n,ind


#Fonction (récursive) pour afficher l'expression préfixe avec parenthèses
#Paramètre : Node (noeud parent pour commencer)
def printBrackets(n):
    if not n.isnb:
        print((not n.par and "(" or " (") + n.data, end = "")
        printBrackets(n.left)
        printBrackets(n.right)
        print(")",end = "")
    else:
        print(" " + str(n.data),end = "")

#Fonction pour juger du besoin des parenthèses 
#Paramètre : Node (noeud à évaluer)
def needBrackets(n):
    if not n.par:
        return False
    if n.data == "-" and not n.right.isnb:
        return True
    if n.par.data in "x/" and n.data in "+-":
        return True
    if n.par.right == n:
        if n.par.data == "/" or (n.par.data == "-" and n.data == "+"):
            return True
    return False

#Fonction (récursive) pour écrire les 2 expressions préfixes
#Paramètres : Node (noeud parent pour commencer), bool (True pour toutes les parenthèses)
def printInfix(n,b):
    if not n.isnb:
        if b or (n.par and needBrackets(n)):
            print("(",end="")
        printInfix(n.left,b)
        print(" ",end="")
    print((n.isnb and n.data < 0 and n.par) and "("+n.data+")" or n.data,end="")
    if not n.isnb:
        print(" ",end="")
        printInfix(n.right,b)
        if b or (n.par and needBrackets(n)):
            print(")",end="")

#Fonction qui renvoie le résultat d'un calcul
#Paramètres : int/float (1er nombre), int/float (deuxième nombre), str (opérateur)
#Renvoie : int avec le résultat de l'opération
def calc(a,b,s):
    if s == "+":
        return a+b
    elif s == "-":
        return a-b
    elif s in "x":
        return a*b
    elif s == "/":
        return Decimal(a)/Decimal(b)

#Fonction (récursive) qui calcul le résultat total
#Paramètre : Node (noeud parent pour commencer)
#Renvoie : int (valeur du noeud en cours)
def calcResult(n):
    if not n.left.isnb and not n.right.isnb:
        return calc(calcResult(n.left),calcResult(n.right),n.data)
    elif not n.left.isnb and n.right.isnb:
        return calc(calcResult(n.left),n.right.data,n.data)
    elif n.left.isnb and not n.right.isnb:
        return calc(n.left.data,calcResult(n.right),n.data)
    else:
        return calc(n.left.data,n.right.data,n.data)

#Fonction (récursive) pour dessiner le schéma de l'arbre
#Paramètres : Node (noeud parent pour commencer), int (nombre d'espaces)
#tableau de bool (évalue le besoin de séparateur)
def printDiag(n,space,sep):
    for i in range(space):
        print(sep[i] and "|  " or "   ",end="")
    if not n.isnb:
        print("+-("+n.data +")")
        space += 1
        if not n.left.isnb:
            sep[space] = True
        printDiag(n.left,space,sep)
        if not n.left.isnb:
            sep[space] = False
        printDiag(n.right,space,sep)
        space -= 1
    else:
        print("+- "+str(n.data))

#Fonction qui écrit le déroulement des opérations en utilisant calcLast en boucle
#Paramètre : Node (noeud parent pour commencer)
def printOrderOfOps(n):
    b = True
    while True:
        print(b and "   " or " = ", end = "")
        printInfix(n,False)
        print()
        if n.isnb:
            break
        n,tmp = calcLast(n,False)
        b = False

#Fonction (récursive) qui calcul le résultat du noeud le plus profond
#Paramètres : Node (noeud parent pour commencer), bool(noeud calculé ou pas à ce tour de boucle)
#Renvoie : tuple(Node,bool) avec le noeud en cours et le booléen cité ci-dessus
def calcLast(n,b):
    if n.left.isnb and n.right.isnb and not b:
        n.data = calc(n.left.data,n.right.data,n.data)
        n.isnb,b = True,True
    elif n.left.isnb and not n.right.isnb:
        n.right,b = calcLast(n.right,b)
    elif not n.left.isnb and n.right.isnb:
        n.left,b = calcLast(n.left,b)
    elif not n.left.isnb and not n.right.isnb:
        n.left,b = calcLast(n.left,b)
        n.right,b = calcLast(n.right,b)
    return n,b

#Fonction qui appelle toutes les fonctions ci-dessus et met le résultat en forme
#Paramètre : Node (noeud parent)
def printResults(n):
    print("\nMaximum depth: ", Node.maxDepth)
    print("Number of nodes: ", Node.nodeNumber)
    print("Maximum value: ", Node.maxValue)
    print("\nResult = ", calcResult(root), end = "\n\n")
    print("Prefix notation with brackets: ")
    printBrackets(root)
    print("\n\nInfix notation with all brackets: ")
    printInfix(root,True)
    print("\nInfix notation with only necessary brackets: ")
    printInfix(root,False)
    sep = [False]*Node.maxDepth
    print("\n\nDiagram:")
    printDiag(root,0,sep)
    print("\n\nOrder of operations:")
    printOrderOfOps(root)

datas = input("\nEnter a calculation in prefix notation\n").rsplit()
root,tmp = makeTree(Node(None,None),0,1)
root,root.par = root.left,None
printResults(root)

  • Partager sur Facebook
  • Partager sur Twitter
18 septembre 2010 à 13:40:37

J'ai décidé de coder une petite solution quand j'ai vu que personne n'avait répondu au sujet. J'espère que ça encouragera d'autres personnes à essayer, même si à mon avis implémenter l'intégralité des fonctionnalités demandées devrait être facultatif.

Ma solution est assez simple, je n'utilise pas de classe mais des tuples pour représenter les arbres. Elle fonctionne avec Python 2.
En théorie elle fonctionne. N'hésitez pas à faire des commentaires.

from operator import add,sub,truediv as div,mul
class Lexerror(Exception): pass
class Parserror(Exception): pass

def lex(ch):
        lexemes,nb=[],None
        for c in ch:
            if c in "+-/*":                
                lexemes.append(c)            
            elif "0"<= c <="9":
                nb = nb + c if nb else c
            elif c!=" ":
                raise Lexerror
            elif nb: 
                    lexemes.append(int(nb))
                    nb=None            
        if nb: lexemes.append(int(nb))
        return lexemes

def parse(lexemes):
        try:
            unite = lexemes[0]
            del lexemes[0]
        except: raise Parserror    
        if isinstance(unite,str):
            return ("Node",unite,parse(lexemes),parse(lexemes))
        else:
            return ("Leaf",unite)

def foldabr(f): #Attend une fonction rendant deux fonctions pour
    fLeaf,fNode = f() #traiter chacun des cas d'arbres possibles
    def aux(abr):
        if abr[0]=="Leaf":
            return fLeaf(abr[1])
        op,opa1,opa2 = abr[1],aux(abr[2]),aux(abr[3])        
        return fNode(op,opa1,opa2)
    return aux

@foldabr
def evalabr():
    return (lambda v : v), (lambda op,opa1,opa2 : \
        {"+" : add, "-" : sub, "*": mul, "/": div}[op] (opa1,opa2))

@foldabr
def nodes(): #Or leaves...
    return (lambda _ : 1),(lambda op,opa1,opa2: 1+opa1+opa2)

@foldabr
def depth():
    return (lambda _ : 1),(lambda op,opa1,opa2: 1+max(opa1,opa2))

@foldabr
def maxval():
    return (lambda v : v),(lambda op,opa1,opa2: max(opa1,opa2))

@foldabr
def NPbckt():
    return (lambda v : str(v)),(lambda op,opa1,opa2: \
        "("+op+" "+opa1+" "+opa2+")")

@foldabr
def Infixbckt():
    return (lambda v : str(v)),(lambda op,opa1,opa2: \
        "("+opa1+" "+op+" "+opa2+")")

@foldabr
def Infix_():
    def fNode(op,(opa1,p1),(opa2,p2)):
        pop, r = {"+":0,"-":1,"*":2,"/":3}[op], ""
        r+= "("+opa1+")" if pop>1 and p1<2 else opa1        
        r+= " "+op+" "
        r+= "("+opa2+")" if pop>0 and p2<2 or op=="/" and p2<4 else opa2
        return (r,pop)    
    return (lambda v :(str(v),4)),fNode

def Infix(abr): return Infix_(abr)[0]

@foldabr
def diagram():
    def linesadd(add,s): #Surrindente un diagramme s selon une châine add à ajouter
       ligne= s.split("\n")
       return ligne[0]+  ("\n" + "\n".join(map(lambda ch : add+ch[1:],ligne[1:]))\
            if ligne[1:] else "") 
    return (lambda v: "+- "+str(v)),(lambda op,opa1,opa2: \
        "+-("+op+")\n   "+linesadd("   |",opa1)+"\n   "+linesadd("    ",opa2))

def steps(abr):
    def step(abr):        
        if abr[2][0]=="Leaf" and abr[3][0]=="Leaf":
            return ("Leaf",evalabr(abr))
        elif abr[2][0]=="Leaf":
           return ("Node",abr[1],abr[2],step(abr[3])) 
        return ("Node",abr[1],step(abr[2]),abr[3])    
    while abr[0]=="Node":
        print " =",Infix(abr)
        abr=step(abr)
    print " =",Infix(abr)
    
def demo():
    abr = parse(lex(raw_input("Enter a calculation in prefix notation\n")))
    print """\nMaximum depth: %d
Number of nodes: %d
Maximum value: %d

Result = %.2f

Prefix notation with brackets:
%s

Infix notation with all brackets:
%s
Infix notation with only necessary brackets:
%s


Diagram:
%s

Order of operations:
    """ % (depth(abr),nodes(abr),maxval(abr),evalabr(abr),\
           NPbckt(abr),Infixbckt(abr),Infix(abr),diagram(abr))
    steps(abr)

demo()


Malheureusement la syntaxe des décorateurs devient vite assez lourde...
  • Partager sur Facebook
  • Partager sur Twitter
19 septembre 2010 à 14:21:15

Après avoir testé quelques opérations, ton script a l'air de fonctionner très bien. Je trouve qu'il est d'ailleurs beaucoup plus "pythonnesque" que le mien. Je n'avais pas pensé à tout faire avec des tuples et ai donc construit une classe pour représenter l'arbre, mais c'est vrai que c'était peut-être plus simple de faire sans.

Citation : EMC1

même si à mon avis implémenter l'intégralité des fonctionnalités demandées devrait être facultatif.


Evidemment, si certaines personnes souhaitent faire l'exercice sans pour autant avoir le temps ou l'envie d'implémenter toutes les fonctionnalités, ce n'est en aucun cas un problème. Même s'il ne s'agit que de calculer le résultat de l'opération, je pense que ça reste quand même un bon exercice d'entrainement.
  • Partager sur Facebook
  • Partager sur Twitter
19 septembre 2010 à 17:43:41

J'ai fait rapidement cet exercice et j'utilise aussi un arbres représenté par des listes.

J'ai rajouter des fonctions mathématiques. Il est alors possible d'avoir des fonctions de 0 à n variables facilement.
Pour les erreurs, je gère les erreurs lexicals dans mon lexer, les erreurs de syntaxe dans mon parser et les erreurs de domaine dans les fonctions. Cependant, j'ai omis de prendre en compte une erreur de syntaxe : lorsqu'un token est en trop. Je me suis rendu compte puis je trouvais que cela n'avait pas d'incidence sur le calcul (dans 1 + 1 2 on ignore le 2)

Pour le lexer, j'ai pris en compte le fait que chaque tokens soit séparés initialement par un espace (peut être modifié facilement).

Et donc voici le code :
from math import *

def testF(f, x):
	try:
		return f(x)
	except:
		raise Exception('Erreur de domaine')

class token:
	add = (2, lambda x: x[0] + x[1])
	sub = (2, lambda x: x[0] - x[1])
	mul = (2, lambda x: x[0] * x[1])
	div = (2, lambda x: x[0] / x[1])
	pow = (2, lambda x: x[0] ** x[1])
	sqrt = (1, lambda x: sqrt(x[0]))
	neg = (1, lambda x: -x[0])
	cos = (1, lambda x: cos(x[0]))
	sin = (1, lambda x: sin(x[0]))
	tan = (1, lambda x: tan(x[0]))
	cosh = (1, lambda x: cosh(x[0]))
	sinh = (1, lambda x: sinh(x[0]))
	tanh = (1, lambda x: tanh(x[0]))
	
	pi = (0, lambda x: pi)
	e = (0, lambda x: e)
	
	nbr = (None)

def lexer(str):
	lsToken = []
	
	for t in str.split():
		if t == '+':		lsToken += [(token.add, 0)]
		elif t == '-':		lsToken += [(token.sub, 0)]
		elif t == '*':		lsToken += [(token.mul, 0)]
		elif t == '/':		lsToken += [(token.div, 0)]
		elif t == '**':		lsToken += [(token.pow, 0)]
		elif t == 'neg':	lsToken += [(token.neg, 0)]
		elif t == 'cos':	lsToken += [(token.cos, 0)]
		elif t == 'sin':	lsToken += [(token.sin, 0)]
		elif t == 'tan':	lsToken += [(token.tan, 0)]
		elif t == 'e':		lsToken += [(token.e, 0)]
		elif t == 'pi':		lsToken += [(token.pi, 0)]
		elif t == 'sqrt':	lsToken += [(token.sqrt, 0)]
		else:
			try:
				lsToken += [(token.nbr, float(t))]
			except:
				raise Exception('Erreur: Le token `' + t + '` est invalide')
	
	return lsToken

def parser(tokens):
	if not tokens:
		raise Exception('Erreur de syntaxe')
	if tokens[0][0] == token.nbr:
		t = tokens[0]
		del tokens[0]
		return [t[0], [t[1]]]
	else:
		tree = [tokens[0][0], []]
		nbrArgs = tokens[0][0][0]
		del tokens[0]
		for n in xrange(nbrArgs):
			tree[1] += [parser(tokens)]
		
		return tree

def evalExpr(tree):
	if tree[0] == token.nbr:
		return tree[1][0]
	
	args = []
	for n in xrange(tree[0][0]):
		args += [evalExpr(tree[1][n])]
	return testF(tree[0][1], args)

print evalExpr(parser(lexer(raw_input())))


Edit : pour les fonctions, je peux factoriser le code en créant une fonction testant le calcul de la variable par une fonction donné. Ainsi, il ne serait plus obligatoire de définir les fonctions avec un domaine particulier à l'extérieur de la classe.

J'ai modifié le code avec la factorisation et c'est maintenant la fonction de calcul qui vérifie le domaine.
  • Partager sur Facebook
  • Partager sur Twitter
22 septembre 2010 à 18:55:02

Je dois trop bete...

Où est l'interet de la notation polonaise ^^' ?
  • Partager sur Facebook
  • Partager sur Twitter
22 septembre 2010 à 23:31:54

Citation : zazapeta

Où est l'interet de la notation polonaise ^^' ?



Plus simple et plus rapide à taper, et plus simple à parser que la notation à base de ().
Léger inconvénient, les programmes deviennent rapidement illisibles.
  • Partager sur Facebook
  • Partager sur Twitter
23 septembre 2010 à 8:20:02

Citation : zazapeta


Où est l'interet de la notation polonaise ^^' ?


Comme l'a dit Lord Casque Noir, la notation polonaise est plus facile à parser que la notation infixe. L'exercice se voulant de niveau intermédiaire, ça me parait beaucoup plus accessible de parser une expression en notation polonaise qu'en notation infixe, surtout pour en faire un arbre binaire.
Mais bon si tu veux le faire en notation infixe personne ne t'en empêche hein ;)
  • Partager sur Facebook
  • Partager sur Twitter
23 septembre 2010 à 17:54:53

Ok merci des précisions.

Heu pour la notation infixe... je verrais si je m'ennuie un peu.

En tout cas je l'ai noté comme un exercice à faire (un jour) ^^
  • Partager sur Facebook
  • Partager sur Twitter
23 mars 2011 à 12:54:35

Ma petite calculatrice préfixée (minimale).
Par contre, je me suis écarté du sujet : nul parser ou arbre binaire de mon côté, j'ai utilisé une pile pour faire le calcul:

from operator import add, truediv as div, mul, sub
from decimal import Decimal as D, InvalidOperation

class InputError(Exception): pass

def is_operator(elem):
    return elem in ('*','+','/','-')

def lexer(expression):
    '''Prends en arguments l'entree de l'utilisateur
        Retourne un liste d'opérandes et d'opérateurs '''
    list_ops = [i for i in expression.split(' ') if i]
    for ind, elem in enumerate(list_ops):
        try :
            list_ops[ind]=D(elem)
        except InvalidOperation :
            if not is_operator(elem) :
                raise InputError("Symbole "+ str(elem)+" non supporté")
    return list_ops

def calculate (operateur, operande1, operande2):
    if operateur == '*' : return mul(operande1,operande2)
    if operateur == '+' : return add(operande1,operande2)
    if operateur == '-' : return sub(operande2,operande1)
    if operateur == '/' : return div(operande2,operande1)
    
def process (list_ops):
    ''' Prends la liste des opérateurs et opérandes en arguments,
        retourne le resultat du calcul '''
    pile_op= []
    
    for elem in reversed(list_ops):
        if isinstance(elem,D):
            pile_op.append(elem)
        elif len(pile_op) < 2:
            raise InputError("Expression mal formée")
        else:
            res = calculate(elem, pile_op.pop(), pile_op.pop())
            pile_op.append(res)

    if len(pile_op)==1:
        return pile_op.pop()
    else :
        raise InputError("Pas assez d'opérateurs")

def main():
    print("Calculatrice à notation préfixée")
    while True:
        user_input = input("Saisissez une expression à calculer ('q' pour quitter) : \n")
        if user_input == 'q':
            break
        else :
            try :
                print ( process( lexer(user_input) ) )
            except InputError as I:
                print(I)
    print("Au revoir")

if __name__ == "__main__":
    main()
  • Partager sur Facebook
  • Partager sur Twitter
11 janvier 2014 à 12:22:28

slt comment ecrire while(x<3>) do x;=x-1 en notation polonaise prefixe et b:=(x>=(y+1))

merci et cest super urgent

  • Partager sur Facebook
  • Partager sur Twitter
11 janvier 2014 à 15:30:58

Bonjour fricaque,

Tu as répondu à un sujet datant d'il y a plusieurs années.

Merci d'en ouvrir un nouveau si tu souhaite poser une question.

Je ferme celui-ci.

  • Partager sur Facebook
  • Partager sur Twitter