Algo Arbre Binaire Algo

Identification

Infoforall

16 - Algorithmes des arbres binaires


Nous allons maintenant implémenter des fonctions récursives de recherche dans l'Arbre Binaire.

Prérequis :

  • Données : Arbre et Arbre Binaire
  • Algo : Parcours en profondeur

Evaluation ✎ :

Documents de cours : -

1 - Implémentation d'un Arbre Binaire

Il existe plusieurs façons d'implémenter le type abstrait ARBRE BINAIRE.

La version objet présentée ici présente des avantages et des inconvénients :

  • Avantage : plutôt simple.
  • Désavantage : pas de différence réelle entre un Arbre NON VIDE et un Noeud.
1.1 Réflexion pour obtenir une implémentation SIMPLE d'un Arbre Binaire

  • Un AB est soit un AB VIDE, soit un AB NON VIDE.
  • Un AB NON VIDE est un triplet (valeur de racine, arbre gauche, arbre droite).
  • Un AB VIDE est un pointeur vers VIDE ou pour garder une similitude avec le cas NON VIDE : un triplet (VIDE, VIDE, VIDE).
  • Si on résume cela par les équations sur les types :

    • AB = AB VIDE | AB NON VIDE
    • AB NON VIDE = (Element, AB, AB) = (Element, AB NON VIDE | AB VIDE, AB NON VIDE | AB VIDE)
    • AB VIDE = (VIDE, VIDE, VIDE)
  • Or, un NOEUD est également un triplet (valeur de racine, noeud-enfant gauche ou VIDE, noeud-enfant droite ou VIDE).
    • Noeud = (Element, Noeud|VIDE, Noeud|VIDE)

Conclusion : si on décide d'implémenter VIDE pour le triplet (VIDE, VIDE, VIDE) pour les Noeuds également, on crée une similitude entre Noeud et Arbre NON VIDE.

On pourra donc se passer des Noeuds sur CETTE implémentation simple : ici, un Noeud n'est qu'un alias vers un Arbre NON VIDE.

Rappel ; nous avions fait la même chose avec certaines listes : une Cellule n'était parfois qu'un alias vers une Liste NON VIDE.

1.2 Arbre Binaire implémenté avec un modèle objet

La classe de base, sans méthode particulière

On utilise une classe AB contenant trois attributs :

  • v pour la valeur attribuée à la racine ;
  • g pour sous-arbre gauche ;
  • d pour sous-arbre droite.
1 2 3 4 5 6 7
class AB: """Implémentation d'un Arbre Binaire sous forme objet simple""" def __init__(self, valeur, gauche, droite): self.v = valeur # valeur attribuée à la racine self.g = gauche # Référence du sous-arbre gauche ou None si on atteint un Arbre Binaire Vide self.d = droite # Référence du sous-arbre droite ou None si on atteint un Arbre Binaire Vide

Un Arbre Binaire VIDE est créé avec
AB(None, None, None).

IMPORTANT : C'est le seul cas où on peut envoyer None.

Un Arbre Binaire NON VIDE sera donc créé par un appel où aucun des 3 paramètres ne peut être None.
AB(valeur, arbre_gauche, arbre_droite),

Une feuille serait alors créée par un appel du type
AB(valeur, AB(None, None, None), AB(None, None, None)).

01° Lire la classe proposée ci-dessus.

  • Quel est le seul cas où on a le droit d'envoyer None comme première valeur au constructeur AB() ?
  • S'agit-il d'une précondition imposée via une assertion ou juste d'une précondition à suivre mais sans vérification ?
  • Que faire pour créer un arbre ne contenant qu'un noeud dont la valeur est 10 (une feuille) ?

...CORRECTION...

Question 1

On ne peut envoyer None qui si on désire créer un Arbre Vide.

Il faut alors utiliser AB(None, None, None).

Question 2

Aucune vérification : on pourrait donc faire n'importe quoi.

Nous aurions pu rajouter une assertion en début de création de façon à créer une AssertionError si on n'envoie pas ce qu'il faut. Cela se nomme la programmation offensive.

Question 3

Pour créer un Arbre ne contenant qu'un noeud-racine (une feuille), il faut donc utiliser ceci :

arbre1 = AB(10, AB(None, None, None), AB(None, None, None))

02° Répondre aux deux questions ci-dessous :

  1. Ligne 6 : comment se nomment les variables qu'on trouve sur cette ligne : des paramètres ou des attributs ?
  2. Ligne 8 : comment se nomme le v de self.v ?

...CORRECTION...

  1. Ligne 6 : ce sont les paramètres.
  2. Ligne 8 : c'est l'attribut de l'objet self.
1.3 Complément pour la classe AB : quelques méthodes

Quelques méthodes

On crée quatre méthodes permettant de

  • relier_a_gauche() : on donne un nouveau sous-arbre gauche à notre arbre NON VIDE;
  • relier_a_droite() : idem mais à droite ;
  • definir_donnees_racine() : modifier le contenu de la racine d'un arbre ;
  • vider() : vider un arbre et en faire un AB VIDE ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
# Déclaration de la classe AB class AB: """Implémentation d'un Arbre Binaire sous forme objet simple""" def __init__(self, valeur, gauche, droite): self.v = valeur self.g = gauche self.d = droite def relier_a_gauche(self:'AB NON VIDE', sous_arbre:'AB'): """Méthode qui définit sous_arbre comme le sous-arbre gauche de l'arbre actif""" self.g = sous_arbre def relier_a_droite(self:'AB NON VIDE', sous_arbre:'AB'): """Méthode qui définit sous_arbre comme le sous-arbre droite de l'arbre actif""" self.d = sous_arbre def definir_donnees_racine(self:'AB', element:'Element NON VIDE'): """Méthode qui place l'élément NON VIDE en tant que racine de l'arbre""" self.v = element def vider(self:'AB'): """Méthode qui transforme l'arbre en AB VIDE""" self.v = None self.g = None self.d = None

03-A° Expliquer ce qui est réalisé par cette instruction :

arbre.relier_a_gauche(AB(None, None, None))

...CORRECTION...

On place un sous-arbre binaire VIDE à gauche de notre arbre.

Attention, cela supprime l'ancien sous-arbre gauche.

03-B° Expliquez ce qui est réalisé par cette instruction :

arbre.g.vider()

...CORRECTION...

Même chose : on place un sous-arbre binaire VIDE à gauche de notre arbre.

Attention, cela supprime l'ancien sous-arbre gauche.

04° Quelle est la réponse 3-A ou 3-B qui respecte le principe d'encapsulation ?

...CORRECTION...

C'est la question 3-A car en 3-B on agit directement sur l'un des attributs.

En 3-A, on utilise la méthode fournie par le créateur de la classe : même en cas de mise à jour, de changements de fonctionnement interne, cela devrait toujours fonctionner.

En 3-B, on agit directement sur l'attribut g. Si la mise à jour change son nom en left, votre programme ne fonctionne plus.

2 - Fonctions d'interface

Maintenant que nous savons comment nous avons implémenté notre Arbre Binaire, nous allons pouvoir étudier et réaliser les fonctions d'interface permettant d'implémenter les primitives nécessaires à la manipulation des Arbres Binaires.

(Rappel) 2.1 Les primitives (2e partie de la définition de l'arbre binaire)

Voici la liste des primitives :

  1. nvABV() -> Arbre Binaire VIDE
  2. nvAB(v:Element, g:Arbre Binaire, d:Arbre Binaire) -> Arbre Binaire NON VIDE
  3. estABV(arbre:Arbre Binaire) -> bool
  4. racine(arbre:Arbre Binaire NON VIDE) -> Noeud
  5. contenu(noeud:Noeud) -> Element
  6. gauche(arbre:Arbre Binaire NON VIDE) -> Arbre Binaire
  7. droite(arbre:Arbre Binaire NON VIDE) -> Arbre Binaire

Notez bien que j'ai rajouté les underscores sur les noms des fonctions implémentées ci-dessous alors que les versions abstraites n'en possèdent pas.

05° Créer un fichier nommé arbre_binaire_ifa.py dans un répertoire qu'on nommera algoAB.

📁 algoAB

📄 arbre_binaire_ifa.py

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
"""Module implémentant un Arbre Binaire (AB) avec un modèle objet """ # Déclaration de la classe AB class AB: """Implémentation d'un Arbre Binaire""" def __init__(self, valeur, gauche, droite): self.v = valeur self.g = gauche self.d = droite def relier_a_gauche(self:'AB NON VIDE', sous_arbre:'AB'): """Méthode qui définit sous_arbre comme le sous-arbre gauche de l'arbre actif""" self.g = sous_arbre def relier_a_droite(self:'AB NON VIDE', sous_arbre:'AB'): """Méthode qui définit sous_arbre comme le sous-arbre droite de l'arbre actif""" self.d = sous_arbre def definir_donnees_racine(self:'AB', element:'Element NON VIDE'): """Méthode qui place l'élément NON VIDE en tant que racine de l'arbre""" self.v = element def vider(self:'AB'): """Méthode qui transforme l'arbre en AB VIDE""" self.v = None self.g = None self.d = None # Fonctions d'interface (accessibles de l'extérieur) def contenu(noeud:'Noeud') -> 'Element': """Renvoie le contenu associé au noeud fourni""" return noeud.v def nv_ABV() -> 'AB VIDE': """Renvoie un nouvel Arbre Binaire Vide""" return AB(None, None, None) def nv_AB(valeur:'Element', g:'AB', d:'AB') -> 'AB NON VIDE': """Renvoie un nouvel Arbre Binaire dont la racine porte valeur et qui mène aux sous-arbres g et d""" return AB(valeur, g, d) def est_ABV(arbre:'AB') -> bool: """Prédicat qui renvoie True si l'arbre est vide""" return arbre.v is None and arbre.g is None and arbre.d is None def racine(arbre:'AB NON VIDE') -> 'Noeud': """Renvoie le noeud-racine de l'Arbre Binaire NON VIDE""" return arbre # ici arbre et racine sont des alias def gauche(arbre:'AB NON VIDE') -> 'AB': """Renvoie le sous-arbre gauche de l'Arbre Binaire NON VIDE""" return arbre.g def droite(arbre:'AB NON VIDE') -> 'AB': """Renvoie le sous-arbre droite de l'Arbre Binaire NON VIDE""" return arbre.d # Programme de test du module if __name__ == "__main__": ad = nv_AB("D", nv_ABV(), nv_ABV()) af = nv_AB("F", ad, nv_ABV()) ae = nv_AB("E", nv_ABV(), af) ah = nv_AB("H", nv_ABV(), nv_ABV()) ag = nv_AB("G", nv_ABV(), ah) ab = nv_AB("B", nv_ABV(), nv_ABV()) ac = nv_AB("C", ag, ab) aa = nv_AB("A", ac, ae) print(contenu(racine(gauche(aa))))

06° Lire chaque fonction d'interface pour réellement comprendre ce qu'elles réalisent et comment elles le réalisent.

07° Dessiner l'Arbre Binaire créé par le programme de test (les lignes 67+).

Lancer le programme et expliquer l'affichage obtenu.

...CORRECTION...

Arbre question 18

En ligne 75, on cherche la racine du sous-arbre gauche de l'arbre dont la racine est A.

On cherche alors son contenu et on l'affiche : on obtient C.

08° Modifier le programme pour qu'il réalise plutôt cet arbre ci :

Arbre question 10

...CORRECTION...

3 - Utilisation du module

Nous allons maintenant réaliser différentes fonctions nous permettant de déterminer les caractéristiques de l'arbre, en utilisant uniquement les fonctions d'interface et sans nous soucier de l'implémentation.

Le mieux est donc de placer notre implémentation et les fonctions d'interface dans un module et de créer nos propres fonctions en utilisant uniquement les fonctions d'interface.

09° Créer un fichier nommé mes_algos.py dans le répertoire algoAB et contenant le programme Python suivant :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
# Importation from arbre_binaire_ifa import contenu from arbre_binaire_ifa import nv_ABV from arbre_binaire_ifa import nv_AB from arbre_binaire_ifa import est_ABV from arbre_binaire_ifa import racine from arbre_binaire_ifa import gauche from arbre_binaire_ifa import droite # Déclarations des fonctions def parcours_prefixe(arbre:'AB') -> None: """Exploration (et affichage) en profondeur en préfixe RGD""" pass def hauteur(arbre:'AB', profondeur_vide=0) -> int: """Renvoie la hauteur de l'Arbre Binaire""" pass def nb_feuilles(arbre:'AB') -> int: """Renvoie le nombre de feuilles de l'Arbre Binaire""" pass def taille(arbre:'AB') -> int: """Renvoie la taille de l'Arbre Binaire""" pass def est_present(arbre:'AB', c:'Element') -> bool: """Prédicat : True si la clé c est la valeur d'un des noeuds""" pass # Instructions du programme principal if __name__ == "__main__": ad = nv_AB("D", nv_ABV(), nv_ABV()) af = nv_AB("F", ad, nv_ABV()) ae = nv_AB("E", nv_ABV(), af) ah = nv_AB("H", nv_ABV(), nv_ABV()) ag = nv_AB("G", nv_ABV(), ah) ab = nv_AB("B", nv_ABV(), nv_ABV()) ac = nv_AB("C", ag, ab) aa = nv_AB("A", ac, ae)

Vous devriez donc avoir deux fichiers situés dans le même répertoire.

📁 algoAB

📄 arbre_binaire_ifa.py

📄 mes_algos.py

Et c'est pour cela qu'avec from arbre_binaire_ifa import ... on parvient facilement à retrouver les fonctions situées dans le fichier arbre_binaire_ifa.py.

10° Expliquer pourquoi on peut directement utiliser la fonction nv_AB() alors que la fonction n'est pas dans ce fichier Python ? Que trouve-t-on directement derrière le import ?

...CORRECTION...

On trouve directement le nom de la fonction nv_AB() derrière le import. On a donc directement importer le nom de cette fonction et on peut l'utiliser juste en tapant son nom.

11° Notre programme importe donc la fonction de cette façon :

from arbre_binaire_ifa import nv_AB

Comment lancer l'appel à nv_AB() si les premières lignes proposaient simplement une importation de ce type :

import arbre_binaire_ifa

...CORRECTION...

Cette fois, on importe que le nom du module arbre_binaire_ifa.

Pour utiliser la fonction, il faut donc taper ceci :

arbre_binaire_ifa.nv_AB()

4 - Parcours et autres algorithmes

(RAPPEL) Algo Parcours PREFIXE d'Arbre sans précondition

Il existe deux versions de l'algorithme : dans la première la précondition est que l'arbre soit NON VIDE, dans la deuxième version, il peut être vide ou pas.

Description des ENTREES et SORTIE
  • ENTREE : Un Arbre Binaire
  • SORTIE : Rien sur l'exemple, simple fonction d'affichage. Mais on pourrait les stocker dans un tableau et renvoyer le tableau.
Description de l'ALGORITHME (sans précondition)

Description de parcourir(arbre: Arbre Binaire)

    SI NON estABV(arbre)

      visiter(racine(arbre))

      parcourir(gauche(arbre))

      parcourir(droite(arbre))

    Fin SI

    Renvoyer VIDE

Note importante pour éviter une erreur bête

Il s'agit ici de SI (NON condition) et pas d'un SINON SI (condition).

12° Réaliser la fonction correspondante. Lancer les appels sur l'Arbre Binaire du programme.

1
def parcours_prefixe(arbre:'AB') -> None:
Arbre question 18

Vous devriez obtenir la réponse suivante :

>>> parcours_prefixe(aa) A C G H B E F D

...CORRECTION...

1 2 3 4 5 6 7
def parcours_prefixe(arbre:'AB') -> None: """Exploration (et affichage) en profondeur en préfixe RGD""" if not est_ABV(arbre): print(contenu(racine(arbre))) parcours_prefixe(gauche(arbre)) parcours_prefixe(droite(arbre))

13° Réaliser la fonction parcours_suffixe(). Ne regardez pas votre ancienne fonction, tentez de retrouver seul la solution : le jour de l'examen, vous partirez de rien.

1
def parcours_suffixe(arbre:'AB') -> None:
Arbre question 18

Vous devriez obtenir la réponse suivante :

H G B C D F E A

...CORRECTION...

1 2 3 4 5 6 7
def parcours_suffixe(arbre:'AB') -> None: """Exploration (et affichage) en profondeur en postfixe GDR""" if not est_ABV(arbre): parcours_suffixe(gauche(arbre)) parcours_suffixe(droite(arbre)) print(contenu(racine(arbre)))

14° Réaliser la fonction parcours_infixe(). Ne regardez pas votre ancienne fonction, tentez de retrouver seul la solution : le jour de l'examen, vous partirez de rien.

1
def parcours_infixe(arbre:'AB') -> None:
Arbre question 18

Vous devriez obtenir la réponse suivante :

G H C B A E D F

...CORRECTION...

1 2 3 4 5 6 7
def parcours_infixe(arbre:'AB') -> None: """Exploration (et affichage) en profondeur en infixe GRD""" if not est_ABV(arbre): parcours_infixe(gauche(arbre)) print(contenu(racine(arbre))) parcours_infixe(droite(arbre))

15° Réaliser la fonction hauteur(). Elle doit renvoyer la hauteur de l'arbre.

Comme vous le voyez, cette fonction possède un paramètre qui permet de choisir la profondeur d'un arbre binaire vide. Ici, elle est réglée par défaut à 0. La racine aura donc une profondeur de 1.

1
def hauteur(arbre:'AB', profondeur_vide=0) -> int:
  • Condition d'arrêt : l'arbre est vide.
  • Cas de base : on renvoie la hauteur définie pour l'arbre vide
  • Cas récursif :
  • L'arbre n'est pas vide : la fonction demande donc récursivement la hauteur à chacun de ses sous-arbres puis
  • elle renvoie 1 + le maximum entre hauteur du sous-arbre gauche et hauteur du sous-arbre droite.

On rappellera que pour trouver le maximum entre deux valeurs, on peut comparer mais on peut également utiliser la fonction native max() qui renvoie le plus grand des arguments qu'on lui fournit : si a est plus grand que b, max(a, b) renvoie alors a.

Le jour de l'épreuve pratique, vous pouvez utiliser max() uniquement si vous êtes capable de la recréer.

...CORRECTION...

1 2 3 4 5 6 7
def hauteur(arbre:'AB', profondeur_vide=0) -> int: """Renvoie la hauteur de l'Arbre Binaire""" if est_ABV(arbre): return profondeur_vide else: return 1 + max(hauteur(gauche(arbre)), hauteur(droite(arbre)))

16° Réaliser la fonction nb_feuilles(). Elle doit renvoyer le nombre de feuilles de l'arbre.

1
def nb_feuilles(arbre:'AB') -> int:
  • Condition d'arrêt 1 : l'arbre est vide.
  • Cas de base : on renvoie 0
  • Condition d'arrêt 2 : les deux sous-arbres de l'arbre sont vides (c'est une feuille !).
  • Cas de base : on renvoie 1
  • Cas récursif :
  • On renvoie la somme de l'appel récursif sur le sous-arbre-gauche et de l'appel récursif sur le sous-arbre droite.

...CORRECTION...

1 2 3 4 5 6 7 8 9
def nb_feuilles(arbre:'AB') -> int: """Renvoie le nombre de feuilles de l'Arbre Binaire""" if est_ABV(arbre): return 0 elif est_ABV(gauche(arbre)) and est_ABV(droite(arbre)): return 1 else: return nb_feuilles(gauche(arbre)) + nb_feuilles(droite(arbre))

17° Variante : votre fonction doit en plus afficher la clé de la feuille lorsqu'il en trouve une.

Sur l'Arbre Binaire de l'exemple, cela donne :

H B D 3
Arbre question 18

...CORRECTION...

1 2 3 4 5 6 7 8 9 10
def nb_feuilles(arbre:'AB') -> int: """Renvoie le nombre de feuilles de l'Arbre Binaire""" if est_ABV(arbre): return 0 elif est_ABV(gauche(arbre)) and est_ABV(droite(arbre)): print(contenu(racine(arbre))) return 1 else: return nb_feuilles(gauche(arbre)) + nb_feuilles(droite(arbre))

18° Réaliser la fonction taille(). Elle doit renvoyer le nombre de noeuds de l'Arbre Binaire.

1
def taille(arbre:'AB') -> int:
  • Condition d'arrêt : l'arbre est vide.
  • Cas de base : on renvoie 0
  • Cas de base : on renvoie 1
  • Cas récursif :
  • On renvoie la somme de 1 et de l'appel récursif sur le sous-arbre-gauche et de l'appel récursif sur le sous-arbre droite.

...CORRECTION...

1 2 3 4 5 6 7
def taille(arbre:'AB') -> int: """Renvoie la taille de l'Arbre Binaire""" if est_ABV(arbre): return 0 else: return 1 + taille(gauche(arbre)) + taille(droite(arbre))

19° Réaliser le prédicat est_present(). Cette fonction doit renvoyer True si la clé fournie apparaît bien comme valeur d'un des noeuds de l'Arbre Binaire.

1
def est_present(arbre:'AB', c:'Element') -> bool:
  • Condition d'arrêt 1 : l'arbre est vide.
  • Cas de base : on renvoie False
  • Condition d'arrêt 2 : la clé de la racine a la bonne valeur.
  • Cas de base : on renvoie True
  • Cas récursif :
  • On renvoie le résultat de la recherche sur l'arbre gauche OU de la recherche dans l'arbre droite.

Question subsidiaire : que renvoie un OU s'il l'un des termes est VRAI ?

...CORRECTION...

1 2 3 4 5 6 7 8 9
def est_present(arbre:'Arbre Binaire', c:'Element') -> bool: """Prédicat : True si c est la clé d'un des noeuds""" if est_ABV(arbre): return False elif contenu(racine(arbre)) == c: return True else: return est_present(gauche(arbre), c) or est_present(droite(arbre), c)

Vous devriez être bien content maintenant et vous rendre compte du parcours personnel réalisé depuis le début de première.

Il est temps de plonger dans le grand bain :

20° Fermer votre programme Python. Remplacer le par celui qui est fourni ici. Il faut reste maintenant à les redéfinir en définissant vous même la récursivité.

Pas facile, mais indispensable pour l'évaluation.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
# Importation from arbre_binaire_ifa import contenu from arbre_binaire_ifa import nv_ABV from arbre_binaire_ifa import nv_AB from arbre_binaire_ifa import est_ABV from arbre_binaire_ifa import racine from arbre_binaire_ifa import gauche from arbre_binaire_ifa import droite # Déclarations des fonctions def parcours_prefixe(arbre:'AB') -> None: """Exploration (et affichage) en profondeur en préfixe RGD""" pass def hauteur(arbre:'AB', profondeur_vide=0) -> int: """Renvoie la hauteur de l'Arbre Binaire""" pass def nb_feuilles(arbre:'AB') -> int: """Renvoie le nombre de feuilles de l'Arbre Binaire""" pass def taille(arbre:'AB') -> int: """Renvoie la taille de l'Arbre Binaire""" pass def est_present(arbre:'AB', c:'Element') -> bool: """Prédicat : True si la clé c est la valeur d'un des noeuds""" pass # Instructions du programme principal if __name__ == "__main__": ad = nv_AB("D", nv_ABV(), nv_ABV()) af = nv_AB("F", ad, nv_ABV()) ae = nv_AB("E", nv_ABV(), af) ah = nv_AB("H", nv_ABV(), nv_ABV()) ag = nv_AB("G", nv_ABV(), ah) ab = nv_AB("B", nv_ABV(), nv_ABV()) ac = nv_AB("C", ag, ab) aa = nv_AB("A", ac, ae)

5 - FAQ

Rien pour le moment

La prochaine activité vous permettra d'analyser plus finement les versions récursives de vos fonctions.

Activité publiée le 10 12 2020
Dernière modification : 10 12 2020
Auteur : ows. h.