Un module de manipulation d’arbres binaires #
graph LR
alert(Ressource)
style alert fill:orange
La classe BinaryTree
, définie dans le
module binary_tree.py
proposé ici, permet de
représenter des arbres binaires.
Cette classe fournit
- « deux » constructeurs :
BinaryTree()
etBinaryTree(data, left, right)
- trois accesseurs :
get_data()
,get_left_subtree()
, etget_right_subtree()
- un reconnaisseur :
is_empty()
Essayez
import binary_tree as bt
help(bt.BinaryTree)
pour afficher l’aide associée à cette classe.
Ou utilisez le notebook Jupyter
Ce module propose aussi des fonctionnalités de visualisation d’arbres binaires, et d’exportation aux formats DOT et PNG.
Parcourir les arbres #
graph LR
alert(Notions)
style alert fill:orange
Parcourir un arbre consiste à visiter chacun des nœuds de l’arbre, à effectuer un traitement sur chacune des étiquettes des nœuds de l’arbre.
On distingue différents parcours selon l’ordre dans lequel les nœuds seront visités.
On distingue principalement les parcours en profondeur d’abord et en largeur.
Trois parcours en profondeur d’abord #
Le principe des parcours en profondeur est de visiter récursivement les nœuds de l’arbre.
On distingue trois parcours, selon que l’on traite l’étiquette d’un nœud :
- avant la visite de ses fils gauche et droit
- après la visite de son fils gauche (et avant la visite de son fils droit)
- après la visite des ses fils gauche et droit
On utilise respectivement les termes
- préfixe
- infixe
- postfixe
pour désigner ces trois parcours en profondeur.
≫ Préfixe, infixe, et postfixe #
graph LR
alert(Application)
style alert fill:orange
Donnez le résultat de l’appel de chacune des trois fonctions suivantes
def afficher_A(a):
if not a.is_empty():
afficher_A(a.get_left_subtree())
print(a.get_data())
afficher_A(a.get_right_subtree())
def afficher_B(a):
if not a.is_empty():
afficher_B(a.get_left_subtree())
afficher_B(a.get_right_subtree())
print(a.get_data())
def afficher_C(a):
if not a.is_empty():
print(a.get_data())
afficher_C(a.get_left_subtree())
afficher_C(a.get_right_subtree())
sur l’arbre ci-dessous.
Associez le terme adéquat parmi préfixe, infixe, et postfixe à chacune des trois fonctions d’affichage.
Associez une des couleurs rouge, verte, jaune, à ces termes et fonctions selon le schéma suivant :
Représentation d’expressions arithmétiques #
graph LR
alert(Programmation)
style alert fill:orange
Une expression arithmétique construite avec des opérateurs binaires – c’est-à-dire à deux opérandes telles l’addition, la soustraction, la multiplication, la division –, peut être représentée par un arbre binaire dont les nœuds internes portent les opérateurs et les feuilles des symboles de variables – x, y, z… –, ou des constantes – 6, 14, etc. –.
Ainsi, l’arbre suivant représente l’expression $6 (x+y) + (y-14)
$
Des expressions #
Cet arbre, et d’autres exemples peuvent être construits avec les instructions suivantes :
# 6 * (x+y) + (y−14)
expression1 = bt.BinaryTree('+',
bt.BinaryTree('*', feuille(6), bt.BinaryTree('+', feuille('x'), feuille('y'))),
bt.BinaryTree('-', feuille('y'), feuille(14)))
# (x+5) * (y/2)
expression2 = bt.BinaryTree('*',
bt.BinaryTree('+', feuille('x'), feuille(5)),
bt.BinaryTree('/', feuille('y'), feuille(2)))
# 4 * (x-1)
expression3 = bt.BinaryTree('*',
feuille(4),
bt.BinaryTree('-', feuille('x'), feuille(1)))
≫ Imprimer une expression #
Proposez une fonction qui accepte en paramètre un arbre représentant une expression arithmétique, et imprime cette expression.
≫ Évaluer une expression #
Proposez une fonction qui accepte en paramètre un arbre représentant une expression arithmétique, et renvoie la valeur de cette expression.
Il est également nécessaire de fournir à cette fonction les valeurs associées à chacune des variables présentes dans l’expression. Quelle structure de données proposez-vous d’utiliser pour mémoriser ces associations ?
≫ Écriture polonaise inverse #
graph LR
alert(Aller plus loin)
style alert fill:orange
Quel affichage produit un parcours postfixe de l’arbre représentant notre expression arithmétique ?
Remarquez que cette expression non-ambiguë ne nécessite pas de parenthèses.
Sachez que cette forme est exploitée pour produire le code machine d’évaluation de l’expression pour une machine à pile.
Parcours en largeur #
graph LR
alert(Notion)
style alert fill:orange
Un parcours en largeur visite les nœuds d’un arbre niveau par niveau :
le nœud racine de profondeur nulle, les nœuds de profondeur 1, puis
les nœuds de profondeur 2, etc.
Pour une profondeur donnée, on visite les nœuds de gauche à droite.
Le principe est que lors de la visite du niveau de profondeur $i
$,
on mémorise dans une structure ad hoc les nœuds fils, qui sont donc
des nœuds de profondeur $i+1
$ qui seront à visiter une fois la
visite de la profondeur $i
$ terminée.
La structure dans laquelle mémoriser les nœuds fils doit permettre :
- d’ajouter un élément (un nœud) à la structure ;
- de récupérer un élément ; plus précisément de récupérer l’élément le plus anciennement inséré ;
- de tester que la structure est vide — notre algorithme est terminé.
On reconnaît là l’interface du type de données abstrait file qui met en œuvre le principe FIFO : premier arrivé, premier sorti.
Pour la réalisation de notre parcours en largeur, les éléments de cette file sont des arbres : l’arbre à parcourir, puis l’arbre fils gauche de sa racine, puis l’arbre fils droit de sa racine, etc.
≫ Parcourir en largeur #
graph LR
alert(Programmation)
style alert fill:orange
Proposez une fonction Python qui renvoie la liste des étiquettes d’un arbre binaire donné ; cette liste sera ordonnée selon un parcours en largeur d’abord de l’arbre.
≫ Parcourir avec une pile #
graph LR
alert(Aller plus loin)
style alert fill:orange
Quel parcours de l’arbre obtient-on si on remplace la file utilisé dans l’algorithme précédent par une pile, donc une structure LIFO, dernier arrivé, dernier sorti ?
Que faut-il modifier pour obtenir un des trois classiques parcours en profondeur ?
Arbres de recherche #
graph LR
alert(Notion)
style alert fill:orange
Un arbre de recherche, ABR, est un arbre binaire qui va être utilisé pour réaliser « efficacement » des opérations de recherche d’une valeur, mais aussi des opérations d’insertion et suppression de valeurs.
Les valeurs des étiquettes de l’arbre doivent donc appartenir à un ensemble ordonné.
Caractériser les arbres binaires de recherche #
→ Définition. Arbre binaire de recherche – ABR #
Un arbre binaire est un arbre binaire tel que pour tout nœud d’étiquette e :
- les étiquettes de tous les nœuds de son sous-arbre gauche sont inférieures ou égales à e, et
- les étiquettes de tous les nœuds de son sous-arbre droit sont supérieures à e.
≫ Des arbres binaires de recherche #
graph LR
alert(Application)
style alert fill:orange
Indiquez quels sont parmi les arbres suivants ceux qui sont des arbres binaires de recherche.
≫ Les plus petite et plus grande étiquettes #
Dans quel nœud d’un arbre binaire de recherche se trouve la plus
petite étiquette ?
La plus grande ?
Dans quel nœud d’un arbre binaire de recherche se trouve l’étiquette médiane ?
≫ Parcours ordonné #
Lequel des classiques parcours d’arbres binaires permet de visiter les nœuds d’un ABR dans l’ordre des étiquettes ?
≫ Reconnaître un arbre binaire de recherche #
graph LR
alert(Programmation)
style alert fill:orange
Proposez un prédicat Python qui reconnaît si un arbre binaire donné est un arbre binaire de recherche.
Deux pistes possibles pour cela sont
-
De réaliser un parcours de l’arbre qui devrait visiter les nœuds dans l’ordre croissant (exercice précédent), et vérifier que les étiquettes sont bien dans l’ordre croissant.
(On suppose qu’il n’y a pas de doublons dans l’arbre.) -
De proposer une fonction récursive qui renvoie la plus petite et la plus grande des valeurs des étiquettes d’un arbre, et vérifie qu’il est un ABR.
Recherche d’une valeur dans un arbre binaire de recherche #
graph LR
alert(Programmation)
style alert fill:orange
Proposez une fonction Python qui renvoie le nœud d’un arbre binaire de
recherche dont l’étiquette est égale à une valeur donnée.
La fonction pourra retourner None
si la valeur n’était pas présente
dans l’arbre.
Insertion d’une valeur dans un arbre binaire de recherche #
L’insertion d’une valeur $v
$ dans un arbre binaire de recherche peut
reposer sur le déroulé récursif suivant :
- si l’arbre est vide, renvoyer l’arbre arbre constitué d’une unique
feuille portant la valeur $
v
$ - si la valeur à insérer $
v
$ est inférieure ou égale à la valeur de la racine, renvoyer l’arbre formé de- la racine
- l’arbre formé du fils gauche dans lequel aura été inséré la valeur
$
v
$ - le fils droit
- sinon – la valeur $
v
$ est supérieur à la valeur de la racine –, renvoyer l’arbre formé de- la racine
- le fils gauche
- l’arbre formé du fils droit dans lequel aura été inséré la valeur
$
v
$
≫ Insérer une valeur dans un ABR #
graph LR
alert(Programmation)
style alert fill:orange
Proposez une fonction Python qui renvoie un arbre binaire de recherche donné augmenté d’une valeur donnée.
Suppression d’une valeur dans un arbre binaire de recherche #
graph LR
alert(Aller plus loin)
style alert fill:orange
Soit une valeur $v
$, nous cherchons à supprimer, s’il exsite, le
nœud d’étiquette $v
$ dans un arbre binaire de recherche donné.
Passons en revue les différents cas de figure :
- si l’arbre est vide, il n’y a rien à faire
- si l’arbre est réduit à une feuille, selon qu’elle porte cette
valeur $
v
$ ou non, il s’agit de renvoyer l’arbre vide ou la feuille - si la valeur $
v
$ est inférieure à la racine, à la manière de ce que nous avons fait pour l’insertion, reconstruire l’arbre formé de- la racine,
- le fils gauche amputé de la valeur $
v
$ - le fils droit
- si la valeur est supérieure à la racine, on agit de même, symétriquement pour les fils gauche et droit
- si la racine porte la valeur $
v
$ et ne possède qu’un unique fils, ce nœud fils devient le nouvel arbre
Reste à traiter la cas où la racine porte la valeur $v
$ et possède
deux fils.
- considérons le successeur de la racine ; il s’agit du nœud portant
la plus petite des valeurs supérieures à $
v
$ - par définition d’un ABR, ce successeur est le nœud le plus à gauche du fils droit de la racine
- l’arbre amputé de $
v
$ est donc formé :- de la valeur de ce successeur
- du fils gauche
- du fils droit amputé de la valeur du successeur.
Une implémentation « efficace » évite de parcourir le fils droit à la recherche de la valeur du successeur, puis de parcourir le fils droit pour supprimer le nœud portant cette valeur.
Coût des opérations #
graph LR
alert(Aller plus loin)
style alert fill:orange
Les arbres binaires de recherche sont introduits pour répondre au besoin de réaliser avec la même efficacité les trois opérations de recherche, ajout, et suppression d’une valeur.
Chacun des algorithmes que nous avons définis pour ces trois
opérations nécessite de visiter un arbre de sa racine à une feuille,
comparant une valeur à l’étiquette des nœuds visités.
Le nombre de comparaisons sera donc égal à la hauteur de l’arbre.
Les parcours de la racine à une feuille peuvent être interrompus dans le cas ou le nœud recherché est un nœud interne.
L’analyse du coût de ces algorithmes se ramène dont à l’étude de la hauteur des arbres binaires des recherche.
Dans le meilleur des cas, un arbre binaire de recherche est
équilibré. Sa hauteur est alors $\lfloor\log_2 n\rfloor
$, $n
$ étant la taille,
nombre d’éléments, de l’arbre.
Le coût des algorithmes pour nos trois opérations est alors au pire
logarithmique.
Dans le pire des cas, un arbre binaire de recherche est
filiforme. Sa hauteur est alors égale à $n-1
$.
Le coût des algorithmes pour nos trois opérations est alors au pire
linéaire.
L’analyse du coût en moyenne des algorithmes nécessite de considérer
- la profondeur moyenne d’un nœud dans un arbre binaire de recherche quelconque,
et
- la hauteur d’un arbre « moyen » parmi tous les arbres binaires.
Nous ne mènerons pas cette étude ici.
→ Résultat. Complexité des algorithmes sur les arbres binaires de recherche #
graph LR
alert(À retenir)
style alert fill:orange
La complexité des algorithmes de recherche, insertion, et suppression d’une valeur dans un arbre binaire de recherche est
- en moyenne logarithmique,
- au pire linéaire.
Le coût de ces opérations est au pire logarithmique dans le cas d’arbres de recherche équilibrés.
Maintenir l’équilibre #
graph LR
alert(Aller plus loin)
style alert fill:orange
Le coût au pire logarithmique des opérations sur les arbres de recherche équilibrés motive la modification de nos algorithmes pour tenter de conserver cette propriété d’équilibre des arbres.
Le principe général repose sur des rotations opérées lors de l’insertion ou la suppression d’une valeur.
On opère des rotations simples comme celles ilustrées ci-dessous, et des rotations doubles.
Ces arbres binaires de recherche équilibrés sont nommés AVL d’après le nom de leurs inventeurs, Georgii Adelson-Velsky et Evguenii Landis en 1962.