Université Paris-Saclay Master Bioinformatique – M1 BIBS 2015-2016 Algorithmique et complexité Projet noté : Arbres N -aires Jusqu’à maintenant vous avez étudié les arbres binaires. Il est néanmoins possible de représenter des arbres dont les nœuds peuvent avoir plus que deux fils chacun (et plus qu’une seule clé). Il existe pour cela des structures de données spécialisées comme, par exemple, les B -arbres (B-trees), les arbres bicolores (red-black trees) ou encore les tries. Pour ce projet nous allons nous contenter de définir et d’implémenter une structure très simple que l’on appelle arbre N -aire. 1 Définition On appelle arbre N -aire un arbre dont les nœuds peuvent chacun avoir entre 0 et N fils. De plus, on impose l’invariant suivant : à tout moment, tous les nœuds qui sont des frères sont triés dans l’ordre lexicographique. La Figure 1 montre un exemple d’arbre N -aire. La racine de l’arbre est étiquetée par le symbole /, et elle a deux fils, respectivement étiquetés etc et home. Le nœud etc n’a pas de fils. En revanche, le nœud home a trois fils, respectivement étiquetés bill, linus et steve. On dit que home est le nœud parent de bill, linus et steve. Les nœuds étiquetés etc et home sont dits frères, puisqu’ils sont les nœuds fils d’un même nœud parent. Pareil pour bill, linus et steve. En revanche, les nœuds étiquetés Figure 1: Un exemple d’arbre N -aire. Ici, N vaut au windows.txt et Documents ne sont moins 3. pas des frères (car leurs nœuds parents sont différents). 2 Structures de données Créez un fichier narbre.h et placez-y les structures suivantes : • struct Noeud – structure composée de deux champs id et n fils (de type entier) représentant respectivement l’identifiant et le nombre de fils du nœud courant ; de deux champs fils et frere (de type pointeur de Noeud) représentant respectivement des liens vers les fils et les frères du nœud courant ; et d’un champ etiquette (de type pointeur de char) qui retient l’étiquette associée au nœud. (Référez-vous aussi à la Figure 2.) • struct NArbre – structure composée d’un champ racine (de type pointeur de Noeud) pour la racine de l’arbre, et de deux champs nb noeuds et N (de type entier) représentant respectivement le nombre de nœuds présents dans l’arbre et le nombre maximal de fils par nœud. 1 Figure 2: Structure d’un nœud. Remarques : Chaque nœud d’un arbre N-aire doit avoir un identifiant unique. Par contre, il peut y avoir plusieurs nœuds étiquetés par la même chaı̂ne de caractères (le champ etiquette du type Noeud). Le champ de l’arbre retenant le nombre de nœuds ( nb noeuds) permet d’attribuer à chaque nœud nouvellement créé un identifiant unique. Conventions : L’identifiant du nœud racine vaut 0. Si un nœud donné n’a pas de fils (frères), alors son champ fils ( frere) vaut NULL. Ces structures permettront de représenter un arbre N -aire d’une façon exploitable de point de vue informatique. La représentation correspondant à l’arbre N -aire de la Figure 1 est illustrée dans la Figure 3. Figure 3: Représentation équivalente du N -arbre dans la Figure 1. Les nœuds de cet arbre suivent la représentation proposée dans la Figure 2. Le symbole ∅ signifie NULL. 3 API Créez ensuite les fonctions ci-dessous dans un fichier narbre.c avec les prototypes associés dans le fichier narbre.h. Il s’agit d’une API (Application Programming Interface), ce qui veut dire que (i) vous devez implémenter ces fonctions telles qu’elles sont spécifiées, mais (ii) vous avez bien sûr droit à définir toutes les fonctions supplémentaires que vous jugez nécessaires pour assurer le fonctionnement correct de l’API. 3.1 Fonctions demandées 1. NArbre *nouvel arbre(int N, char *etiquette) – Crée (avec allocation mémoire) un nouvel arbre N -aire. N représente le nombre maximal de fils par nœud, et etiquette est l’étiquette de la racine de l’arbre. La fonction renvoie un pointeur vers cet arbre. 2. Noeud *nouveau noeud(char *etiquette) – Crée (avec allocation mémoire) un nouveau nœud étiqueté par la chaı̂ne de caractères pointée par etiquette. Renvoie un pointeur vers ce nœud. Vous autoriserez une longueur de la chaı̂ne de caractères inférieure ou égale à 16. Si la chaı̂ne pointée par etiquette en contient plus, vous garderez uniquement les 16 premiers caractères. Vous aurez besoin d’allouer de la mémoire séparément pour le champ etiquette du nœud. Indication : Vous pouvez utiliser les fonctions strlen et strncpy définies dans string.h. Rappelez-vous qu’une chaı̂ne de caractères est un tableau de char dont le dernier élément est le caractère nul '\0'. 2 3. void free arbre(NArbre *A) – Libère la mémoire allouée à l’arbre (ainsi que pour tous ses nœuds et chaı̂nes de caractères allouées dynamiquement). 4. void affiche arbre(NArbre *A) – Affiche l’arbre reçu en argument. D’abord, un message est affiché, pour dire quel est le nombre total de nœuds de l’arbre, et pour afficher la valeur de N. Ensuite, pour chaque nœud n, il faut afficher l’identifiant du nœud entre parenthèses, suivi d’un espace, suivi de la chaı̂ne de caractères représentée par n->etiquette. L’affichage commence par la racine de l’arbre. Cette ligne est située le plus à gauche possible. Tous les nœuds frères apparaissent au même niveau (colonne à l’écran). Les fils d’un nœud reçoivent chacun une indentation de 5 espaces par rapport au niveau de leur nœud parent. Voici le résultat de affiche arbre sur l’arbre N -aire de la Figure 3 (ici N vaut 3) : L’arbre a 11 noeud(s), N = 3. ( 0) / ( 2) etc ( 1) home ( 7) bill ( 8) windows.txt ( 5) linus ( 6) Documents (10) penguins.c ( 3) steve ( 4) Documents ( 9) apples.h 5. int ajout noeud(NArbre *A, char *etiquette, int id parent) – Ajoute un nouveau nœud comme fils du nœud dont l’identifiant est id parent. Le nouveau nœud est étiqueté par la chaı̂ne de caractères etiquette. L’ajout d’un nouveau nœud à la liste de fils du nœud parent (dont l’identifiant est id parent) doit se faire dans l’ordre lexicographique. Indication : Vous pouvez utiliser la fonction strcmp définie dans string.h. La fonction renvoie l’identifiant du nœud nouvellement créé, ou -1 si l’ajout n’est pas possible. Ceci peut arriver : (i) si l’identifiant du parent n’est pas valide (s’il est inférieur à zéro), (ii) si l’identifiant du parent n’est pas trouvé, (iii) si le parent est déjà rempli, ou (iv) si le parent possède déjà un fils ayant la même étiquette etiquette. Si l’ajout d’un nouveau nœud échoue, un message doit être affiché sur le flux standard d’erreur pour en expliquer la cause. 3 3.2 Exemple d’utilisation de l’API Voici comment utiliser l’API que vous venez d’implémenter pour créer et afficher l’arbre N -aire de la Figure 3 : NArbre *A; int home, bill, linus, steve, dlinus, dsteve; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 A = nouvel_arbre(3, "/"); home = ajout_noeud(A, "home", 0); ajout_noeud(A, "etc", 0); steve = ajout_noeud(A, "steve", home); dsteve = ajout_noeud(A, "Documents", steve); linus = ajout_noeud(A, "linus", home); dlinus = ajout_noeud(A, "Documents", linus); bill = ajout_noeud(A, "bill", home); ajout_noeud(A, "windows.txt", bill); ajout_noeud(A, "apples.h", dsteve); ajout_noeud(A, "penguins.c", dlinus); affiche_arbre(A); free_arbre(A); Votre code sera évalué dans le cadre d’un client de test du même style (mais qui teste en plus les cas particuliers mentionnés) ; il est donc important que vous suivez cette API à la lettre. 4 Évaluation Vous m’enverrez par mail vos fichiers narbre.c et narbre.h à [email protected] avant dimanche le 8 mai 2016 à minuit. Un projet rendu en retard ne sera pas corrigé. Le travail demandé est un travail individuel. Votre code sera évalué selon plusieurs critères, notamment : le respect de la spécification de l’API, la justesse du code et des résultats. Une attention particulière sera apportée aux avertissements de compilation (options -Wall et -ansi pour gcc), ainsi qu’aux fuites de mémoire et aux erreurs détectées par valgrind. (Référez-vous aux TPs n° 2 et 3.) 4