Tous les articles par icnisnboissy

Corrigé exercice 3 les arbres

Q1a
Nombre de nœuds = 7 (taille max pour une hauteur h : 2h – 1 = 1).
Q1b
profondeur maximale du nœud racine = 4
NB : par convention, ici un arbre réduit à un seul nœud est de hauteur 1
Q2
Taille : 7
Hauteur : 3 (par convention h(Δ) = 1)

Q3
Taille : 7
Hauteur : 4

Q4

def hauteur(self):
    return self.racine.hauteur()

Q5

def taille(self) -> int:
   if self.gauche == None and self.droite == None:
      return 1
   elif self.gauche == None:
      return 1 + self.droite.taille()
   elif self.droite == None:
      return 1 + self.gauche.taille()
   else:
      return 1 + self.gauche.taille() + self.droite.taille()
def taille(self):
   return self.racine.taille()

Q6a
2(h-1) – 1 < taille ≤ 2h – 1
doù t_min = 2(h-1) – 1

Q6b

def bien_construit(self) -> bool:
   return 2**(self.racine.hauteur() - 1) - 1 < self.racine.taille() <= 2**self.racine.hauteur() - 1

Corrigé exercice 2 les arbres

1.a. Il y a 4 feuilles, d’étiquette 12, val, 21 et 32.
1.b. Le sous-arbre gauche du nœud 23 est 19-21.
1.c. La hauteur de l’arbre est 4. Sa taille est 9.
1.d. Les valeurs possibles de val sont 16 et 17.

2.a. Parcours infixe : 12-13-15-16-18-19-21-23-32
2.b. Parcours suffixe : 12-13-16-15-21-19-32-23-18

3.a.


3.b.

racine = Noeud(18)
racine.insere([15, 13, 12, 16, 23, 32, 19, 21])

(d’autres solutions sont possibles)

3.c. bloc 3 – bloc 2
ne rentre pas dans bloc 1

4.

class Noeud():
    def __init__(self, v):
        self.ag = None
        self.ad = None
        self.v = v

    def insere(self, v):
        n = self
        est_insere = False
        while not est_insere:
            if v == n.v:
                est_insere = True
            elif v < n.v:
                if n.ag != None:
                    n = n.ag
                else:
                    n.ag = Noeud(v)
                    est_insere = True
            else:
                if n.ad != None:
                    n = n.ad
                else:
                    n.ad = Noeud(v)
                    est_insere = True

    def insere_tout(self, vals):
        for v in vals:
            self.insere(v)

    def recherche(self, v):
        arbre = self
        while not arbre is None:
            if arbre.v == v:
                return True
            if v < arbre.v:
                arbre = arbre.ag
            else:
                arbre = arbre.ad
        return False


    # version récursive (non demandée)

    def recherche_rec(self, v):
        if self is None:
            return False
        if self.v == v:
            return True
        if v < self.v:
            if self.ag is not None:
                return self.ag.recherche_rec(v)
            else:
                return False
        else:
            if self.ad is not None:
                return self.ad.recherche_rec(v)
            else:
                return False


racine = Noeud(18)
racine.insere_tout([12, 13, 15, 14, 19, 21, 32, 23])
print(racine.recherche(149))
print(racine.recherche(12))

Exercices -Arbres

Exercice 1

2021, sujet 0

Question 1

Déterminer la taille et la hauteur de l’arbre binaire suivant : image

Question 2

On décide de numéroter en binaire les nœuds d’un arbre binaire de la façon suivante :

  • la racine correspond à 1 ;
  • la numérotation pour un fils gauche s’obtient en ajoutant le chiffre 0 à droite au numéro de son père ;
  • la numérotation pour un fils droit s’obtient en ajoutant le chiffre 1 à droite au numéro de son père ;

Par exemple, dans l’arbre ci-dessous, on a utilisé ce procédé pour numéroter les nœuds A, B, C, E et F .

image
  1. Dans l’exemple précédent, quel est le numéro en binaire associé au nœud G ?
  2. Quel est le nœud dont le numéro en binaire vaut 13 en décimal ?
  3. En notant h la hauteur de l’arbre, sur combien de bits seront numérotés les nœuds les plus en bas ?
  4. Justifier que pour tout arbre de hauteur h et de taille n ≥ 2, on a : h ≤ n ≤ 2h − 1.

Question 3
Un arbre binaire est dit complet si tous les niveaux de l’arbre sont remplis. image

On décide de représenter un arbre binaire complet par un tableau de taille n + 1, où n est la taille de l’arbre, de la façon suivante :

  • La racine a pour indice 1 ;
  • Le fils gauche du nœud d’indice i a pour indice 2 × i ;
  • Le fils droit du nœud d’indice i a pour indice 2 × i + 1 ;
  • On place la taille n de l’arbre dans la case d’indice 0

Répondre aux questions suivantes :

  1. Déterminer le tableau qui représente l’arbre binaire complet de l’exemple précédent.
  2. On considère le père du nœud d’indice i avec i ≥ 2. Quel est son indice dans le tableau ?

Question 4

On se place dans le cas particulier d’un arbre binaire de recherche complet où les nœuds contiennent des entiers et pour lequel la valeur de chaque noeud est supérieure à celles des noeuds de son fils gauche, et inférieure à celles des noeuds de son fils droit.

Écrire une fonction recherche ayant pour paramètres un arbre arbre et un élément element. Cette fonction renvoie True si element est dans l’arbre et False sinon. L’arbre sera représenté par un tableau comme dans la question précédente. corrigé

Corrigé

merci à G.Lassus

Exercice 2

2021, Métropole sujet 1, exercice 1

Dans cet exercice, les arbres binaires de recherche ne peuvent pas comporter plusieurs fois la même clé. De plus, un arbre binaire de recherche limité à un nœud a une hauteur de 1. On considère l’arbre binaire de recherche représenté ci-dessous (figure 1), où val représente un entier :

image

1.a Donner le nombre de feuilles de cet arbre et préciser leur valeur (étiquette).

1.b Donner le sous arbre-gauche du nœud 23.

1.c Donner la hauteur et la taille de l’arbre.

1.d Donner les valeurs entières possibles de val pour cet arbre binaire de recherche.

On suppose, pour la suite de cet exercice, que val est égal à 16.

2. On rappelle qu’un parcours infixe depuis un nœud consiste, dans l’ordre, à faire un parcours infixe sur le sous arbre-gauche, afficher le nœud puis faire un parcours infixe sur le sous-arbre droit.
Dans le cas d’un parcours suffixe, on fait un parcours suffixe sur le sous-arbre gauche puis un parcours suffixe sur le sous-arbre droit, avant d’afficher le nœud.

a. Donner les valeurs d’affichage des nœuds dans le cas du parcours infixe de l’arbre.
b. Donner les valeurs d’affichage des nœuds dans le cas du parcours suffixe de l’arbre.

3. On considère la classe Noeud définie de la façon suivante en Python :

image

a. Représenter l’arbre construit suite à l’exécution de l’instruction suivante :

racine = Noeud(18)
racine.insere_tout([12, 13, 15, 16, 19, 21, 32, 23])

b. Écrire les deux instructions permettant de construire l’arbre de la figure 1. On rappelle que le nombre val est égal à 16.

c. On considère l’arbre tel qu’il est présenté sur la figure 1. Déterminer l’ordre d’exécution des blocs (repérés de 1 à 3) suite à l’application de la méthode insere(19) au nœud racine de cet arbre.

4. Écrire une méthode recherche(self, v) qui prend en argument un entier v et renvoie la valeur True si cet entier est une étiquette de l’arbre, False sinon.

corrigé

Exercice 3

2021, Métropole Candidats Libres 2, exercice 3

On rappelle qu’un arbre binaire est composé de nœuds, chacun des nœuds possédant éventuellement un sous-arbre gauche et éventuellement un sous-arbre droit. Un nœud sans sous-arbre est appelé feuille. La taille d’un arbre est le nombre de nœuds qu’il contient ; sa hauteur est le nombre de nœuds du plus long chemin qui joint le nœud racine à l’une des feuilles. Ainsi la hauteur d’un arbre réduit à un nœud, c’est-à-dire la racine, est 1.

Dans un arbre binaire de recherche, chaque nœud contient une clé, ici un nombre entier, qui est :

  • strictement supérieure à toutes les clés des nœuds du sous-arbre gauche ;
  • strictement inférieure à toutes les clés des nœuds du sous-arbre droit.

Un arbre binaire de recherche est dit « bien construit » s’il n’existe pas d’arbre de hauteur inférieure qui pourrait contenir tous ses nœuds.

On considère l’arbre binaire de recherche ci-dessous.

image

1.a. Quelle est la taille de l’arbre ci-dessus ?

1.b. Quelle est la hauteur de l’arbre ci-dessus ?

2. Cet arbre binaire de recherche n’est pas « bien construit ». Proposer un arbre binaire de recherche contenant les mêmes clés et dont la hauteur est plus petite que celle de l’arbre initial.

3. Les classes Noeud et Arbre ci-dessous permettent de mettre en œuvre en Python la structure d’arbre binaire de recherche. La méthode insere permet d’insérer récursivement une nouvelle clé.

class Noeud :

    def __init__(self, cle):
        self.cle = cle
        self.gauche = None
        self.droit = None

    def insere(self, cle):
        if cle < self.cle :
            if self.gauche == None :
                self.gauche = Noeud(cle)
            else :
                self.gauche.insere(cle)
        elif cle > self.cle :
            if self.droit == None :
                self.droit = Noeud(cle)
            else :
                self.droit.insere(cle)

class Arbre :

    def __init__(self, cle):
        self.racine = Noeud(cle)

    def insere(self, cle):
        self.racine.insere(cle)

Donner la représentation de l’arbre codé par les instructions ci-dessous.

a = Arbre(10)
a.insere(20)
a.insere(15)
a.insere(12)
a.insere(8)
a.insere(4)
a.insere(5)

4. Pour calculer la hauteur d’un arbre non vide, on a écrit la méthode ci-dessous dans la classe Noeud.

def hauteur(self):
    if self.gauche == None and self.droit == None:
        return 1
    if self.gauche == None:
        return 1 + self.droit.hauteur()
    elif self.droit == None:
        return 1 + self.gauche.hauteur()
    else:
        hg = self.gauche.hauteur()
        hd = self.droit.hauteur()
        if hg > hd:
            return hg + 1
        else:
            return hd + 1

Écrire la méthode hauteur de la classe Arbre qui renvoie la hauteur de l’arbre.

5. Écrire les méthodes taille des classes Noeud et Arbre permettant de calculer la taille d’un arbre. corrigé Méthode de la classe :

6. On souhaite écrire une méthode bien_construit de la classe Arbre qui renvoie la valeur True si l’arbre est « bien construit » et False sinon.

On rappelle que la taille maximale d’un arbre binaire de recherche de hauteur est 2h-1.

.

6.a Quelle est la taille minimale, notée min d’un arbre binaire de recherche « bien construit » de hauteur h?

6.b Écrire la méthode bien_construit demandée.

corrigé

Corrigé exercice 1 les arbres

Q1 La taille est 9, la hauteur est 4.
Q2 1. G est associé à 1010.
Q2 2. 13 s’écrit 1101 en binaire, c’est donc le nœud I.
Q2 3. Les nœuds les plus en bas sont notés sur bits.
Q2 4. L’arbre de hauteur de taille minimale est l’arbre filiforme, qui est de taille .
L’arbre de hauteur de taille maximale est l’arbre complet, qui est de taille . Si est la taille d’un arbre quelconque de taille

, on a donc bien $$ h \leqslant n \leqslant 2^h-1 $$.

Q3 1. Tableau : [15, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O] .
Q3 2. Le père du nœud d’indice i a pour indice i//2.

Q4 :





def recherche(arbre, element):
    i = 1
    while i < len(arbre):
        if arbre[i] == element:
            return True
        if element < arbre[i]:
            i = 2*i # on se place sur le fils gauche
        else:
            i = 2*i +  1 # on se place sur le fils droit
    return False

Outils en ligne

Environnement de développement Python en ligne pour travailler la programmation sur n’importe quel appareil sans installation

Replit.com

Environnement Linux (Debian) en ligne :

  • Cocalc (l’enregistrement est facultatif mais vous permettra de retrouver votre travail).
  • Webminal est une alternative très intéressante mais l’enregistrement est obligatoire et nécessite une adresse mail.

Le principe du “diviser pour régner” en Python EXERCICES

Exercice 1: Recherche valeur dans une liste

Algorithme:

fonction chercher : (tableau, clé) —-> booléen
1. Pour un tableau de taille 1, il contient la clé si valeur est la clé
2. On sépare le tableau en deux parties sensiblement de même taille (gauche et droite)
3. Le tableau contient la clé si
chercher(gauche, clé) ou chercher(droite, clé)
est vrai.


Exemple
tableau = [4, 10, 20, 5]
clé = 10
A-t-on clé dans tableau ?
Exemple
clé = 10
[4, 10, 20, 5]

diviser [4, 10] [20, 5]

diviser [4] [10] [20] [5]

combiner Faux ou Vrai Faux ou Faux

combiner Vrai ou Faux

combiner Vrai

Implémenter cette fonction sous python.

Exercice 2: Calcul y puissance x

On souhaite calculer N=752. La méthode basique consiste à multiplier le nombre 7 par lui-même 52 fois… Ce qui n’est pas très rapide !

L’idée consiste donc à diviser le problème en 2 : on va calculer  726× 726, c’est-à-dire( 726)2. Là, il n’y a plus que 26 + 1 opérations (26 multiplications pour calculer 726, et une dernière pour faire le carré du résultat.

On recommence ensuite avec 726 : on le calcule en faisant (713)2.

 N se calcule alors en 13+1+1 opérations au lieu de 52 initialement.

On ne s’arrête pas là, bien entendu : on continue tant que l’on peut utiliser ce principe :

N=752

=(726)2

=((713)2)2

=[[(76)2×7]2]2

=[[((73)2)2×7]2]2

=[[((72×7)2)2×7]2]2

Le principe est, vous l’avez peut-être remarqué, récursif.

Implémentation en Python du “diviser pour régner”

Nous allons écrire une fonction “puissance(x,n)” basée sur ce paradigme, où x et n sont deux entiers (positif pour n). Pour cela, nous allons prendre en compte que:

Implémenter cette fonction avec python

Exercice 3: Faire pivoter une image

Méthode diviser pour régner

Diviser pour régner

Le diviser pour régner est une méthode algorithmique basée sur le principe suivant :

On prend un problème (généralement complexe à résoudre), on divise ce problème en une multitude de petits problèmes, l’idée étant que les « petits problèmes » seront plus simples à résoudre que le problème original. Une fois les petits problèmes résolus, on recombine les « petits problèmes résolus » afin d’obtenir la solution du problème de départ.

Le paradigme « diviser pour régner » repose donc sur 3 étapes :

  • DIVISER : le problème d’origine est divisé en un certain nombre de sous-problèmes
  • RÉGNER : on résout les sous-problèmes (les sous-problèmes sont plus faciles à résoudre que le problème d’origine)
  • COMBINER : les solutions des sous-problèmes sont combinées afin d’obtenir la solution du problème d’origine.

Les algorithmes basés sur le paradigme « diviser pour régner » sont très souvent des algorithmes récursifs.

Nous allons maintenant étudier un de ces algorithmes basés sur le principe diviser pour régner : le tri-fusion

Tri-fusion

Nous avons déjà étudié des algorithmes de tri : le tri par insertion et le tri par sélection. Nous allons maintenant étudier une nouvelle méthode de tri, le tri-fusion. Comme pour les algorithmes déjà étudiés, cet algorithme de tri fusion prend en entrée un tableau non trié et donne en sortie, le même tableau, mais trié.

À faire vous-même 1

Étudiez cet algorithme :

TRI FUSION (tableau):
• Si tableau est de taille <= 1 on ne fait rien.
• Sinon, On sépare tableau en 2 parties gauche et droite,
• On appelle Tri fusion sur gauche et sur droite
• On fusionne gauche et droite dans tableau

FUSIONNER (`tableau`, `gauche`, `droite`):
* On parcourt les deux tableaux `gauche` et `droite` en même temps,
Pour chaque paire d'éléments, on place le plus petit dans tableau.
* S'il reste des éléments dans `gauche` ou dans `droite` on les place à la fin
de tableau		

Pour trier un tableau A, on fait l’appel initial TRI-FUSION(A, 1, A.longueur)

Rappel : Attention, en algorithmique, les indices des tableaux commencent à 1


Cet algorithme est un peu difficile à appréhender, on notera qu’il est composé de deux fonctions FUSIONNER et TRI-FUSION (fonction récursive). La fonction TRI-FUSION assure la phase « DIVISER » et la fonction FUSION assure les phases « RÉGNER » et « COMBINER ».

Voici un exemple d’application de cet algorithme sur le tableau A = [23, 12, 4, 56, 35, 32, 42, 57, 3] :

À faire vous-même 2

Étudiez attentivement le schéma ci-dessous afin de mieux comprendre le principe du tri-fusion (identifiez bien les phases « DIVISER » et « COMBINER »).


On remarque que dans le cas du tri-fusion, la phase « RÉGNER » se réduit à sa plus simple expression, en effet, à la fin de la phase « DIVISER », nous avons à trier des tableaux qui comportent un seul élément, ce qui est évidemment trivial.

À faire vous-même 3

Reprenez tout le raisonnement qui vient d’être fait sur le tableau T = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]. Vous n’hésiterez pas à faire un schéma et à expliquer la fusion de 2 tableaux triés.


Nous avons vu que le tri par insertion et tri par sélection ont tous les deux une complexité O(n2)

. Qu’en est-il pour le tri-fusion ?

Le calcul rigoureux de la complexité de cet algorithme sort du cadre de ce cours. Mais, en remarquant que la première phase (DIVISER) consiste à « couper » les tableaux en deux plusieurs fois de suite, intuitivement, on peut dire qu’un logarithme base 2 doit intervenir. La deuxième phase consiste à faire des comparaisons entre les premiers éléments de chaque tableau à fusionner, on peut donc supposer que pour un tableau de n éléments, on aura n comparaisons. En combinant ces 2 constations on peut donc dire que la complexité du tri-fusion est en O(n.log(n))

(encore une fois la « démonstration » proposée ici n’a rien de rigoureux).

La comparaison des courbes de la fonction n2 (en rouge) et n.log(n)

(en bleu) :

nous montre que l’algorithme de tri-fusion est plus « efficace » que l’algorithme de tri par insertion ou que l’algorithme de tri par sélection.

Présentation vidéo détaillée du tri fusion

Utilisation du tri fusion:
Contrairement au tri par sélection ou par insertion, le tri fusion est réellement utilisé en pratique.
Il a de nombreux avantages :
• complexité optimale (cela ne signifie pas qu’il est le plus rapide)
• stable (voir plus bas)
• facile à mettre en œuvre
Cependant, il est possible d’améliorer la méthode :
timsort, le tri natif en Python et Javascript utilise une combinaison du tri fusion et du tri par insertion.


Exercices Diviser pour régner


FICHE REVISION

Auteur : David Roche

Projet : implémentation en Python des algorithmes sur les arbres binaires

Le but de ce projet est d’implémenter en Python les algorithmes sur les arbres binaires précédemment étudiés. Il sera donc sans doute nécessaire de reprendre ce qui a été vu sur la structure de données « arbre » et sur « les algorithmes sur les arbres binaires ».

Comme nous l’avons déjà dit, Python ne propose pas de structure de données permettant d’implémenter directement les arbres binaires. Il va donc être nécessaire de créer cette structure. Pour programmer ce type de structure, nous allons utiliser le paradigme objet (à voir si pas encore étudier paradigme objet)

Vous trouverez ci-dessous la classe « ArbreBinaire » qui va nous permettre d’implémenter des arbres binaires.


class ArbreBinaire:
    def __init__(self, valeur):
        self.valeur = valeur
        self.enfant_gauche = None
        self.enfant_droit = None

    def insert_gauche(self, valeur):
        if self.enfant_gauche == None:
            self.enfant_gauche = ArbreBinaire(valeur)
        else:
            new_node = ArbreBinaire(valeur)
            new_node.enfant_gauche = self.enfant_gauche
            self.enfant_gauche = new_node

    def insert_droit(self, valeur):
        if self.enfant_droit == None:
            self.enfant_droit = ArbreBinaire(valeur)
        else:
            new_node = ArbreBinaire(valeur)
            new_node.enfant_droit = self.enfant_droit
            self.enfant_droit = new_node

   def get_valeur(self):
        return self.valeur

   def get_gauche(self):
        return self.enfant_gauche

   def get_droit(self):
        return self.enfant_droit
			

À faire vous-même 1

Étudiez attentivement la classe « ArbreBinaire » (méthodes et attributs). Vous pouvez, par exemple, vous interroger sur l’utilité de toutes les méthodes de cette classe.


Voici un exemple d’utilisation de cette classe pour construire un arbre binaire :

Soit l’arbre binaire suivant : Arbre 1

Voici le programme qui va permettre de construire cet arbre à l’aide de la classe « ArbreBinaire » :


class ArbreBinaire:
   def __init__(self, valeur):
      self.valeur = valeur
      self.enfant_gauche = None
      self.enfant_droit = None

   def insert_gauche(self, valeur):
      if self.enfant_gauche == None:
         self.enfant_gauche = ArbreBinaire(valeur)
      else:
         new_node = ArbreBinaire(valeur)
         new_node.enfant_gauche = self.enfant_gauche
         self.enfant_gauche = new_node

   def insert_droit(self, valeur):
      if self.enfant_droit == None:
         self.enfant_droit = ArbreBinaire(valeur)
      else:
         new_node = ArbreBinaire(valeur)
         new_node.enfant_droit = self.enfant_droit
         self.enfant_droit = new_node

   def get_valeur(self):
      return self.valeur

   def get_gauche(self):
      return self.enfant_gauche

   def get_droit(self):
      return self.enfant_droit

#######fin de la classe########

######début de la construction de l'arbre binaire###########

racine = ArbreBinaire('A')
racine.insert_gauche('B')
racine.insert_droit('F')

b_node = racine.get_gauche()
b_node.insert_gauche('C')
b_node.insert_droit('D')

f_node = racine.get_droit()
f_node.insert_gauche('G')
f_node.insert_droit('H')

c_node = b_node.get_gauche()
c_node.insert_droit('E')

g_node = f_node.get_gauche()
g_node.insert_gauche('I')

h_node = f_node.get_droit()
h_node.insert_droit('J')

######fin de la construction de l'arbre binaire###########

À faire vous-même 2

Étudiez attentivement le programme ci-dessus afin de comprendre le principe de « construction d’un arbre binaire »


Il est possible d’afficher un arbre binaire dans la console Python, pour cela, nous allons écrire une fonction « affiche ». Cette fonction renvoie une série de tuples de la forme (valeur,arbre_gauche, arbre_droite), comme « arbre_gauche » et « arbre_droite » seront eux-mêmes affichés sous forme de tuples, on aura donc un affichage qui ressemblera à : (valeur,(valeur_gauche,arbre_gauche_gauche,arbre_gauche_droite),(valeur_droite,arbre_droite_gauche,arbre_droite_droite)), mais comme « arbre_gauche_gauche » sera lui-même représenté par un tuple… Nous allons donc avoir des tuples qui contiendront des tuples qui eux-mêmes contiendront des tuples…

Pour l’arbre binaire défini ci-dessus, on aura :


('A', ('B', ('C', None, ('E', None, None)), ('D', None, None)), ('F', ('G', ('I', None, None), None), ('H', None, ('J', None, None))))
			

Voici le programme augmenté de la fonction « affiche » :


class ArbreBinaire:
   def __init__(self, valeur):
      self.valeur = valeur
      self.enfant_gauche = None
      self.enfant_droit = None

   def insert_gauche(self, valeur):
      if self.enfant_gauche == None:
         self.enfant_gauche = ArbreBinaire(valeur)
      else:
         new_node = ArbreBinaire(valeur)
         new_node.enfant_gauche = self.enfant_gauche
         self.enfant_gauche = new_node

   def insert_droit(self, valeur):
      if self.enfant_droit == None:
         self.enfant_droit = ArbreBinaire(valeur)
      else:
         new_node = ArbreBinaire(valeur)
         new_node.enfant_droit = self.enfant_droit
         self.enfant_droit = new_node

   def get_valeur(self):
      return self.valeur

   def get_gauche(self):
      return self.enfant_gauche

   def get_droit(self):
      return self.enfant_droit

#######fin de la classe########

######début de la construction de l'arbre binaire###########

racine = ArbreBinaire('A')
racine.insert_gauche('B')
racine.insert_droit('F')

b_node = racine.get_gauche()
b_node.insert_gauche('C')
b_node.insert_droit('D')

f_node = racine.get_droit()
f_node.insert_gauche('G')
f_node.insert_droit('H')

c_node = b_node.get_gauche()
c_node.insert_droit('E')

g_node = f_node.get_gauche()
g_node.insert_gauche('I')

h_node = f_node.get_droit()
h_node.insert_droit('J')

######fin de la construction de l'arbre binaire###########

def affiche(T):
   if T != None:
      return (T.get_valeur(),affiche(T.get_gauche()),affiche(T.get_droit()))

À faire vous-même 3

Vérifiez que « affiche(racine) » renvoie bien :


('A', ('B', ('C', None, ('E', None, None)), ('D', None, None)), ('F', ('G', ('I', None, None), None), ('H', None, ('J', None, None))))

N.B : la fonction « affiche » n’a pas une importance fondamentale, elle sert uniquement à vérifier que les arbres programmés sont bien corrects.

À faire vous-même 4

Programmez à l’aide de la classe « ArbreBinaire », l’arbre binaire suivant :

Arbre 2

Vérifiez votre programme à l’aide de la fonction « affiche »


Vous allez maintenant pouvoir commencer à travailler sur l’implémentation des algorithmes sur les arbres binaires :

À faire vous-même 5

Programmez la fonction « hauteur » qui prend un arbre binaire T en paramètre et renvoie la hauteur de T (algorithme correspondant, voir ici)

Testez votre fonction en utilisant l’arbre vu plus haut (schéma « Arbre 1 »).


À faire vous-même 6

Programmez la fonction « taille » qui prend un arbre binaire T en paramètre et renvoie la taille de T (algorithme correspondant, voir ici)

Testez votre fonction en utilisant l’arbre vu plus haut (schéma « Arbre 1 »).


À faire vous-même 7

Programmez la fonction « parcours_infixe » qui prend un arbre binaire T en paramètre et qui permet d’obtenir le parcours infixe de l’arbre T (algorithme correspondant, voir ici)

Testez votre fonction en utilisant l’arbre vu plus haut (schéma « Arbre 1 »).


À faire vous-même 8

Programmez la fonction « parcours_prefixe » qui prend un arbre binaire T en paramètre et qui permet d’obtenir le parcours préfixe de l’arbre T (algorithme correspondant, voir ici)

Testez votre fonction en utilisant l’arbre vu plus haut (schéma « Arbre 1 »).


À faire vous-même 9

Programmez la fonction « parcours_suffixe » qui prend un arbre binaire T en paramètre et qui permet d’obtenir le parcours suffixe de l’arbre T (algorithme correspondant, voir ici)

Testez votre fonction en utilisant l’arbre vu plus haut (schéma « Arbre 1 »).


À faire vous-même 10

Programmez la fonction « parcours_largeur » qui prend un arbre binaire T en paramètre et qui permet d’obtenir le parcours en largeur de l’arbre T (algorithme correspondant, voir ici)

Testez votre fonction en utilisant l’arbre vu plus haut (schéma « Arbre 1 »).


Nous allons maintenant travailler sur les arbres binaires de recherche.

À faire vous-même 11

Programmez, à l’aide de la classe « ArbreBinaire », l’arbre binaire de recherche ci-dessous : Arbre 3

Vérifiez votre réponse à l’aide de la fonction « affichage »


À faire vous-même 12

Afin de vérifier que l’arbre binaire « Arbre 3 » est bien un arbre binaire de recherche, utilisez la fonction « parcours_infixe » programmée dans le « À faire vous-même 7 ».


À faire vous-même 13

Programmez la fonction « arbre_recherche » qui prend un arbre binaire T et un entier k en paramètres et qui renvoie True si k appartient à T et False dans le cas contraire (algorithme correspondant, voir ici)

Testez votre fonction en utilisant l’arbre vu plus haut (schéma « Arbre 3 ») avec k = 13 et k = 16.


À faire vous-même 14

Programmez la fonction « arbre_recherche_ite » (version itérative de la fonction « arbre_recherche ») qui prend un arbre binaire T et un entier k en paramètres et qui renvoie True si k appartient à T et False dans le cas contraire (algorithme correspondant, voir ici)

Testez votre fonction en utilisant l’arbre vu plus haut (schéma « Arbre 3 ») avec k = 13 et k = 16.


À faire vous-même 15

Programmez la fonction « arbre_insertion » qui prend T (un arbre binaire) et y (un objet de type « ArbreBinaire ») en paramètres et qui insert y dans T (algorithme correspondant, voir ici)

Testez votre fonction en utilisant l’arbre vu plus haut (schéma « Arbre 3 ») avec y.valeur = 16.


Auteur : David Roche

Algorithme sur les arbres binaires

Merci de revoir le courts sur les arbres.

Avant d’entrer dans le vif du sujet (les algorithmes), nous allons un peu approfondir la notion d’arbre binaire :

À chaque nœud d’un arbre binaire, on associe une clé (« valeur » associée au nœud on peut aussi utiliser le terme « valeur » à la place de clé), un « sous-arbre gauche » et un « sous-arbre droit »

Soit l’arbre binaire suivant :

si on prend le nœud ayant pour clé A (le nœud racine de l’arbre) on a :

  • le sous-arbre gauche est composé du nœud ayant pour clé B, du nœud ayant pour clé C, du nœud ayant pour clé D et du nœud ayant pour clé E
  • le sous-arbre droit est composé du nœud ayant pour clé F, du nœud ayant pour clé G, du nœud ayant pour clé H, du nœud ayant pour clé I et du nœud ayant pour clé J

si on prend le noeud ayant pour clé B on a :

  • le sous-arbre gauche est composé du nœud ayant pour clé C et du nœud ayant pour clé E
  • le sous-arbre droit est uniquement composé du nœud ayant pour clé D

Un arbre (ou un sous-arbre) vide est noté NIL (NIL est une abréviation du latin nihil qui veut dire « rien »)

si on prend le nœud ayant pour clé G on a :

  • le sous-arbre gauche est uniquement composé du nœud ayant pour clé I
  • le sous-arbre droit est vide (NIL)

Il faut bien avoir en tête qu’un sous-arbre (droite ou gauche) est un arbre (même s’il contient un seul nœud ou pas de nœud de tout (NIL)).

Soit un arbre T : T.racine correspond au nœud racine de l’arbre T

Soit un nœud x :

  • x.gauche correspond au sous-arbre gauche du nœud x
  • x.droit correspond au sous-arbre droit du nœud x
  • x.clé correspond à la clé du nœud x

Il faut noter que si le nœud x est une feuille, x.gauche et x.droite sont des arbres vides (NIL)

Calculer la hauteur d’un arbre

Nous allons commencer à travailler sur les algorithmes en nous intéressant à l’algorithme qui permet de calculer la hauteur d’un arbre :

À faire vous-même 1

Étudiez cet algorithme :


VARIABLE
T : arbre
x : noeud

DEBUT
HAUTEUR(T) :
  si T ≠ NIL :
    x ← T.racine
    renvoyer 1 + max(HAUTEUR(x.gauche), HAUTEUR(x.droit))
  sinon :
    renvoyer 0
  fin si
FIN
		

N.B. la fonction max renvoie la plus grande valeur des 2 valeurs passées en paramètre (exemple : max(5,6) renvoie 6)

Cet algorithme est loin d’être simple, n’hésitez pas à écrire votre raisonnement sur une feuille de brouillon. Vous pourrez par exemple essayer d’appliquer cet algorithme sur l’arbre binaire ci-dessous. N’hésitez pas à poser des questions si nécessaire.

Si vraiment vous avez des difficultés à comprendre le fonctionnement de l’algorithme sur l’arbre ci-dessus, vous trouverez ici un petit calcul qui pourrait vous aider.


Comme vous l’avez sans doute remarqué, nous avons dans l’algorithme ci-dessus une fonction récursive. Vous aurez l’occasion de constater que c’est souvent le cas dans les algorithmes qui travaillent sur des structures de données telles que les arbres.

Calculer la taille d’un arbre

Nous allons maintenant étudier un algorithme qui permet de calculer le nombre de nœuds présents dans un arbre.

À faire vous-même 2

Étudiez cet algorithme :


VARIABLE
T : arbre
x : noeud

DEBUT
TAILLE(T) :
  si T ≠ NIL :
    x ← T.racine
    renvoyer 1 + TAILLE(x.gauche) + TAILLE(x.droit)
  sinon :
    renvoyer 0
  fin si
FIN
		

Cet algorithme ressemble beaucoup à l’algorithme étudié au « À faire vous-même 1 », son étude ne devrait donc pas vous poser de problème.

Appliquez cet algorithme à l’exemple suivant :


Il existe plusieurs façons de parcourir un arbre (parcourir un arbre = passer par tous les nœuds), nous allons en étudier quelques-unes :

Parcourir un arbre dans l’ordre infixe

À faire vous-même 3

Étudiez cet algorithme :


VARIABLE
T : arbre
x : noeud

DEBUT
PARCOURS-INFIXE(T) :
  si T ≠ NIL :
    x ← T.racine
    PARCOURS-INFIXE(x.gauche)
    affiche x.clé
    PARCOURS-INFIXE(x.droit)
  fin si
FIN
		

Vérifiez qu’en appliquant l’algorithme ci-dessus, l’arbre ci-dessous est bien parcouru dans l’ordre suivant : C, E, B, D, A, I, G, F, H, J


Parcourir un arbre dans l’ordre préfixe

À faire vous-même 4

Étudiez cet algorithme :


VARIABLE
T : arbre
x : noeud

DEBUT
PARCOURS-PREFIXE(T) :
  si T ≠ NIL :
    x ← T.racine
    affiche x.clé
    PARCOURS-PREFIXE(x.gauche)
    PARCOURS-PREFIXE(x.droit)
  fin si
FIN
		

Vérifiez qu’en appliquant l’algorithme ci-dessus, l’arbre ci-dessous est bien parcouru dans l’ordre suivant : A, B, C, E, D, F, G, I, H, J


Parcourir un arbre dans l’ordre suffixe

À faire vous-même 5

Étudiez cet algorithme :


VARIABLE
T : arbre
x : noeud

DEBUT
PARCOURS-SUFFIXE(T) :
  si T ≠ NIL :
    x ← T.racine
    PARCOURS-SUFFIXE(x.gauche)
    PARCOURS-SUFFIXE(x.droit)
    affiche x.clé
  fin si
FIN
		

Vérifiez qu’en appliquant l’algorithme ci-dessus, l’arbre ci-dessous est bien parcouru dans l’ordre suivant : E, C, D, B, I, G, J, H, F, A


Le choix du parcours infixe, préfixe ou suffixe dépend du problème à traiter, on pourra retenir pour les parcours préfixe et suffixe (le cas du parcours infixe sera traité un peu plus loin) que :

  • dans le cas du parcours préfixe, un nœud est affiché avant d’aller visiter ces enfants
  • dans le cas du parcours suffixe, on affiche chaque nœud après avoir affiché chacun de ses fils

Parcourir un arbre en largeur d’abord

À faire vous-même 6

Étudiez cet algorithme :


VARIABLE
T : arbre
Tg : arbre
Td : arbre
x : noeud
f : file (initialement vide)

DEBUT
PARCOURS-LARGEUR(T) :
  enfiler(T, f) //on place la racine dans la file
  tant que f non vide :
    x ← defiler(f)
    affiche x.clé
    si x.gauche ≠ NIL :
      Tg ← x.gauche
      enfiler(Tg, f)
    fin si
    si x.droit ≠ NIL :
      Td ← x.droite
      enfiler(Td, f)
    fin si
  fin tant que
FIN
		

Vérifiez qu’en appliquant l’algorithme ci-dessus, l’arbre ci-dessous est bien parcouru dans l’ordre suivant : A, B, F, C, D, G, H, E, I, J

Selon vous, pourquoi parle-t-on de parcours en largeur ?


Il est important de bien noter l’utilisation d’une file (FIFO) pour cet algorithme de parcours en largeur. Vous noterez aussi que cet algorithme n’utilise pas de fonction récursive.

Arbre binaire de recherche

Un arbre binaire de recherche est un cas particulier d’arbre binaire. Pour avoir un arbre binaire de recherche :

  • il faut avoir un arbre binaire !
  • il faut que les clés de nœuds composant l’arbre soient ordonnables (on doit pouvoir classer les nœuds, par exemple, de la plus petite clé à la plus grande)
  • soit x un noeud d’un arbre binaire de recherche. Si y est un nœud du sous-arbre gauche de x, alors il faut que y.clé ⩽ x.clé. Si y est un nœud du sous-arbre droit de x, il faut alors que x.clé ⩽ y.clé

À faire vous-même 7

Vérifiez que l’arbre ci-dessus est bien un arbre binaire de recherche.

À faire vous-même 8

Appliquez l’algorithme de parcours infixe sur l’arbre ci-dessous :

Que remarquez-vous ?


Recherche d’une clé dans un arbre binaire de recherche

Nous allons maintenant étudier un algorithme permettant de rechercher une clé de valeur k dans un arbre binaire de recherche. Si k est bien présent dans l’arbre binaire de recherche, l’algorithme renvoie vrai, dans le cas contraire, il renvoie faux.

À faire vous-même 9

Étudiez l’algorithme suivant:


VARIABLE
T : arbre
x : noeud
k : entier
DEBUT
ARBRE-RECHERCHE(T,k) :
  si T == NIL :
    renvoyer faux
  fin si
  x ← T.racine
  si k == x.clé :
    renvoyer vrai
  fin si
  si k < x.clé :
    ARBRE-RECHERCHE(x.gauche,k)
  sinon :
    ARBRE-RECHERCHE(x.droit,k)
  fin si
FIN
		

À faire vous-même 10

Appliquez l’algorithme de recherche d’une clé dans un arbre binaire de recherche sur l’arbre ci-dessous. On prendra k = 13.


À faire vous-même 11

Appliquez l’algorithme de recherche d’une clé dans un arbre binaire de recherche sur l’arbre ci-dessous. On prendra k = 16.


Cet algorithme de recherche d’une clé dans un arbre binaire de recherche ressemble beaucoup à la recherche dichotomique vue en première. C’est principalement pour cette raison qu’en général, la complexité en temps dans le pire des cas de l’algorithme de recherche d’une clé dans un arbre binaire de recherche est O(log2(n))

À noter qu’il existe une version dite « itérative » (qui n’est pas récursive) de cet algorithme de recherche :

À faire vous-même 12

Étudiez l’algorithme suivant:


VARIABLE
T : arbre
x : noeud
k : entier
DEBUT
ARBRE-RECHERCHE_ITE(T,k) :
  x ← T.racine
  tant que T ≠ NIL et k ≠ x.clé :
    x ← T.racine
    si k < x.clé :
      T ← x.gauche
    sinon :
      T ← x.droit
    fin si
  fin tant que
  si k == x.clé :
    renvoyer vrai
  sinon :
    renvoyer faux
  fin si
FIN
		

Insertion d’une clé dans un arbre binaire de recherche

Il est tout à fait possible d’insérer un nœud y dans un arbre binaire de recherche (non vide) :

À faire vous-même 13

Étudiez l’algorithme suivant:


VARIABLE
T : arbre
x : noeud
y : noeud
DEBUT
ARBRE-INSERTION(T,y) :
  x ← T.racine
  tant que T ≠ NIL :
    x ← T.racine
    si y.clé < x.clé :
      T ← x.gauche
    sinon :
      T ← x.droit
    fin si
  fin tant que
  si y.clé < x.clé :
    insérer y à gauche de x
  sinon :
    insérer y à droite de x
  fin si
FIN
		

À faire vous-même 14

Appliquez l’algorithme d’insertion d’un nœud y dans un arbre binaire de recherche sur l’arbre ci-dessous. On prendra y.clé = 16.


Merci à l’auteur : David Roche

Algorithmes gloutons


Présentation des d’algorithmes gloutons avec exemples et TD. cet article est une copie d’un article du site www.math93.com/ , vous pouvez le voir ici .

Voici une petite vidéo sur les algorithmes Glouton

  1. Présentation de la notion d’algorithme glouton.
    1. Les problèmes d’optimisation.
    2. Résoudre un problème d’optimisation : les algorithmes gloutons
  2. Le problème du rendu de monnaie.
  3. Le TD sur les algorithmes gloutons et le rendu de monnaie.

1. Présentation de la notion d’algorithme glouton


1.1. Les problèmes d’optimisation

L’optimisation est une branche des mathématiques cherchant à modéliser, à analyser et à résoudre les problèmes qui consistent à minimiser ou maximiser une fonction sur un ensemble.

Les problèmes d’optimisation classiques sont par exemple : 

  • la répartition optimale de tâches suivant des critères précis (emploi du temps avec plusieurs contraintes) ;
  • le problème du rendu de monnaie ;
  • la recherche d’un plus court chemin dans un graphe ;
  • le problème du voyageur de commerce.

1.2. Résoudre un problème d’optimisation : les algorithmes gloutons

De nombreuses techniques informatiques sont susceptibles d’apporter une solution exacte ou approchée à ces problèmes.

  • Recherche de toutes les solutions
    La technique la plus basique pour résoudre ce type de problème d’optimisation consiste à énumérer de façon exhaustive toutes les solutions possibles, puis à choisir la meilleure. Cette approche par force brute, impose souvent un coût en temps trop important pour être utilisée.
        
  • Les algorithmes gloutons
    Un algorithme glouton (greedy algorithm) est un algorithme qui suit le principe de faire, étape par étape, un choix optimum local.
    Au cours de la construction de la solution, l’algorithme résout une partie du problème puis se focalise ensuite sur le sous-problème restant à résoudre.
    La méthode gloutonne consiste à choisir des solutions locales optimales d’un problème dans le but d’obtenir une solution optimale globale au problème.
    • Le principal avantage des algorithmes gloutons est leur facilité de mise en œuvre.
    • Le principal défaut est qu’il ne renvoie pas toujours la solution optimale nous le verrons.
    • Dans certaines situations dites canoniques, il arrive qu’ils renvoient non pas un optimum mais l’optimum d’un problème

2. Le problème du rendu de monnaie


Un achat dit en espèces se traduit par un échange de pièces et de billets. Dans la suite, les pièces désignent indifféremment les véritables pièces que les billets.

Supposons qu’un achat induise un rendu de 49 euros on considérant pour simplifier que les ‘pièces’ prennent les valeurs 1, 2, 5, 10, 20, 50, 100 euros. Quelles pièces peuvent être rendues ?

  • 49=4×10+1×5+2×2

soit 7 pièces au total
(Quatre pièces de 10 euros, 1 pièce de 5 euros et deux pièces de 2 euros.) ou 49=9×5+2×2  soit 11 pièces au total ou 49=9×5+4×1  soit 13 pièces au total ou 49=49×1

  •   soit 49 pièces au total

Le problème du rendu de monnaie consiste à déterminer la solution avec le nombre minimal de pièces.

Rendre 49 euros avec un minimum de pièces est un problème d’optimisation. En pratique, tout individu met en œuvre un algorithme glouton.

  1. Il choisit d’abord la plus grandeur valeur de monnaie, parmi 1, 2, 5, 10, contenue dans 49 euros.
    En l’occurrence, quatre fois une pièce de 10 euros.
  2. La somme de 9 euros restant à rendre, il choisit une pièce de 5 euros,
  3. Puis deux pièces de 2 euros.

Solution optimale et système canonique du rendu de monnaie

Cette stratégie gagnante pour la somme de 49 euros l’est-elle pour n’importe quelle somme à rendre ?

  • Un système canonique
    • On peut montrer que l’algorithme glouton du rendu de monnaie renvoie une solution optimale pour le système monétaire français
      {1,2,5,10,20,50,100}
    • Pour cette raison, un tel système de monnaie est qualifié de canonique.
  • Des systèmes non canoniques
  • D ’autres systèmes ne sont pas canoniques. L’algorithme glouton ne répond alors pas de manière optimale.
  • Par exemple, avec le système {1,3,6,12,24,30},l’algorithme glouton répond en proposant le rendu : 49=30+12+6+1 soit 4 pièces alors que la solution optimale est 49=2×24+1

  • soit 3 pièces. 

3. Le TD sur les algorithmes gloutons et le rendu de monnaie

  • Prérequis au TD
    • Python : liste, parcours de listes.
        

On cherche à implémenter un algorithme glouton de rendu de monnaie en ne considérant que des valeurs entières des pièces du système.
Par ailleurs, la plus petite pièce du système sera toujours 1, on est ainsi certain de pouvoir rendre la monnaie quelque soit la somme choisie.

Fonctionnement de l’algorithme glouton du rendu de monnaie

  1. On choisit un système de monnaies, par exemple  systeme=[1,2,5,10,20,50,100]
  2. On choisit une valeur, par exemple valeur=49 euros .
  3. On choisit la plus grande ‘pièce’ du système inférieure à la valeur et on l’ajoute à la liste des pièces à rendre.
  4. On calcule le reste à rendre et on recommence jusqu’à obtenir un reste à rendre nul.

 

Exercice 1

  1. Écrire une une fonction python qui reçoit deux arguments – la somme à rendre et le système de monnaie – et qui renvoie la liste des pièces choisies par l’algorithme glouton.
  2. Tester votre fonction avec les valeurs et les systèmes proposés dans les exemples du cours ci-dessus.
  3. Inventez un système non canonique différent de celui de l’exemple (mais toujours de valeur minimale 1) et trouver un exemple qui le prouve.
    Votre fonction devra donc donner une solution mais qui n’est pas optimale.
  4. Complément : créer un programme (ou une autre fonction) qui va afficher toutes les informations suivantes :
    49=4×10+1×5+2×2
    Soit 7 pièces au total
    C’est à dire : Quatre pièces de 10 euros, 1 pièce de 5 euros et deux pièces de 2 euros.

Aide pour l’exercice 1:

  • Etape 1
    • Définissons le système de pièces à l’aide d’un tableau nommé  systeme constitué des valeurs des pièces classées par valeurs croissantes (de plus petite pièce 1).
    • On définit aussi une valeur à tester, les 49 euros de l’exemple ci-dessus, dans la variable valeur.
    • Ainsi que l’indice i de la plus grande des pièces de systeme.
# valeurs des pièces du système choisi
systeme = [1,2,5,10,20,50,100]
# valeur à tester (par exemple 49 euros)
valeur=49
# indice de la première pièce à comparer à la somme à rendre
i = len(systeme) - 1
  • Etape 2 : Fonctionnement de l’algorithme
    • On cherche à déterminer les pièces à rendre pour la variable valeur.
    • Initialisations
      • La somme à rendre à rendre est initialement stockée dans la variable somme_a_rendre. On l’initialise donc à  valeur.
      • liste_pieces, la liste des pièces à rendre est initialisée à une liste vide
      • i = len(systeme) - 1 : indice de la première pièce à comparer à la somme à rendre
    • On boucle tant que somme_a_rendre > 0
      • La variable piece prend la valeur de la pièce de systeme d’indice i
      • Si somme_a_rendre < piece
        • Alors i pend la valeur i-1
      • Sinon
        • alors on ajoute la piece à la liste des pièces à rendre liste_pieces
        • on enlève la valeur de piece de somme_a_rendre
    • On renvoie la liste : liste_pieces

Exercice 2

  • On cherche maintenant à généraliser notre algorithme avec le système de pièces et de billets utilisées en Europe et des valeurs décimales.
  • On va donc considérer un système composé de pièces et de billets :
    • Les pièces (en euros) : 0,01 – 0,02  – 0,05 – 0,10 – 0,20 – 0,50 – 1 – 2 ;
    • Les billets (en euros)  : 5 – 10 – 20 – 50 – 100 – 200 – 500.
  1. Modifier donc votre programme afin qu’il affiche le nombre les pièces à rendre et les billets.
  2. Tester les avec plusieurs exemples.
  3. Et si il n’y avait ni billet de 5, ni billet de 10 euros.
    Montrer avec un exemple bien choisi que la solution donnée par l’algorithme n’est pas optimale

Exercice 3 – Une pièce de 7 euros

  • On peut se demander pourquoi il n’y a pas de pièce de 7 euros par exemple.
  • En fait c’est parce que si tel était le cas, l’algorithme glouton de rendu de monnaie ne serait plus optimal.
  1. Tester votre algorithme en ajoutant une pièce de 7 euros dans le système.
  2. Trouver un exemple qui renvoie une solution qui n’est pas optimale.