xml
, ou bien un programme informatique peut être représenté par un arbre.
Cet arbre pourrait représenter le programme suivant :
y = 1337 for i in range(42): y = y * 2 print(y)
L'interpréteur Python, quand il lit un programme, commence par le traduire en arbre syntaxique en suivant la grammaire du langage. Il peut détecter à cette occasion une erreur de syntaxe avant même le début de l'exécution du programme.
Exercice 1 : Chercher lesquels parmi les formats de fichiers suivants peuvent être lus et représentés par un arbre syntaxique.
.odt
).svg
).html
).jpg
)
Cet arbre binaire est une représentation de l'expression .
Exercice 2 : Dessiner un arbre représentant l'expression .
On pourra ne pas dessiner
nil
.
Un nœud contient un élément et des enfants, qui sont d'autres nœuds : zéro, un ou plusieurs nœuds.
Un arbre enraciné est un ensemble fini non vide de nœuds, avec un nœud en particulier : la racine de l'arbre. Les nœuds sont organisés de manière hiérarchique dans un graphe :
Dans un arbre binaire, un nœud contient un élément et deux enfants, qui sont d'autres arbres binaires.
Dans l'exemple précédent,
+
,×
,3
,x
, et les autres sont des nœuds, sauf lesnil
qui représentent l'absence de nœud.
Un arbre binaire est un ensemble fini de nœuds qui ont exactement deux enfants, un à gauche, un à droite ; les enfants sont des nœuds ou éventuellement nil
(une absence de nœud) et que l'on pourra alors omettre.
Une définition récursive est alors :
nil
.L'arbre binaire ci-dessous est un sous-arbre de l'exemple précédent. On a, cette fois, omis les
nil
.
Les arbres binaires sont presque des cas particuliers d'arbres enracinés.
- Un arbre binaire peut être vide, mais pas un arbre enraciné.
- Sinon, un arbre binaire non vide est un exemple d'arbre enraciné.
Un arbre binaire est aussi un graphe connexe, sans cycle.
Les feuilles sont les extrémités de l'arbre.
Dans le premier exemple, 3
, y
, x
, et 2
sont des feuilles.
La taille d'un arbre est son nombre de nœuds.
L'exemple de l'arbre de l'expression littérale est un arbre de taille , dont feuilles. Il y a donc nœuds intérieurs.
⚠️ La définition de hauteur n'est pas la même partout. Vérifier celle du document que vous lisez.
nil
.L'arbre précédent donné en exemple est de hauteur ; la branche la plus profonde possède les nœuds +
, ×
, -
, 3
.
Dans l'autre définition très commune, un arbre vide a pour hauteur , et s'il est non vide, c'est le nombre maximal de liens pour joindre la racine à une feuille.
Même si l'autre est assez répandue, nous préférerons la définition où l'arbre vide est de hauteur nulle. Cette définition et l'autre ne diffère que de .
La hauteur est une notion importante pour le calcul de la complexité d'un algorithme. De nombreux algorithmes ont une complexité en , et ainsi, un décalage de dans la définition est absolument sans importance.
Un arbre binaire peigne est un cas particulier extrême d'arbre binaire, tous les nœuds intérieurs ont un seul enfant qui est non vide, et toujours du même côté. Techniquement c'est une liste chainée.
Un arbre peigne montre une situation extrême où certains algorithmes que nous verrons ne seront pas efficaces. Avec un arbre peigne, la hauteur est égale à la taille ; on ne peut pas obtenir une plus grande hauteur à taille fixée.
Un arbre binaire parfait possède des nœuds intérieurs qui ont tous exactement deux enfants non vides. C'est l'arbre idéal pour certains algorithmes... Une taille maximale pour une hauteur minimale.
Un arbre binaire est équilibré si pour chaque nœud, son sous-arbre gauche et son sous-arbre droit ont une hauteur qui ne diffère que de au plus.
Concrètement, un arbre est équilibré quand tous les nœuds intérieurs ont deux enfants non vides, sauf les plus éloignés qui en ont un ou deux. Techniquement, les nœuds intérieurs forment un arbre parfait, et l'arbre peut devenir parfait uniquement en complétant avec des feuilles au dernier niveau.
Un arbre binaire est presque complet (à gauche) s'il est équilibré et qu'à la profondeur maximale les feuilles sont entassées du même côté (à gauche).
⚠️ les définitions de (presque) complet et équilibré varient parfois, de même en anglais. Vérifier avec le document que vous utilisez.
Prenons par exemple, l'expression numérique qui peut être représentée par l'arbre binaire suivant. (nil
non dessinés.)
Un arbre binaire peut être représenté par un ensemble de nœuds de la forme (enfant_gauche, élément, enfant_droit)
et avec None
pour désigner nil
.
Notre exemple peut être alors représenté par :
six_1 = (None, "6", None) cinq_1 = (None, "5", None) produit_1 = (six_1, "×", cinq_1) trois_1 = (None, "3", None) sept_1 = (None, "7", None) six_2 = (None, "6", None) somme_1 = (sept_1, "+", six_2) somme_2 = (trois_1, "+", somme_1) expr_A = (produit_1, "-", somme_2)
Remarque : on notera la numérotation des nœuds afin d'avoir deux nœuds étiquetés
"six_?"
différents. De même, plusieurs sommes le composent, l'objectif étant de sécuriser l'éventuel partage de données, même si c'est possible en faisant très attention...
Avec cette représentation on peut donner le code de certaines fonctions.
def est_vide(arbre): return arbre is None def somme(arbre): """On suppose ici que l'arbre binaire ne comporte que des valeurs numériques.""" if est_vide(arbre): return 0 else: return somme(arbre[0]) + arbre[1] + somme(arbre[2]) def taille(arbre): if est_vide(arbre): return 0 else: return taille(arbre[0]) + 1 + taille(arbre[2]) def hauteur(arbre): if est_vide(arbre): return 0 else: return 1 + max(hauteur(arbre[0]), hauteur(arbre[2])) def est_peigne_gauche(arbre): if est_vide(arbre): return True else: return est_vide(arbre[2]) and est_peigne_gauche(arbre[0]) # variante une ligne return est_vide(arbre) or (est_vide(arbre[2]) and est_peigne_gauche(arbre[0])) # l'évaluation paresseuse permet de rester efficace, on traîte le cas difficile en dernier ! if __name__ == '__main__': # test six_1 = (None, "6", None) cinq_1 = (None, "5", None) produit_1 = (six_1, "×", cinq_1) trois_1 = (None, "3", None) sept_1 = (None, "7", None) six_2 = (None, "6", None) somme_1 = (sept_1, "+", six_2) somme_2 = (trois_1, "+", somme_1) expr_A = (produit_1, "-", somme_2) assert taille(expr_A) == 9 assert hauteur(expr_A) == 4, hauteur(expr_A) assert not est_peigne_gauche(expr_A) # Il y a un not !!! assert est_peigne_gauche(trois_1) # comme toute feuille
On remarquera que ce code est bien moins lisible que les suivants. Nous n'utiliserons plus ici les tuples avec les indices pour accéder aux champs de données !
Nœud
class Nœud: def __init__(self, gauche, élément, droite): self.gauche = gauche self.élément = élément self.droite = droite def est_vide(arbre): return arbre is None def somme(arbre): """On suppose ici que l'arbre binaire ne comporte que des valeurs numériques.""" if est_vide(arbre): return 0 else: return somme(arbre.gauche) + arbre.élément + somme(arbre.droite) def taille(arbre): if est_vide(arbre): return 0 else: return taille(arbre.gauche) + 1 + taille(arbre.droite) def hauteur(arbre): if est_vide(arbre): return 0 else: return 1 + max(hauteur(arbre.gauche), hauteur(arbre.droite)) def est_peigne_gauche(arbre): if est_vide(arbre): return True else: return est_vide(arbre.droite) and est_peigne_gauche(arbre.gauche) # variante une ligne return est_vide(arbre) or (est_vide(arbre.droite) and est_peigne_gauche(arbre.gauche)) # l'évaluation paresseuse permet de rester efficace, on traîte le cas difficile en dernier ! if __name__ == '__main__': # test six_1 = Nœud(None, "6", None) cinq_1 = Nœud(None, "5", None) produit_1 = Nœud(six_1, "×", cinq_1) trois_1 = Nœud(None, "3", None) sept_1 = Nœud(None, "7", None) six_2 = Nœud(None, "6", None) somme_1 = Nœud(sept_1, "+", six_2) somme_2 = Nœud(trois_1, "+", somme_1) expr_A = Nœud(produit_1, "-", somme_2) assert taille(expr_A) == 9 assert hauteur(expr_A) == 4, hauteur(expr_A) assert not est_peigne_gauche(expr_A) # Il y a un not !!! assert est_peigne_gauche(trois_1) # comme toute feuille
On remarque un code bien plus lisible et extensible à volonté. Très bon choix.
Le type tuple nommé est peu connu en Python, c'est pourtant un type recommandé au sein du programme de NSI ; il permet d'avoir la légèreté du tuple en ayant une part de l'expressivité de la POO.
Lire la documentation officielle ; On constatera ici, qu'après la redéfinition de
Nœud
, la suite du code est inchangée !
import collections Nœud = collections.namedtuple('Nœud', ['gauche', 'élément', 'droite']) def est_vide(arbre): return arbre is None def somme(arbre): """On suppose ici que l'arbre binaire ne comporte que des valeurs numériques.""" if est_vide(arbre): return 0 else: return somme(arbre.gauche) + arbre.élément + somme(arbre.droite) def taille(arbre): if est_vide(arbre): return 0 else: return taille(arbre.gauche) + 1 + taille(arbre.droite) def hauteur(arbre): if est_vide(arbre): return 0 else: return 1 + max(hauteur(arbre.gauche), hauteur(arbre.droite)) def est_peigne_gauche(arbre): if est_vide(arbre): return True else: return est_vide(arbre.droite) and est_peigne_gauche(arbre.gauche) # variante une ligne return est_vide(arbre) or (est_vide(arbre.droite) and est_peigne_gauche(arbre.gauche)) # l'évaluation paresseuse permet de rester efficace, on traîte le cas difficile en dernier ! if __name__ == '__main__': # test six_1 = Nœud(None, "6", None) cinq_1 = Nœud(None, "5", None) produit_1 = Nœud(six_1, "×", cinq_1) trois_1 = Nœud(None, "3", None) sept_1 = Nœud(None, "7", None) six_2 = Nœud(None, "6", None) somme_1 = Nœud(sept_1, "+", six_2) somme_2 = Nœud(trois_1, "+", somme_1) expr_A = Nœud(produit_1, "-", somme_2) assert taille(expr_A) == 9 assert hauteur(expr_A) == 4, hauteur(expr_A) assert not est_peigne_gauche(expr_A) # Il y a un not !!! assert est_peigne_gauche(trois_1) # comme toute feuille
On remarque un code aussi clair, moins extensible mais plus léger en empreinte mémoire. À utiliser en cas de volonté d'optimisation temporelle mais surtout mémoire. (Ou alors pour préparer une initiation à la POO ???)
Nous utiliserons la POO pour la suite, pour sa lisibilité, son extensibilité. Nous ne cherchons pas ici les performances.
Les fonctions que nous avons découvertes utilisaient des opérateurs commutatifs, en effet on a max(a, b) == max(b, a)
et 1 + x + y == x + 1 + y == y + x + 1
. Que se passe-t-il avec des fonctions non commutatives ?
Si on conserve une action à gauche avant celle de droite, alors il reste trois positions pour appliquer l'action sur le nœud en cours.
Exercice 3 : Appliquer les fonctions suivantes. Vous ferez sur papier les parcours avec action
définie comme « afficher le nœud en cours », sur l'arbre de notre exemple.
def parcours_préfixe(arbre, action): if not est_vide(arbre): action(arbre) parcours_préfixe(arbre.gauche) parcours_préfixe(arbre.droite) def parcours_infixe(arbre, action): if not est_vide(arbre): parcours_infixe(arbre.gauche) action(arbre) parcours_infixe(arbre.droite) def parcours_postfixe(arbre, action): if not est_vide(arbre): parcours_postfixe(arbre.gauche) parcours_postfixe(arbre.droite) action(arbre)
Ceci est un dessin humoristique lié aux parcours d'arbre.
Dessiner à la main tous les arbres binaires ayant trois ou quatre nœuds.
Facultatif : Pour dessiner des arbres dans un document écrit en Markdown, on regardera le code source de ce document...
Combien y a-t-il d'arbres binaires ayant nœuds. (Ne pas tous les dessiner ; trouver juste le moyen de tous les dénombrer.)
On considère l'arbre binaire suivant :
Nœud
.affichage
définie par :
(
, puis)
affichage(exemple_6)
produit '((B(C))A(D))'
.arbre
dont affichage(arbre)
produit '(1((2)3))'
.élément
du nœud.0
.Un arbre binaire est étiqueté avec des lettres.
ALORHGIMET
.OLHRAMIEGT
.