Algo AB Récursivité

Identification

Infoforall

17 - Récursivité sur les AB


Nous allons nous attarder sur le déroulement des fonctions récursives que vous avez réalisé dans l'activité précédente.

Nous allons notamment voir différentes façons de rédiger une réponse permettant d'expliquer les appels récursifs.

Logiciel nécessaire pour l'activité : -

Prérequis :

  • Données : Arbre et Arbre Binaire
  • Algos : Parcours d'Arbres
  • Algos : Algorithmes des Arbres Binaires

Evaluation ✎ :

Documents de cours : open document ou pdf

1 - Fonctions d'interface

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

Je le décris ici sous forme d'un type immuable, mais on pourait faire la même chose en muable.

  1. nvABV() -> Arbre Binaire VIDE
  2. Crée un nouvel ARBRE BINAIRE vide.
    On le note ainsi pour dire nouvelArbreBinaireVide() mais c'est plus long.

  3. nvAB(v:Element, g:Arbre Binaire, d:Arbre Binaire) -> Arbre Binaire NON VIDE
  4. Crée un nouvel ARBRE BINAIRE dont la racine porte la valeur v et mène aux sous-arbres transmis à la fonction, g et d.

  5. estABV(arbre:Arbre Binaire) -> bool
  6. Prédicat qui renvoie True si l'arbre est un arbre binaire vide.

  7. racine(arbre:Arbre Binaire NON VIDE) -> Noeud
  8. Renvoie le noeud jouant le rôle de la racine pour cet arbre binaire NON VIDE.

  9. contenu(noeud:Noeud) -> Element
  10. Renvoie l'élément rattaché au noeud transmis.

  11. gauche(arbre:Arbre Binaire NON VIDE) -> Arbre Binaire
  12. Renvoie le sous-arbre gauche de arbre. On obtient bien un Arbre. Si vous voulez le noeud gauche, il faudra appliquer en plus la fonction racine().

  13. droite(arbre:Arbre Binaire NON VIDE) -> Arbre Binaire
  14. Renvoie le sous-arbre droite de arbre.

(Rappel) 1.2 Module de l'arbre binaire sous forme d'un objet

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 76 77 78 79 80 81
"""Module implémentant un Arbre Binaire (AB) sous forme d'objets """ # Déclaration de la classe AB class AB: """Implémentation d'un Arbre Binaire sous forme objet simple""" def __init__(self, valeur=None, gauche=None, droite=None): assert valeur or (valeur == None and gauche == None and droite == None) self.v = valeur self.g = gauche self.d = droite if valeur and gauche is None: self.g = AB() if valeur and droite is None: self.d = AB() 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() def nv_AB(valeur:'Element', g:'AB|None'=None, d:'AB|None'=None) -> '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 # dans cette implémentation, un AB non vide est un pointeur vers sa racine 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") af = nv_AB("F", g=ad) ae = nv_AB("E", d=af) ah = nv_AB("H") ag = nv_AB("G", d=ah) ab = nv_AB("B") ac = nv_AB("C", g=ag, d=ab) aa = nv_AB("A", g=ac, d=ae)

Dans toute l'activité, nous travaillerons avec cet Arbre Binaire :

1 2 3 4 5 6 7 8
ad = nv_AB("D") af = nv_AB("F") ae = nv_AB("E"), g=ad, d=af) ah = nv_AB("H") ag = nv_AB("G", d=ah) ab = nv_AB("B") ac = nv_AB("C", g=ag, d=ab) aa = nv_AB("A", g=ac, d=ae)
Arbre qui sera utilisé pendant cette activité : celui décrit sur le code ci-dessus

Nous ne reviendrons plus sur les trois façons de parcourir un Arbre Binaire en profondeur. Voici simplement les trois exemples pour lequel l'exploration se résume à un simple affichage de la clé ou de l'étiquette des noeuds :

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

2 - Chercher la hauteur de l'Arbre Binaire

Le jour de l'examen, comment parvenir à retrouver la fonction récursive hauteur() ?

Définir le cas de base : quand peut-on répondre définitivement ?

Si l'Arbre Binaire est un arbre VIDE, on peut répondre : la hauteur est -1 si on prend comme convention une profondeur de 0 pour la racine.

Définir le cas récursif

On peut pas répondre immédiatement  il faut relancer des appels récursifs. Lesquels ?

Le cas récursif doit répondre 1 + la plus grande de deux hauteurs à gauche et à droite.

Version Python

1 2 3 4 5 6 7
def hauteur(arbre:'AB', profondeur_vide=-1) -> 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)))

Exemple détaillé avec la recherche de la hauteur de l'Arbre ag, l'Arbre dont la racine est G.

On notera AV l'arbre vide.

1er appel

hauteur(ag) va renvoyer 1 + max(hauteur(gauche(ag)), hauteur(droite(ag))), ce qui devient

hauteur(ag) va renvoyer 1 + max(hauteur(AV), hauteur(ah)).

hauteur(AV) RENVOIE -1 (un cas de base).

hauteur(ah) va renvoyer 1 + max(hauteur(AV), hauteur(AV)) = 1 + max(-1, -1)

-> hauteur(ah) RENVOIE 0

-> hauteur(ag) RENVOIE 1

Réponse finale

Comment rédiger cela sans y passer 15 minutes ? Deux façons de faire :

  1. Réponse textuelle : noter les appels récursifs au fur et à mesure, en laissant un peu de place. De cette façon, on pourra noter les résultats en dessous au fur et à mesure des réponses définitives lors d'un dépilement. C'est une méthode TOP -> DOWN.
  2. Réponse textuelle : idem mais en commençant par fournir les réponses des fonctions les plus basses. C'est une méthode BOTTOM -> UP. Nous allons voir que moins long mais que cela s'écarte de la façon dont la machine traite les appels.
  3. Réponse graphique : encore plus court, mais difficile à comprendre tant qu'on a pas compris la notion d'appels récursifs.

Commençons avec un exemple de rédaction de réponse complète si on vous demande des justifications :

On écrit les appels dans l'ordre d'apparition :

Réponse finale : empilement

Ensuite, on rajoute les réponses lorsqu'elles arrivent lors du dépilement.

Réponse finale : etape 2

Il suffit alors de compléter votre rédaction au fur et à mesure des réponses qu'on parvient à obtenir.

Réponse finale : etape 3

Au final, on aura alors quelque chose qui ressemble à cela :

Réponse finale : etape 4

01° Réaliser l'empilement des appels récursifs de la fonction hauteur() sur l'arbre de racine A, qu'on notera aa. Fournir ensuite la réponse finale que va renvoyer la fonction.

Arbre exercice
1 2 3 4 5 6 7
def hauteur(arbre:'AB', profondeur_vide=-1) -> 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)))

...CORRECTION...

L'empilement provoque des appels à ces fonctions :

Correction question 1

Il reste à dépiler en allant chercher les réponses au fur et à mesure :

Correction question 1-d

02° Réaliser l'empilement des appels récursifs de la fonction hauteur() sur l'arbre aa. Fournir ensuite la réponse finale que va renvoyer la fonction.

Pour cette question :

  • Appliquer d'abord la méthode BOTTOM -> UP
  • Appliquer ensuite la méthode consistant à noter clairement les réponses sur l'arbre lui-même
Arbre de la partie 1

...CORRECTION...

03° En utilisant uniquement les fonctions d'interface (et sans regarder le code déjà fourni), retrouver le code de la fonction récursive hauteur().

Pour cela, demandez-vous quel est (ou sont) le (ou les) cas de base qui permettent de répondre immédiatement. Réflechir ensuite à la réponse qu'on peut fournir dans le cas récursif.

3 - Recherche recursive de la taille de l'Arbre Binaire

La taille correspond au nombre de noeuds.

04° Fournir le code d'une fonction taille() où le seul cas de base est l'arbre vide : on doit renvoyer 0 puisqu'il ne contient pas de noeud.

...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))

05° Fournir le code d'une fonction taille() qui possède deux cas de base : l'arbre vide (on renvoie 0) et la feuille (on renvoie 1).

Pour un humain, est-ce une meilleure ou une moins bonne version que la précédente ?

Pour la machine, est-ce une meilleure ou une moins bonne version que la précédente ?

...CORRECTION...

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

Bien mieux pour un humain qui rédige car on atteint plus vite un cas de base (la feuille) qu'on parvient à détecter visuellement et facilement.

Par contre, pour la machine c'est moins bien : on tombe plus souvent sur des noeuds qui ne sont pas des feuillles. Et donc :

  • Avec cette version à deux cas de base, on effectue 3 vérifications avant de passer la main;
  • Avec la version précédente, on effectue uniquement 1 vérification (suis-le vide ?)

La version à un seul cas de base va donc être plus rapide.

06° Réaliser l'empilement (en TOP -> DOWN) des appels récursifs de la fonction taille() sur l'arbre de racine A, qu'on notera aa. Fournir ensuite la réponse finale que va renvoyer la fonction. On prendra l'algorithme à deux cas de base pour limiter les appels récursifs : sur le cas d'une feuille, on peut répondre immédiatement.

Arbre exercice

...CORRECTION...

Correction question 6

07° Réaliser l'empilement des appels récursifs de la fonction taille() sur l'arbre aa (en BOTTOM -> UP sinon, ça va être long). Fournir ensuite la réponse finale que va renvoyer la fonction. On prendra l'algorithme à deux cas de base pour limiter les appels récursifs : sur le cas d'une feuille, on peut répondre immédiatement.

Finaliser la réponse en notant les réponses sur l'arbre.

Arbre de la partie 1

...CORRECTION...

4 - Recherche récursive

Voyons également comment trouver

  • un noeud possèdant une clé/étiquette précise ?
  • un noeud n'ayant que des sous-arbres vides (une feuille donc) ?

08° Que doit renvoyer la fonction récursive nb_f() qui compte les feuilles sur le cas de base "Arbre Vide" ? Sur le cas de base "Feuille" ? Comment tester qu'un arbre est en réalité constitué uniquement d'une feuille ?

Fournir alors le code de la fonction récursive nb_f().

...CORRECTION...

Si l'arbre est vide : on renvoie 0.

Si l'arbre est constitué uniquement une feuille : on renvoie 1.

L'arbre est constitué d'une feuille si les sous-arbres gauche et droite sont des arbres vides.

Sinon, c'est le cas récursif : on renvoie la somme de l'appel de cette fonction à gauche et de l'appel à droite.

1 2 3 4 5 6 7 8 9
def nb_f(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_f(gauche(arbre)) + nb_f(droite(arbre))

09° Réaliser l'empilement des appels récursifs de la fonction nb_f() sur l'arbre de racine A, qu'on notera aa (en TOP -> DOWN). Fournir ensuite la réponse finale que va renvoyer la fonction.

Arbre exercice

...CORRECTION...

Correction question 9

10° Réaliser l'empilement des appels récursifs de la fonction nb_f() sur l'arbre aa (en BOTTOM -> UP). Fournir ensuite la réponse finale que va renvoyer la fonction. Placer les valeurs sur l'arbre.

Arbre de la partie 1

...CORRECTION...

11° Réaliser le prédicat est_present(). La fonction doit renvoyer True si la clé fournie apparaît bien dans l'Arbre Binaire.

  • Cas de base 1 : si l'arbre est vide, on renvoie False car elle ne peut être là.
  • Cas de base 2 : la clé de la racine a la bonne valeur, 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 : quel est le seul cas où un OU renvoie False ?

1 2 3
def est_present(arbre:'AB', c:'Cle') -> bool: """Prédicat : True si c est la clé d'un des noeuds""" pass

...CORRECTION...

1 2 3 4 5 6 7 8 9
def est_present(arbre:'AB', c:'Cle') -> 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)

12° Réaliser l'empilement des appels récursifs de la fonction est_present() sur l'arbre de racine A en recherchant la clé "B". Fournir ensuite la réponse finale que va renvoyer la fonction. N'oubliez pas que le OU et le ET sont paresseux en Python  : si la lecture du premier terme permet de répondre, on n'évalue pas le deuxième terme.

Arbre exercice

...CORRECTION...

Correction question 12

13° Réaliser l'empilement des appels récursifs de la fonction est_present() sur l'arbre de racine A en recherchant la clé "C". Fournir ensuite la réponse finale que va renvoyer la fonction. N'oubliez pas que le OU et le ET sont paresseux en Python.

Arbre exercice

...CORRECTION...

Correction question 13

5 - FAQ

Rien pour le moment

Il nous reste encore à voir le parcours en largeur puis un type particulier d'Arbre Binaire : l'Arbre Binaire de Recherche.

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