Files
TermNSI/Arbres/ARBRES_BINAIRES.ipynb
T

22 KiB

Les arbres binaires

Parmi la forêt d'arbres possibles, on s'intéressera essentiellement aux arbres dit binaires. Ceux ci sont principalement utilisés pour les bases de données (indexation), les moteurs de recherche, et les compilateurs.

Définition

Un arbre binaire est un arbre de degré 2 (dont les noeuds sont de degré 2 au plus).

Les enfants d'un noeud sont lus de gauche à droite et sont appelés fils gauche et fils droit.

fig15.png

Exercice 1

Parmi les arbres du cours précédent, lesquels sont binaires ?

Réponse :

Remarque

Les arbres binaires forment une structure de données qui peut se définir de façon récursive.

Un arbre binaire est:

  • soit vide
  • soit composé d'une racine portant une étiquette (clé) et d'une paire d'arbres binaires, appelés sous-arbre gauche et sous-arbre droit.

Les arbres binaires sont utilisés dans de très nombreuses activités informatiques, nous allons étudier et implanter cette structure de données.


Comment représenter un arbre binaire... à la main...

L'idée est de représenter l'arbre avec un tableau.

[r, a, b] La racine r suivie de ses fils gauche et droit.

fig16.png

fig17.png

Puis on rajoute dans l'ordre les fils gauche et droit de a, puis ceux de b.

[r, a, b, c, d, e, f]

Remarque

Chaque noeud se repère par son indice n dans la liste, son fils gauche se trouvant alors à l'indice 2n + 1 et son fils droit à l'indice 2n + 2.

Exemple: b est d'indice 2, son fils gauche se trouve alors à l'indice 5 et son fils droit à l'indice 6.

Si un noeud n'a pas de fils, on le précise en mettant None à sa place. Notre arbre est alors représenté par le tableau:

[r, a, b, c, d, e, f, None, None, None, None, None, None, None, None]

Quelle taille doit avoir le tableau ?

fig18.png

Cet arbre est complet (tous les noeuds internes ont deux fils).

Il possède 3 niveaux, sa hauteur ou profondeur est donc de 2. La taille du tableau sera de:

2^0 + 2^1 + 2^2 + 2^3 = 2^4 -1 = 15

Il faut compter le nombre de noeuds, y compris les noeuds "fantômes" des feuilles.

Exercice 2

  1. Quelle est la taille du tableau qui permet de représenter cet arbre ?

fig19.png

  1. Écrire le tableau représentant cet arbre et le stocker dans une variable t

  2. Quelle propriété ont les indices des fils gauches et droits ?

  3. Voici un tableau représentant un arbre binaire. Le dessiner. Que peut-il représenter ?

In [ ]:
# Tableau de la question 4
arbre_expression = ['*', '-', 5, 2, 6, None, None, None, None, None, None, None, None, None, None]
In [ ]:
# Question 2 : Écrire le tableau représentant l'arbre
t = # À compléter

Code Python pour créer un arbre

Voici un code Python qui crée la liste représentant l'arbre de l'exercice 2 :

In [ ]:
def creation_arbre(r, profondeur):
    """r : la racine (str ou int). la profondeur de l'arbre (int)"""
    Arbre = [r] + [None for i in range(2**(profondeur+1)-2)]
    return Arbre

def insertion_noeud(arbre, n, fg, fd):
    """Insère les noeuds et leurs enfants dans l'arbre"""
    indice = arbre.index(n)
    arbre[2*indice+1] = fg
    arbre[2*indice+2] = fd

# création de l'arbre
arbre = creation_arbre("r", 5)
# ajout des noeuds par niveau de gauche à droite
insertion_noeud(arbre, "r", "a", "b")
insertion_noeud(arbre, "a", "c", "d")
insertion_noeud(arbre, "b", "e", "f")
insertion_noeud(arbre, "c", None, "h")
insertion_noeud(arbre, "d", "i", "j")
insertion_noeud(arbre, "e", "k", None)
insertion_noeud(arbre, "f", None, None)
insertion_noeud(arbre, "h", None, None)
insertion_noeud(arbre, "i", None, None)
insertion_noeud(arbre, "j", "m", None)
insertion_noeud(arbre, "k", None, None)
insertion_noeud(arbre, "m", None, None)

# pour vérifier
print("Taille du tableau :", len(arbre))
print("Arbre :", arbre)

Exercice 3

  1. Écrire une fonction qui retourne le parent d'un noeud s'il est dans l'arbre et None sinon.
  2. Écrire une fonction qui retourne True si l'arbre est vide
  3. Écrire une fonction qui retourne les enfants d'un noeud.
  4. Une fonction qui renvoie le fils gauche d'un noeud s'il existe et None sinon
  5. Même question pour le fils droit
  6. Écrire une fonction qui renvoie True si le noeud est la racine de l'arbre
  7. Écrire une fonction qui renvoie True si le noeud est une feuille
  8. Écrire une fonction qui renvoie True si le noeud comporte un frère gauche ou droit.

Rappel : dans un tableau, si on note i l'indice d'un noeud, alors ses fils auront comme indice 2i+1 et 2i+2.

Inversement, pour trouver le parent d'un noeud, il faut calculer : (i-1) // 2

In [ ]:
# Question 1 : Parent d'un noeud
def parent(arbre, noeud):
    """Retourne le parent du noeud s'il existe, None sinon"""
    pass  # À compléter
In [ ]:
# Question 2 : Arbre vide
def est_vide(arbre):
    """Retourne True si l'arbre est vide"""
    pass  # À compléter
In [ ]:
# Question 3 : Enfants d'un noeud
def enfants(arbre, noeud):
    """Retourne les enfants du noeud"""
    pass  # À compléter
In [ ]:
# Question 4 : Fils gauche
def fils_gauche(arbre, noeud):
    """Retourne le fils gauche s'il existe, None sinon"""
    pass  # À compléter
In [ ]:
# Question 5 : Fils droit
def fils_droit(arbre, noeud):
    """Retourne le fils droit s'il existe, None sinon"""
    pass  # À compléter
In [ ]:
# Question 6 : Est racine
def est_racine(arbre, noeud):
    """Retourne True si le noeud est la racine"""
    pass  # À compléter
In [ ]:
# Question 7 : Est feuille
def est_feuille(arbre, noeud):
    """Retourne True si le noeud est une feuille"""
    pass  # À compléter
In [ ]:
# Question 8 : A un frère
def a_frere(arbre, noeud):
    """Retourne True si le noeud a un frère"""
    pass  # À compléter

Une seconde façon de faire...

Comme vous l'avez sans doute constaté, il est assez fastidieux de représenter un arbre avec un unique tableau surtout pour un arbre très profond.

L'idée est de représenter l'arbre avec un tableau contenant des tableaux.

fig20.png

Cet arbre se représente par le tableau:

['r', ['a', [], []], ['b', [], []]]

Exercice 4

Écrire le tableau représentant l'arbre ci-dessous et le stocker dans une variable t

fig21.png

In [ ]:
# Exercice 4 : Représentation avec tableaux imbriqués
t = # À compléter

Construction récursive avec dictionnaires

Le code Python ci-dessous construit l'arbre de manière récursive.

  • Les noeuds sont représentés par un dictionnaire.
  • L'arbre se construit depuis la racine en construisant les sous-arbres des fils gauche et droit.
In [ ]:
def noeud(nom, fg=None, fd=None):
    return {'racine': nom, 'fg': fg, 'fd': fd}

# création des noeuds
k = noeud('k')
f = noeud('f')
e = noeud('e', k, None)
b = noeud('b', e, f)
m = noeud('m')
j = noeud('j', m, None)
i = noeud('i')
d = noeud('d', i, j)
h = noeud('h')
c = noeud('c', None, h)
a = noeud('a', c, d)
racine = noeud('r', a, b)

# création de l'arbre sous forme de liste imbriquée
def construit(arbre):
    if arbre is None:
        return []
    else:
        return [arbre['racine'], construit(arbre['fg']), construit(arbre['fd'])]

arbre1 = construit(racine)
print(arbre1)

Exercice 5

Écrire toutes les fonctions de l'exercice 3 dans le cas de cette implémentation de l'arbre (tableaux imbriqués).

In [ ]:
# Exercice 5 : Fonctions pour tableaux imbriqués
# À compléter

Hauteur et parcours d'un arbre

Nous allons maintenant nous intéresser à la hauteur de l'arbre, ainsi qu'aux différentes façons de le parcourir:

  • Le parcours en largeur
  • Le parcours en profondeur (parcours préfixe, parcours infixe et parcours suffixe).

Calcul de la hauteur de l'arbre

L'idée est la suivante:

  • Si l'arbre est vide, la hauteur vaut -1
  • Sinon la hauteur vaut 1 auquel il faut ajouter le maximum entre les hauteurs des sous-arbres gauche et droit.
  • Ces sous-arbres sont eux-mêmes des arbres dont il faut calculer la hauteur.

Une méthode récursive semble donc tout à fait adaptée à la situation.

fig22.png

Exercice 6

Écrire cette fonction pour l'arbre précédent et vérifier que sa profondeur est de 4.

In [ ]:
# Exercice 6 : Hauteur de l'arbre
def hauteur(arbre):
    """Retourne la hauteur de l'arbre"""
    pass  # À compléter

# Test
# print(hauteur(arbre1))  # Doit afficher 4

Les parcours

Le parcours en largeur

Le parcours en largeur d'un arbre consiste à partir de la racine. On visite ensuite son fils gauche puis son fils droit, puis le fils gauche du fils gauche etc... Comme le montre le schéma ci-dessous:

fig23.png

L'idée est la suivante:

On utilise une File.

  • On met l'arbre dans la file.
  • Puis tant que la file n'est pas vide:
    • On défile la file
    • On récupère sa racine
    • On enfile son fils gauche s'il existe
    • On enfile son fils droit s'il existe

fig25.png

Exercice 7

  1. Rappeler les fonctions permettant de définir une structure de file en Python.
  2. Implémenter alors cette fonction et l'essayer sur l'arbre précédent.
In [ ]:
# Exercice 7 : Parcours en largeur
def parcours_largeur(arbre):
    """Parcours en largeur de l'arbre"""
    pass  # À compléter

Les parcours en profondeur

On se balade autour de l'arbre en suivant les pointillés.

fig24.png

Voici un algorithme permettant de réaliser ce parcours:

fig26.png

Exercice 8

Proposer une fonction permettant de réaliser le parcours en profondeur d'un arbre.

In [ ]:
# Exercice 8 : Parcours en profondeur
def parcours_profondeur(arbre):
    """Parcours en profondeur de l'arbre"""
    pass  # À compléter

Les trois types de parcours en profondeur

Dans le schéma ci-dessus, on a rajouté des "noeuds fantômes" pour montrer que l'on peut considérer que chaque noeud est visité trois fois:

  • une fois par la gauche
  • une fois par en dessous
  • une fois par la droite

Définition - Parcours préfixe : Dans un parcours préfixe, on liste le noeud la première fois qu'on le rencontre.


Définition - Parcours infixe : Dans un parcours infixe, on liste le noeud la seconde fois qu'on le rencontre.

Ce qui correspond à:

  • On liste chaque noeud ayant un fils gauche la seconde fois qu'on le voit
  • On liste chaque noeud sans fils gauche la première fois qu'on le voit

Définition - Parcours suffixe : Dans un parcours suffixe, on note le noeud la dernière fois qu'on le rencontre.

Exercice 9

Écrire les sommets dans l'ordre d'un parcours préfixe.

Réponse :

Exercice 10

Écrire les sommets dans l'ordre d'un parcours infixe.

Réponse :

Exercice 11

Écrire les sommets dans l'ordre d'un parcours suffixe.

Réponse :

Exercice 12

  1. Voici trois algorithmes récursifs, dire pour chacun d'entre eux à quel parcours il correspond.

fig27.png fig28.png fig29.png

  1. Implémenter ces trois fonctions en Python et confirmer les réponses des questions précédentes.
In [ ]:
# Exercice 12 : Implémentation des trois parcours

def parcours_1(arbre):
    """Premier algorithme - À identifier"""
    pass  # À compléter

def parcours_2(arbre):
    """Deuxième algorithme - À identifier"""
    pass  # À compléter

def parcours_3(arbre):
    """Troisième algorithme - À identifier"""
    pass  # À compléter

Une troisième façon de faire... en programmation objet

Nous allons créer une classe Noeud dont les attributs d'instances seront:

  • le nom (ou valeur) de la racine
  • son fils gauche (None par défaut) ou de type Noeud
  • son fils droit (None par défaut) ou de type Noeud

Exercice 13

  1. Implémentez cette classe Noeud
  2. Stocker l'arbre comme étant l'objet de type "Noeud" correspondant à la racine.
  3. Écrire une méthode est_feuille qui renvoie True si le noeud est une feuille et False sinon
  4. Définir une fonction hauteur prenant comme argument un arbre permettant de renvoyer la hauteur de cet arbre
  5. Écrire une méthode hauteur dans la classe Noeud permettant de faire la même chose
  6. Proposer des fonctions pour le parcours infixe, préfixe et suffixe pour cette façon de coder un arbre.
  7. Transformer alors vos fonctions en méthodes de la classe Noeud
In [ ]:
# Exercice 13 : Classe Noeud

class Noeud:
    def __init__(self, valeur, gauche=None, droit=None):
        """Constructeur de la classe Noeud"""
        pass  # À compléter
    
    def est_feuille(self):
        """Retourne True si le noeud est une feuille"""
        pass  # À compléter
    
    def hauteur(self):
        """Retourne la hauteur de l'arbre"""
        pass  # À compléter
    
    def parcours_prefixe(self):
        """Parcours préfixe de l'arbre"""
        pass  # À compléter
    
    def parcours_infixe(self):
        """Parcours infixe de l'arbre"""
        pass  # À compléter
    
    def parcours_suffixe(self):
        """Parcours suffixe de l'arbre"""
        pass  # À compléter
In [ ]:
# Construction de l'arbre avec la classe Noeud
# À compléter