Université de Nice-Sophia Antipolis Deug MIAS-MI 1 Algorithmique & Programmation 2002 2003 TP N 10 Arbres binaires Buts : structuration des arbres binaires en Java. classes internes. objets de parcours. 1 Les arbres binaires en Java L orientation Objet de Java impose de compliquer légèrement la structure des pointeurs qui décrivent les arbres binaires. En effet, avec les déclarations générales données dans le cours : type T_arbre = pointeur sur T_noeud T_noeud = article : T_élément gauche, droite = T_arbre finarticle procédure insérer (donnée-résultat a : T_arbre, x : T_élément) début si a = alors a = nouveau (x,, ) sinonsi x a. alors insérer (a.gauche, x) sinon { x > a. insérer (a.droite, x) fin la transmission en donnée-résultat est essentielle lors de la première 1 insertion dans un arbre vide (qui est représenté par le pointeur ). Mais ce mode de transmission n existe pas en Java et on ne peut pas non plus s en sortir avec une fonction d instance du style : Tree insert (Element x) { if (this == null) return new Tree(x, null, null) ; else { if (x.compareto(content) <= 0) left = left.insert(x) ; else right = right.insert(x) ; return this ; qui, avec la plus grande naïveté, suppose qu une expression comme null.insert(x) possède le moindre sens! 1. Dans les insertions suivantes, a n est plus modifié. 1
Comme vous l avez déjà vu avec les listes chaînées et pour les mêmes raisons il faudra donc mettre à distance la racine de l arbre et disposer de deux classes, une classe Node qui s occupe de gérer tout ce qui est récursif (chaque nœud a deux sous-nœuds) et où les références null jouent leur rôle normal de marqueurs d inexistence, et une classe Tree qui gère un nœud unique, la racine. Un arbre vide ne sera donc pas un arbre inexistant (il ne vaudra pas null) mais un arbre sans (sa racine vaudra null). Mais, à la différence de ce que vous avez vu avec les listes, et pour vous apprendre un peu plus de Java, on ne mettra pas ces deux classes dans des fichiers distincts car, somme toute, la classe Node ne présente aucun intérêt en dehors de la classe Tree (elle n est pas réutilisable ailleurs). On le déclarera donc en classe interne, à l intérieur du fichier BinaryTree.java ; on la déclarera naturellement private mais aussi static car cette classe est commune à tous les arbres et pas à une instance particulière. En plus de cette localisation logique, un intérêt de cette présentation sera l inutilité de définir des accesseurs pour les attributs de Node : étant à la maison, ils seront accessibles dans l ensemble de la classe Tree! Le compilateur considèrera cependant cette classe comme une entité à part entière et, quand vous compilerez le fichier BinaryTree.java, vous verrez que, outre BinaryTree.class, il produira un fichier BinaryTree$Node.class qui contiendra le code de la classe Node ainsi que ses liens d import/export. Pour résumer, ces déclarations seront simplement équivalentes à la déclaration dans le même programme des types : type T_noeud = pointeur sur T_descripteurDeNoeud T_descripteurDeNoeud = article : T_élément gauche, droite = T_noeud finarticle T_arbre = pointeur sur T_descripteurDeArbre T_descripteurDeArbre = article racine : T_noeud finarticle Et l insertion se fera au moyen de deux procédures, l une récursive au niveau des nœuds : procédure insérersousnoeud (données a : T_noeud, x : T_élément) { Antécédent : a début si x a. alors si a.gauche = alors a.gauche nouveau(x,, ) sinon insérersousnoeud(a.gauche, x) sinon { x > a. si a.droite = alors a.droite nouveau(x,, ) sinon insérersousnoeud(a.droite, x) fin l autre, non récursive, au niveau des arbres, démarrant simplement le processus : 2
procédure insérer (données t : T_arbre, x : T_élément) { Antécédent : t initialisé (t ) début si t.racine = alors { arbre vide t.racine nouveau(x,, ) sinon { arbre non vide insérersousnoeud(t.racine, x) fin où l on remarque que toutes les transmissions se font maintenant sur le mode donnée. Un schéma mettra en évidence cette technique «Objet» de mise à distance de la racine. On montre deux états du chaînage à partir d une variable t de type T_arbre : arbre non vide t arbre vide t racine racine Isabelle gauche droite Frédéric gauche droite Patricia gauche droite Dimitri gauche droite Irène gauche droite Vlad gauche droite 2 Statistiques sur les arbres Le squelette qui vous est fourni dans le fichier BinaryTree.java contient des moyens d évaluation du nombre de nœuds d un arbre. Vous pouvez constater que ces moyens sont constitués de deux méthodes d instance nbnodes : l une dans la classe Node, récursive et pri- 3
vée, l autre dans la classe Tree, globale et exportée, doit traiter à part le cas root == null (car null.nbnodes() n aurait aucun sens) mais reste une méthode d instance car, dès qu il existe en tant qu objet, un arbre, même vide, ne vaut jamais null. a. Sur le même modèle, écrivez des méthodes height qui calculent la hauteur et nbleaves qui comptent le nombre de feuilles. 3 Arbres binaires de consultation Ces arbres, qu on appelle parfois en abrégé «BST» (Binary Search Tree) satisfont une condition récursive forte sur la répartition des clefs, condition simple 2 mais qu il n est pas si facile d écrire avec justesse : b. (difficile, donc facultatif) Faites renvoyer un résultat exact à la méthode isbst de la classe Node. Cependant, ce test n est pas ici un réel problème puisque, ne pouvant fabriquer des arbres qu avec le constructeur d arbre vide et des utilisations successives de la méthode insert, on est assuré que la condition est satisfaite 3! Les méthodes d insertion, simples traductions des algorithmes donnés dans la première section, ainsi que les méthodes de recherche figurent dans le squelette fourni mais ces dernières présentent une bizarrerie qu il faudrait bien expliquer. c. (subtil) Quelle est cette bizarrerie? 4 Les parcours d arbres Java propose un mécanisme général pour parcourir toute collection d objets comme tableaux, listes ou arbres. Il consiste à associer à chaque collection un objet qu on appelle une «énumération» ou un «itérateur». Cet objet sera une instance d une classe qui proposera trois outils de base : un constructeur «initialise l énumération» qui prend la collection en argument ; une méthode d instance «il y en a encore» qui renvoie vrai si le parcours n est pas terminé ; une méthode d instance «donne le prochain» qui renvoie le composant suivant et, par effet de bord, le retire de l énumération. Il devra bien sûr exister une telle classe pour chaque type de collection (listes et arbres ne se parcourent pas de la même façon) mais, comme elles proposeront toutes les outils ci-dessus, on trouve pratique de les considérer comme héritières d un même modèle de classe afin de fixer une fois pour toutes les noms de leurs méthodes d instance. Cette notion de modèle de classe existe en Java sous le nom de interface et l API de Java en propose deux pour le parcours des collections : Enumeration et Iterator. Vous pourrez les chercher dans l API mais voici la déclaration de la plus simple d entre elles : public interface Enumeration { public boolean hasmoreelements () ; public Object nextelement () ; 2. Toutes les clefs du sous-arbre gauche sont inférieures à la clef de la racine, elle-même inférieure à toutes les clefs du sous-arbre droit. 3. Ce ne sera quand même pas inutile de disposer d une méthode isbst quand, plus tard, on se livrera à de l arboriculture plus tordue et qu on voudra vérifier qu on ne fait pas n importe quoi! 4
Et, pour qu une classe MyEnumeration hérite de ce modèle, il suffira qu elle soit déclarée de la façon suivante : public class MyEnumeration implements Enumeration { // Attribut // (souvent une collection partielle intermédiaire) public MyEnumeration (...) { // corps du constructeur : initialise l attribut // à partir de la collection passée en paramètre public boolean hasmoreelements () { // corps : l attribut est-il vide? public Object nextelement () { // corps : retire un élément de l attribut // en ajoute éventuellement d autres // et renvoie l élément retiré Enfin, pour faire le parcours d une collection en traitant chaque élément, il suffira, utilisant complètement la notion d héritage, d écrire : Enumeration foo = new MyEnumeration(myCollection) ; while (foo.hasmoreelements()) deal(foo.nextelement()) 4.1 Parcours en profondeur Les trois parcours d arbre en profondeur, préfixe, infixe et postfixe, ne donneront pas lieu à exercices puisqu on fait simplement de la récupération. En effet, la classe Vector que vous connaissez déjà propose une méthode d instance elements() qui renvoie un objet de type Enumeration. Il nous suffira donc de recopier un BinaryTree dans un Vector, ce que font récursivement les méthodes fillprefixed, fillinfixed et fillpostfixed de la classe Node et d appeler simplement la méthode elements() sur ce vecteur pour obtenir les trois énumérations dans la classe BinaryTree. Regardez comment on fait et comment ces énumérations sont utilisées dans la classe TestBinaryTree. 4.2 Parcours en largeur Malheureusement, la technique ne marche plus pour le parcours en largeur car on ne voit pas bien comment recopier un arbre dans un vecteur en suivant cet ordre bizarre qui ne cesse de sauter de branche en branche! La solution est d utiliser une file d attente comme collection intermédiaire, file d attente qui varie au cours de l énumération. Les diverses façons d implémenter la notion de file d attente n ont pas encore été traitées en cours mais vous avez déjà fait la queue à la poste ou attendu chez le dentiste et vous connaissez bien le principe : on quitte la file quand on est devenu le premier et, quand on arrive, on se met en dernier! Une classe Queue qui implémente 4 cette notion vous est 4. On a choisi une implémentation (avec des pointeurs) un peu sophistiquée pour que vous ne soyiez pas tentés de vous y intéresser pour l instant! 5
fournie dans les Documents de cours avec ses trois méthodes d instance isempty, enqueue (arriver) et dequeue (sortir) et vous n aurez aucune difficulté à l utiliser. L algorithme du parcours en largeur est alors le suivant : initialisation: on crée une queue vide on y met la racine de l arbre à chaque étape : on fait sortir le premier (c est lui qui sera renvoyé) ; on met à la queue son sous-nœud gauche et son sous-nœud droit (ils attendront que la fin du niveau supérieur et le début de leur niveau soient sortis pour sortir à leur tour). Pour changer un peu (et parce que ses méthodes ont des noms plus simples) on a choisi d utiliser le modèle de classe Iterator pour définir une classe BreadthIterator. Comme la classe Node, elle sera interne à la classe BinaryTree, privée (elle ne sert que là) et statique (il n y en a pas une pour chaque instance d arbre mais une seule, commune à tous les arbres). d. Implémentez la classe BreadthIterator et la méthode breadthtraversal de telle sorte que les lignes 50 à 54 de la classe TestBinaryTree fassent leur travail. Vous vous servirez du schéma général d implémentation et de l algorithme donnés plus haut. Remarque : En allant regarder dans l API pour savoir ce qu il faut mettre dans cette classe vous verrez qu un objet Iterator permet aussi de supprimer des éléments de la collection pendant le parcours. Comme il est hors de question de le faire ici, vous utiliserez pour cette méthode l implémentation suivante qui coupera court à toute mauvaise tentation : public void remove () { throw new UnsupportedOperationException(" removing not allowed ") ; 5 Squelette Le squelette, trop long pour être reproduit ici est composé de quatre classes : Queue : outil du parcours en largeur, elle n est pas à étudier : vous pourrez y revenir plus tard quand vous aurez eu le cours sur les files d attente ; Key : elle encapsule la notion générale de clef et en donne une implémentation avec des chaînes de caractères : utilisée par les classes suivantes, elle ne donne lieu à aucun exercice ; BinaryTree : c est votre classe de travail, beaucoup est fourni (mais à étudier et comprendre) et il ne reste que quelques trous à combler... TestBinaryTree : elle vous permettra de tester votre travail ; il vous suffira de retirer les marques de commentaires au fur et à mesure que vous aurez écrit les méthodes correspondantes. Toutes ces classes sont compilables et TestBinaryTree est exécutable en l état. 6