arbres binaires

Objectifs :

À la fin de cette séquence vous serez capable de :

  • Identifier des situations nécessitant une structure de données arborescente.
  • Évaluer quelques mesures des arbres binaires (taille, encadrement de la hauteur, etc.)

1. Définitions

Un arbre est une structure de données similaire à une liste chaînée, mais au lieu que chaque nœud pointe simplement vers le nœud suivant de manière linéaire, chaque nœud pointe vers plusieurs nœuds. Un arbre est un exemple de structure de données non linéaire. La structure est une façon de représenter la nature hiérarchique d'une structure sous une forme graphique.

Un arbre est appelé arbre binaire si chaque nœud a zéro enfant, un enfant ou deux enfants.
L'arbre vide est également un arbre binaire valide.
Un nœud n'ayant aucun fils est appelé feuille ou nœud externe.

  • La profondeur d’un nœud est la longueur du chemin simple entre la racine et ce noeud.

  • Un niveau c'est l’ensemble de tous les nœuds de même profondeur.

  • La hauteur d'un arbre ou profondeur d'un arbre est le nombre total de niveaux de l'arbre.

  • La taille d'un arbre est le nombre de noeuds de cet arbre.

mon_AB
un_AB

Un arbre binaire peut également être défini comme une structure récursive. Ainsi, un arbre binaire AB est soit vide soit se compose de :

  • Un nœud R, appelé racine de AB, qui stocke un élément ;
  • Un arbre binaire (éventuellement vide), appelé le sous-arbre gauche de AB ;
  • Un arbre binaire (éventuellement vide), appelé le sous-arbre droit de AB.

2. Opérations de base sur les structures de données abstraites arbres binaires

  • est_vide() : Renvoie True si l'arbre AB est vide, False sinon.
  • insere(AB, elt) : insère elt dans AB.
  • supprime(AB, elt) : efface elt dans AB.
  • recherche(AB, elt) : Renvoie True si elt est présent dans AB, False sinon.
  • Parcourir un arbre.

Quelques opérations auxilliaires

  • Trouver la taille de l'arbre ;
  • Trouver la hauteur de l'arbre ;
  • Trouver le niveau qui a la plus grande somme ;

3. Parcourir un arbre

Afin de traiter les arbres, on a besoin de visiter ses différents nœuds.
Le processus de visite de tous les nœuds d'un arbre est appelé parcours d'arbre. Chaque nœud n'est traité qu'une seule fois mais il peut être visité plus d'une fois.

3.1. Parcourir un arbre en profondeur d'abord

3.1.1. Parcours préfixe (on dit aussi préordre)

Le parcours s'effectue comme suit :

  1. on visite d'abord la racine ;
  2. puis on parcourt le sous-arbre gauche en préfixe ;
  3. et enfin, on parcourt le sous-arbre droit en préfixe.

L'algorithme du parcours préfixe

Fonction parcours_prefixe(AB)
    """
        Entrée : AB est un arbre binaire ;
        Sortie : Tous les noeuds de l'arbre ont été visités en préfixe.
    """
    Si AB n'est pas vide,
        renvoyer la cle de la racine de AB    cas de base
        parcours_prefixe(sous-arbre gauche de AB)
        parcours_prefixe(sous-arbre droit de AB)
    FinSi
FinFonction
Exemple :

3.1.2. Parcours postfixe (on dit aussi postordre)

Le parcours s'effectue comme suit :

  1. on parcourt d'abord le sous-arbre gauche en postfixe ;
  2. puis, on parcourt le sous-arbre droit en postfixe.
  3. enfin, on visite la racine ;

Exemple :

3.1.3. Parcours infixe (on dit aussi en ordre)

Le parcours s'effectue comme suit :

  1. on parcourt d'abord en infixe le sous-arbre gauche ;
  2. puis, on visite la racine ;
  3. enfin, on parcourt en infixe le sous-arbre droit.

L'algorithme du parcours

Fonction parcours_infixe(AB)
    """
        Entrée : AB est un arbre binaire ;
        Sortie : Tous les noeuds de AB ont été visités en infixe.
    """
    Si AB n'est pas vide,
        parcours_infixe(sous-arbre gauche de AB)
        renvoyer la clé de la racine de AB
        parcours_infixe(sous-arbre droit de AB)
    FinSi
FinFonction
Exemple :

3.2. Parcourir un arbre en largeur d'abord

Parcourir un arbre en largeur d'abord revient à parcourir un arbre niveau par niveau. On visite la racine, puis tous les noeuds de niveau 1, en partant de la gauche vers la droite, puis tous les noeuds de niveau 2. Ainsi de suite.

L'algorithme du parcours
Traditionnellement, le parcours en largeur d'abord est implémenté en utilisant une liste pour stocker les valeurs des nœuds visités dans le parcours en largeur d'abord, puis une file d'attente pour stocker les nœuds qui n'ont pas encore été visités.

  1. Au départ, on place la racine dans la file,
  2. puis, tant que la file n'est pas vide :
    • défiler le nœud en tête de file,
    • traiter ce nœud (ajouter sa clé à lst, par exemple),
    • ajouter les deux sous-arbres enfants à la file, s'ils exitent.
Fonction parcours_en_largeur(AB)
    """
        Entrée : AB est un arbre binaire ;
        Sortie : Tous les noeuds de AB ont été visités en largeur d'abord.
    """
    F est une file vide
    lst est une liste vide
    Tant Que AB n'est pas vide :
        insérer la clé de la racine de AB dans lst
        Si le sous-arbre gauche de AB n'est pas vide,
            enfiler dans F le sous-arbre gauche de AB
        FinSi
        Si le sous-arbre droit de AB n'est pas vide,
            enfiler dans F le sous-arbre droit de AB
        FinSi
        Si F n'est pas vide,
            racine ← défiler(F)
        Sinon:
            racine ← None
        FinSi
    Fin Tant que

FinFonction
Exemple :

3.5. Déterminer la taille d'un arbre binaire

Calculez la taille des sous-arbres gauche et droit de manière récursive, ajoutez 1 (nœud racine).

L'algorithme de détermination de la taille

Fonction taille_recursive(AB):
    """
        Entrée : AB est un arbre binaire ;
        Sortie : Renvoie le nombre de clé de l'AB.
    """
    Si AB est vide :
        renvoyer 0
    sinon:
        renvoyer taille_recursive(sous-arbre gauche de AB) + taille_recursive(sous-arbre droit de AB) + 1
    FinSi            
FinFonction

4. Une implantation d'un arbre binaire à l'aide d'une liste chaînée

4.1. Les classes nœud et arbre binaire

In [ ]:
class Noeud:
    """Modélise un nœud d'un arbre binaire."""
    def __init__(self, cle, gauche = None, droit = None):
        self.cle = cle
        self.gauche = gauche
        self.droit = droit

    #Afficher le noeud
    def __str__(self):
        """Affiche la valeur (cle) du nœud."""
        return f"{self.cle}"
    
    def __repr__(self):
        """Affiche le nœud."""
        return f"Noeud({repr(self.cle)}, {repr(self.gauche)}, {repr(self.droit)})"

    
class ArbreBinaire:
    """Représente un arbre binaire."""
    def __init__(self, racine):
        self.racine = racine
        
        #Afficher l'AB    
    def __repr__(self):
        """Affiche l'arbre binaire."""
        return f"ArbreBinaire({repr(self.racine)})"
    
    def est_vide(self):
        """Renvoie True si l'AB est vide, False sinon."""
        return self.racine is None
    
    def get_droit(self):
        """Renvoie l'AB droit."""
    
    def get_gauche(self):
        """Renvoie l'AB droit."""
        

4.2. Créer une instance d'ArbreBinaire

mon_AB
un_AB
In [ ]:
noeud7 = Noeud(7)
noeud9 = Noeud(9)
noeud14 = Noeud(14)
noeud17 = Noeud(17)
noeud23 = Noeud(23)
noeud31 = Noeud(31)
noeud23.gauche = noeud14
noeud23.droit = noeud31
noeud14.gauche = noeud7
noeud14.droit = noeud17
noeud7.droit = noeud9

mon_AB = ArbreBinaire(noeud23) #un arbre est identifié par sa racine
In [ ]:
# Une autre façon de représenter le noeud14
exple = Noeud(14, gauche=noeud7, droit=noeud17)
repr(exple)

Faites-vous plaisir 1 :

  1. Créer l'arbre binaire un_AB, donné ci-dessus.
  2. Compléter les méthodes d'arbre binaire get_droit et get_gauche de la structure de données abstraite arbre binaire.
    Tester vos méthodes avec l'instance mon_AB.
  3. En utlisant l'algorithme du parcours préfixé, écrire en langage Python puis l'insérer dans la classe ArbreBinaire, la méthode affiche_prefixe qui affiche les clés de l'AB.
  4. En déduire les méthodes affiche_postfixe et affiche_infixe.
  5. En vous inspirant de la méthode taille_recursive, écrire, en Python, la méthode somme_recursive qui renvoie la somme des valeurs de l'arbre binaire.
  6. En vous inspirant de la méthode taille_recursive, écrire, en Python, la méthode hauteur_recursive qui renvoie le nombre de niveau de l'arbre binaire.
  7. Traduire, en langage Python, la méthode parcours_largeur dont l'algorithme est donné ci-dessus.

5. Une autre implantation d'un arbre binaire à l'aide d'une liste chaînée

In [ ]:
#un noeud est considéré comme un arbre binaire
class AB:
    """Représente un arbre binaire."""
    def __init__(self, cle, gauche = None, droit = None):
        self.cle = cle
        self.gauche = gauche
        self.droit = droit
        
        #Afficher l'arbre
    def __repr__(self):
        """Affiche l'AB."""
        return f"AB({repr(self.cle)}, {repr(self.gauche)}, {repr(self.droit)})"
In [ ]:
# une instance de cet arbre
gauche = AB(2)
droit = AB(3)
arbre = AB(1, gauche, droit)
In [ ]:
#de façon plus concise
arbre = AB(1, AB(2), AB(3))

Faites-vous plaisir 2 :

Écrire puis l'insérer dans la classe AB la fonction d'arbre binaire ajoute_a_gauche(AB, donnee) qui insère la clé donnee comme clé du sous-arbre gauche de AB.

Une application des arbres binaires : Arbres d'expression

Une expression est une suite de symboles qui suivent des règles données. Un symbole peut être soit un opérande, soit un opérateur. On considère, ici, uniquement les opérateurs arithmétiques binaires sous la forme opérande-opérateur-opérande. Les opérateurs arithmétiques standard sont +, -, × et /.
Un arbre d'expression est un arbre binaire avec les propriétés suivantes :

  • Chaque feuille est un opérande.
  • Les nœuds racine et interne sont des opérateurs.
  • Les sous-arbres sont des sous-expressions, la racine étant un opérateur.

Exemple : a × (b + c) + d

arbres binaires de recherche (ABR)

1. Définition

Il s'agit d'arbres binaires étiquetés aux sommets. L'étiquette d'un sommet est la clé ou le contenu du sommet. Contrairement à un arbre binaire général, les clés d’un ABR doivent être comparables entre elles.
Ainsi, un ABR a les propriétés suivantes :

  • les clés du sous-arbre gauche d'un nœud sont inférieures (ou égales) à la clé du nœud.
  • les clés du sous-arbre droit d'un nœud sont strictement supérieures (ou égales) à la clé du nœud.

2. Quelques primitives de la SDA arbre binaire de recherche

2.1. Chercher le maximum dans un ABR

L'algorithme de recherche de la plus grande clé dans l'ABR

maximum_recursive(ABR)
    """
        Entrée : ABR est un arbre binaire de recherche ;
        Sortie : La clé la plus grande dans l'ABR.
    """
    Si l'ABR droit est vide,
        renvoyer la clé de la racine cas de base
    Sinon
        maximum_recursive(ABR.droit) cas récursif
    FinSi
FinFonction

Faites-vous plaisir 3 :

Écrire l'algorithme de recherche de la plus petite clé dans l'ABR, minimum_recursive(ABR).

2.2. Test d'appartenance

L'algorithme de la fonction recherche_recursive

Fonction recherche_recursive(ABR, donnee)
    """
        Entrée : ABR est un arbre binaire de recherche ;
                 donnee est une valeur.
        Sortie : True si donnee appartient à l'ABR, False sinon.
    """
    Si ABR est vide,
        renvoyer False cas de base
    FinSi
    Si donnee < clé de la racine,
        renvoyer recherche_recursive(ABR_gauche, donnee)
    Sinon
        Si donnee > clé de la racine,
            renvoyer recherche_recursive(ABR_droit, donnee)
        Sinon #donnee = clé de la racine
            renvoyer True  cas de base 
        FinSi
    FinSi
FinFonction

Faites-vous plaisir 4 :

Écrire l'algorithme de la fonction insere_recursive(ABR, donnee)

3. Implantation chaînée

In [ ]:
class Noeud:
    """Modélise un noeud d'un arbre binaire de recherche."""
    def __init__(self, cle, gauche = None, droit = None):
        self.cle = cle
        self.gauche = gauche
        self.droit = droit

    #Afficher le noeud
    def __str__(self):
        """Affiche la valeur (cle) du noeud."""
        return f"{self.cle}"
    
    def __repr__(self):
        """Affiche le noeud."""
        return f"Noeud({repr(self.cle)}, {repr(self.gauche)}, {repr(self.droit)})"

    
class ABR:
    """Représente un arbre binaire de recherche."""
    def __init__(self, racine):
        self.racine = racine
        
        #Afficher l'ABR    
    def __repr__(self):
        """Affiche l'ABR."""
        return f"ABR({repr(self.racine)})"
    
    def est_vide(self):
        """Renvoie True si l'ABR est vide, False sinon."""
        return self is None
    

Faites-vous plaisir 5 :

  1. Modéliser en Python les ABR donnés dans la définition.
  2. Donner, à la main, leur parcours infixé respectif ; puis le vérifier à l'aide du programme Python.
  3. Traduire, en Python, les méthodes d'ABR maximum_recursive_ABR, recherche_recursive_ABR, dont les algorithmes sont donnés ci-avant.
  4. Traduire, en Python, la méthode insere_recursive_ABR qui ajoute une clé donnee à l'ABR.

Faites-vous plaisir 6 :

  1. On donne l'arbre binaire ci-contre.
    1. Donner l'affichage préfixé, postfixé et infixé de cet arbre binaire.
    2. Donner l'affichage en largeur d'abord de cet arbre.
  2. Dessiner un AB dont l'affichage infixé est le suivant : CBIDFGE.
  3. Un AB a huit nœuds. Les parcours postfixé et infixé de l'arbre sont donnés ci-dessous.
    Parcours postfixé : FECHGDBA
    Parcours dans l'ordre : FCEABHDG
    Dessinez cet arbre.
  4. Donner les parcours préfixé, postfixé et infixé de l'arbre d'expression ci-dessous.

Faites-vous plaisir 7 :

  1. Dessinez tous les ABR possibles pour les données 5, 9 et 12.
    1. Créez un ABR en utilisant les données suivantes entrées sous forme d'ensemble séquentiel :
      14 23 7 10 33 56 80 66 70.
    2. Insérer dans cet arbre 44 et 50.
  2. Lequel de ces deux arbres binaires est un ABR ? Justifier
    1. Créez un ABR en utilisant les données suivantes entrées sous forme d'ensemble séquentiel :
      80 70 66 56 33 23 14 10 7.
    2. Insérer 40 et 50 dans cet arbre.
  3. Visiter, à l'aide du parcours infixé, les nœuds de l'ABR ci-après.
In [ ]:
 
In [ ]: