# Arbres : Exercice de parcours en largeur **Exercice :** Effectuer en python le parcours en largeur d'un arbre. **Rappel :** Lorsque l'on parcourt l'arbre en *"largeur"* on commence par explorer les frères/soeurs avant d'explorer les fils. **Rappel :** Par convention, on préfère explorer de gauche a droite. ## Ressources ### Classes Arbre et Noeud ```python= class Noeud: def __init__(self, cle): self.cle = cle class Arbre: def __init__(self, cle): self.racine = Noeud(cle) self.sag = None # Le sous-arbre gauche, qui est lui-même un arbre self.sad = None # Le sous-droit gauche, qui est lui-même un arbre ``` ### Classe File ```python! class File: def __init__(self): self.liste = [] def enfiler(self, e): self.liste.append(e) def defiler(self): if len(self.liste) != 0: e = self.liste[0] self.liste = self.liste[1:] return e else: return None def estVide(self): if len(self.liste) == 0: return True else: return False ``` ## Jeux de tests : exemples d'arbres pour faire des tests Le premier arbre que vous pouvez utiliser pour faire vos tests est celui présent à la page 276 de votre manuel : ![](https://i.imgur.com/p08Yg7z.png) **Rappels :** - La racine de cet arbre est A - Les feuilles de cet arbre sont : D, E, G, I - Il s'agit d'un arbre binaire (au maximum deux fils par noeud). Il n'est ni localement complet, ni complet (le noeud *B* a un seul fils, de même pour *H*). - Cet arbre est de taille 9 (on peut compter 9 noeuds) - La hauteur de cet arbre est de 3 (le noeud le plus distant de la racine a une hauteur de 3, en effet, H(I) = 3). Attention, on compte les arêtes depuis la racine et pas les noeuds. Il s'agit d'une distance. Construction de cet arbre a partir de notre classe *Arbre* : ```python= arbre1 = Arbre('A') arbre1.sag = Arbre('B') arbre1.sag.sag = Arbre('C') arbre1.sag.sag = Arbre('C') arbre1.sag.sag.sag = Arbre('D') arbre1.sag.sag.sad = Arbre('E') arbre1.sad = Arbre('F') arbre1.sad.sag = Arbre('G') arbre1.sad.sad = Arbre('H') ``` Le deuxième arbre que vous pouvez utiliser pour faire vos tests est celui dessiné par Guillaume au tableau : ![](https://i.imgur.com/YbhRGQp.png) **Rappels :** - La racine de cet arbre est A - Les feuilles de cet arbre sont : J, I, H, L, K - Il s'agit d'un arbre binaire qui n'est ni complet, ni localement complet. - Cet arbre est de taille 12. - Cet arbre est de hauteur 4 - On peut utiliser les mots : *hauteur*, *profondeur* et *niveau* de manière équivalente. Construction de cet arbre a partir de notre classe *Arbre* : ```python= arbre2 = Arbre('A') arbre2.sag = Arbre('C') arbre2.sad = Arbre('B') arbre2.sag.sag = Arbre('F') arbre2.sag.sag.sag = Arbre('J') arbre2.sag.sag.sad = Arbre('I') arbre2.sad.sag = Arbre('E') arbre2.sad.sad = Arbre('D') arbre2.sad.sag.sag = Arbre('H') arbre2.sad.sad = Arbre('D') arbre2.sad.sad.sag = Arbre('G') arbre2.sad.sad.sag.sag = Arbre('L') arbre2.sad.sad.sag.sad = Arbre('K') ``` # Resolution de l'exercice ## Determiner les interfaces On commence par determiner le prototype de la fonction a implementer : ```python= parcoursLargeur(a : Arbre) -> list ``` On écrit une fonction qui prend un unique paramètre : l'arbre que l'on veut parcourir. Elle retourne une liste qui contient les clés des noeuds qui ont été parcourus, dans l'ordre dans lequel ils ont été parcourus. ## Solution ### 1) La solution "officielle" L'essentiel ici est d'avoir l'idee d'utiliser une structure de file qui va servir a stocker le "travail" qui reste a realiser ainsi que l'ordre dans lequel il faut le realiser. ```python= def parcoursLargeur(a : Arbre) -> list: result = [] # On cree une liste vide pour stocker les cles des noeuds que l'on a parcourus f = File() # On cree une file vide, elle sert a stocker le "travail" qui reste a realiser et l'ordre dans lequel il faut le faire. Il s'agit des prochains elements a parcourir au fur et a mesure qu'on les decouvre. f.enfiler(a) # Avant toute chose, on enfile la racine, c'est le prochain element a traiter # On verifie les entrees pour eviter de faire des calculs sur n'importe quoi. On sors de la fonction et on avertit l'utilisateur en cas de pb. if a == None: print("Erreur vous avez demande le parcours d'un arbre vide") return None # On continue le parcours tant qu'il reste du travail a faire dans la file ... while not f.estVide(): courant = f.defiler() # On recupere dans la file le travail a realiser. Il s'agit du sous-arbre courant a traiter result.append(courant.racine.cle) # On indique que l'on vient de le parcourir (ajout a la liste) # On teste la presence de sous-arbres gauche et droit. # Si ils sont existants, on les ajoute a la file des prochains elements a parcourir ... if courant.sag != None: f.enfiler(courant.sag) if courant.sad != None: f.enfiler(courant.sad) return result ``` **Remarque :** L'ordinateur n'a pas une vision globale de l'arbre en entier comme nous (humains) on peut l'avoir du premier coups d'oeil. C'est du a notre maniere de decrire l'arbre dans la classe *Arbre*. En effet, lorsque l'on considere un arbre *A*, les seules informations dont dispose l'ordinateur sont : - la racine - un "lien" vers la racine du SAG - un "lien" vers la racine du SAD Il va etre oblige de se balader progressivement de lien en lien jusque les avoir tous parcourus. C'est la que la notion d'exploration et de parcours prends son sens. **Remarque :** Dans cet algorithme, si on remplace la structure de file par une pile, alors on obtient un parcours en profondeur. **Remarque :** Cet algorithme n'est pas recursif. ### 2) La solution d'Augustin ```python= # Fonction sans File qui permet de parcourir un arbre en largeur def parcoursLargeur(arbre : Arbre) -> list: element = [arbre.Racine.clef] liste_de_sous_arbre = [arbre] while len(liste_de_sous_arbre) > 0: nouveau_sous_arbre = [] for i in range(len(liste_de_sous_arbre)): if liste_de_sous_arbre[i].Sag is not None: nouveau_sous_arbre.append(liste_de_sous_arbre[i].Sag) element.append(liste_de_sous_arbre[i].Sag.Racine.clef) if liste_de_sous_arbre[i].Sad is not None: nouveau_sous_arbre.append(liste_de_sous_arbre[i].Sad) element.append(liste_de_sous_arbre[i].Sad.Racine.clef) liste_de_sous_arbre = nouveau_sous_arbre return element ``` **Remarque :** La solution d'Augustin est equivalente a la precedente. Il n'utilise pas la classe *File*, mais son utilisation astucieuse des listes est equivalente au passage par une file. ## Les resultats attendus : Pour *arbre1* le parcours en largeur donne : ```python ['A', 'B', 'F', 'C', 'G', 'H', 'D', 'E', 'I'] ``` Pour *arbre2* le parcours en largeur donne : ```python ['A', 'C', 'B', 'F', 'E', 'D', 'J', 'I', 'H', 'G', 'L', 'K'] ``` # Deuxieme etape : Recherche d'un maximum dans l'arbre On profite de la capacite a explorer un arbre pour effectuer un traitement sur les donnees qu'il peut contenir. On considere un arbre modifie qui embarque/encapsule de l'information a l'interieur de ses noeuds. Dans ce cas precis on va stocker un nombre dans chaque noeud, on veut ecrire une fonction qui retourne la cle du noeud qui contient la plus grande valeur ainsi que la valeur de ce maximum. ## Modification des classes Noeud et Arbre On ajoute un attribut *'data'* a la classe *Noeud*, il contient l'information contenue a l'interieur de celui-ci. Ici, on considerera qu'il s'agit toujours d'un nombre. On modifie le constructeur de la classe en consequence : ```python= class Noeud: def __init__(self, cle, data): self.cle = cle self.data = data class Arbre: def __init__(self, cle, data): self.racine = Noeud(cle, data) self.sag = None self.sad = None ``` ## Construction du jeu de tests On cree un arbre de test, on pourra alors executer notre algorithme dessus pour essayer de se convaincre que notre code fonctionne tel que prevu. ```python= a = Arbre('A', 10) a.sag = Arbre('B', 42) a.sag.sag = Arbre('C', 55) a.sag.sag.sag = Arbre('D', 17) a.sag.sag.sad = Arbre('E', 21) a.sad = Arbre('F', 32) a.sad.sag = Arbre('G', 26) a.sad.sad = Arbre('H', 101) a.sad.sad.sad = Arbre('I', 1) ``` ## Determiner les interfaces Comme toujours, avant de commencer a ecrire du code, on s'interroge sur les entrees/sorties de notre fontion et sur leur nature. Ici, on veut ecrire une fonction qui prend en parametre un objet de type *Arbre* et qui renvoie un couple constitue de : - la cle du noeud qui contient le maximum (chaine de caracteres) - la valeur de ce maximum (entier) ```python rechercheMax(a : Arbre) -> (str, int) ``` ## Le programme de recherche de maximum ```python def rechercheMax(a): f = File() f.enfiler(a) val_max = a.racine.data # Variable temporaire qui contient la valeur max rencontree jusque-la (a ce stade de l'exploration de l'arbre) cle_max = a.racine.cle # Variable temporaire qui contient cle du noeud ayant la valeur max if a == None: print("Erreur vous avez demande le parcours d'un arbre vide") return None while not f.estVide(): courant = f.defiler() if courant.racine.data > val_max: val_max = courant.racine.data cle_max = courant.racine.cle if courant.sag != None: f.enfiler(courant.sag) if courant.sad != None: f.enfiler(courant.sad) return (cle_max, val_max) ``` ## Test et resultat attendu On execute la fonction sur l'arbre *a* construit auparavent : ```python print(rechercheMax(a)) ``` Le resultat est le suivant : ```python ('H', 101) ```