Les arbres binaires – Implémentations Nathalie Junior Bouquet mars 2014 Défnition : Un arbre binaire est soit vide, soit de la forme B = <o, G, D>, où G et D sont des arbres binaires disjoints et ’o’ est un nœud appelé racine. e u c u q l q o e n Figure 1 – Arbre binaire 1 Le type algébrique abstrait sorte ArbreBinaire utilise Nœud, Élément opérations arbre-vide : → ArbreBinaire <–, –, –> : Nœud × ArbreBinaire × ArbreBinaire → ArbreBinaire racine : ArbreBinaire → Nœud g : ArbreBinaire → ArbreBinaire d : ArbreBinaire → ArbreBinaire contenu : Nœud → Élément préconditions racine(B) est-défini-ssi B ̸= arbre-vide g(B) est-défini-ssi B ̸= arbre-vide d (B) est-défini-ssi B ̸= arbre-vide axiomes racine(<o, G, D>) = o g(<o, G, D>) = G d (<o, G, D>) = D avec G, D : ArbreBinaire o : Nœud L’opération contenu permet d’associer à chaque Nœud de l’arbre une information de type Élément. Un arbre dont les nœuds contiennent des éléments est dit arbre étiqueté. 1 Algorithmique Les arbres binaires – Implémentations mars 2014 Prépas Epita On notera, abusivement, <r, G, D> l’arbre dont la racine contient l’élément r. r G D Figure 2 – Arbre binaire : une structure récursive 2 Implémentations 2.1 Implémentation dynamique La représentation la plus naturelle reproduit la structure récursive (voir figure 2). On utilise les pointeurs pour chaîner entre eux les nœuds. A chaque nœud on associe deux pointeurs, vers les deux sous-arbres gauche et droit. L’arbre non vide est représenté par un pointeur sur le nœud racine. Il a la valeur nul s’il est vide. Nous utiliserons la plupart du temps des arbres étiquetés. Un champ supplémentaire cle représente l’information contenue dans le nœud. 2.1.1 Le type types /* t_element */ t_arbreBinaire = ↑ t_noeudBinaire t_noeudBinaire = enregistrement t_element cle t_arbreBinaire fg, fd fin enregistrement t_noeudBinaire B e c u NUL u q l NUL NUL q e o n NUL NUL NUL NUL NUL NUL NUL NUL Figure 3 – Représentation dynamique de l’arbre de la figure 1 2 Algorithmique Les arbres binaires – Implémentations mars 2014 2.1.2 2.2 Prépas Epita Implémentation des opérations Type abstrait : ArbreBinaire Implémentation : type t_arbreBinaire B : ArbreBinaire B : t_arbreBinaire arbre-vide nul contenu(racine(B)) B↑.cle g(B) B↑.fg d(B) B↑.fd Implémentation statique : la numérotation hiérarchique On peut utiliser un simple vecteur pour représenter un arbre binaire. Il suffit de stocker chaque valeur à la position correspondant au numéro en ordre hiérarchique du nœud la contenant. 1 f 2 3 i p 4 5 n r 8 9 u 6 7 a t 10 _ a Figure 4 – Arbre binaire parfait + numérotation hiérarchique constantes MaxN = ... types /* t_element*/ t_vect_elts = MaxN t_element variables t_vect_elts B L’arbre de la figure 4 sera représenté par le tableau suivant : 1 f 2 p 3 i 4 n 5 r 6 a 7 t 8 u 9 _ 10 a L’utilisation de cette représentation est intéressante sur un arbre parfait (ou complet, comme celui de la figure 8), car on peut limiter la taille du vecteur à celle de l’arbre. Par contre, sur un arbre quelconque, la place occupée est moins optimale. Représentation de l’arbre de la figure 1 : 1 e 2 c 3 u 4 u 5 l 6 q 7 8 3 9 q 10 e 11 12 o 13 n ... ... Algorithmique Les arbres binaires – Implémentations mars 2014 Prépas Epita La place utilisée sera d’autant moins optimisée que la hauteur de l’arbre sera élevée. Un arbre dégénéré (ou filiforme) à n nœuds nécessitera un vecteur de taille 2n − 1 ! f i l i f e Figure 6 – Peigne droit Figure 5 – Arbre filiforme Représentation de l’arbre filiforme de la figure 5 : 1 f 2 3 i ... ... 7 l ... ... 14 i ... ... 29 f ... ... 15 g ... ... 30 i 31 n 58 e ... ... Représentation du peigne droit de la figure 6 : 1 p 2 d 3 e ... ... 6 r 7 i ... ... 14 o ... ... 62 t 63 e ... ... Utilisation ◦ La racine est en position 1 dans le vecteur. ◦ Si i est la position du nœud actuel alors : ◃ son fils gauche se trouve à la position 2i ◃ son fils droit se trouve à la position 2i + 1 ◃ son père (sauf pour la racine de l’arbre), se trouve à la position i div 2. En pratique, il faut avoir une valeur particulière (∅) pour remplacer la racine de l’arbre vide. Sauf si l’arbre est parfait : la taille de l’arbre suffit ! 4 Algorithmique Les arbres binaires – Implémentations mars 2014 3 3.1 Prépas Epita Parcours d’un arbre binaire Parcours en profondeur Le parcours en profondeur main gauche consiste à descendre dans l’arbre à gauche le plus loin possible. Lorsqu’on ne peut plus descendre, on remonte d’un niveau : si on vient de la gauche, on descend à droite, sinon on remonte encore. . . Et on recommence. . . La manière la plus simple d’envisager ce parcours est récursive. Il revient tout simplement à parcourir le sous-arbre gauche, puis à parcourir le sous-arbre droit ! r (1) (3) (2) G (1) : préfixe D (2) : infixe (3) : suffixe Figure 7 – Parcours en profondeur d’un arbre binaire Lors du parcours en profondeur, chaque nœud est rencontré trois fois : (1) avant de descendre sur le sous arbre-gauche : ordre préfixe, (2) en remontant de la gauche, avant de descendre à droite : ordre infixe (ou symétrique), (3) en remontant de la droite : ordre suffixe (ou postfixe). Exemples : les trois ordres de traitement des nœuds d’un arbre induits lors d’un parcours en profondeur. ◦ L’ordre préfixe sur l’arbre de la figure 8 donnera : lui_est_complet ◦ L’ordre infixe sur l’arbre de la figure 4 donnera : un_parf ait ◦ L’ordre suffixe sur l’arbre de la figure 1 donnera : quelconque l u c s i _ e t o _ m l p e Figure 8 – Arbre binaire complet 5 t Algorithmique Les arbres binaires – Implémentations mars 2014 Prépas Epita L’algorithme : Avec l’implémentation statique : Avec l’implémentation dynamique : ↓ algorithme procedure parcours_prof parametres locaux t_arbreBinaire B debut si B[i] = ∅ alors /* ∅ = "vide" */ sinon prof_rec (B, 2*i) prof_rec (B, 2*i+1) fin si fin algorithme procedure prof_rec debut si B = nul alors /* terminaison */ sinon /* traitement préfixe */ parcours_prof (B↑.fg) /* traitement infixe */ parcours_prof (B↑.fd) /* traitement suffixe */ fin si fin algorithme procedure parcours_prof 3.2 algorithme procedure prof_rec parametres locaux t_vect_elts B entier i → algorithme procedure parcours_prof parametres locaux t_vect_elts B debut prof_rec (B, 1) fin algorithme parcours_prof Parcours en largeur Le parcours en largeur ou par niveaux consiste à visiter les nœuds niveaux par niveaux (en général de gauche à droite) : c’est l’ordre hiérarchique. L’algorithme classique utilise une file : – on enfile la racine de l’arbre – tant que la file n’est pas vide : – on récupère et on défile le premier élément de la file – on enfile chacun de ses fils (s’ils existent), d’abord le gauche puis le droit. L’algorithme : Avec l’implémentation dynamique algorithme procedure parcours_largeur parametres locaux t_arbreBinaire B variables t_file f debut si B <> nul alors f ← enfiler (B, file_vide()) faire B ← defiler (f) /* traitement B↑.cle */ si B↑.fg <> nul alors f ← enfiler (B↑.fg, f) fin si si B↑.fd <> nul alors f ← enfiler (B↑.fd, f) fin si tant que non est_vide (f) fin si fin algorithme procedure parcours_largeur Pour la représentation statique, on peut envisager une implémentation sans file, en effectuant un parcours séquentiel du vecteur (en passant les éventuelles valeurs ∅). Mais cela n’est réellement intéressant que lorsque l’arbre est parfait ! Sinon, il suffit d’utiliser le parcours classique avec une file d’entiers, qui contiendra les numéros en ordre hiérarchique des nœuds. 6