Lycée Faidherbe, Lille MP1 Cours d’informatique 2013–2014 Arbres de données I Homogènes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introduction 2 Définition 2 comme graphes 4 II 2 Arbres 5 Implémentation 5 Binaires Définition 6 brements 7 Parcours 10 IV Implémentation 3 Hétérogènes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Introduction 5 III Exemples 3 ................................ 6 Implémentation 6 Fonctions élémentaires 7 DénomEncombrement 8 Arbres parfaits 8 Arbre équilibré 9 Exercices ............................... 12 Exemple 12 Longueur de cheminement 12 Arbres de Fibonacci 12 Symétrisation 12 Hiérarchie 13 Reconstruction d’un arbre 13 Représentation binaire 13 Parcours en largeur 13 Profondeur minimale 14 Tas 14 Arbres page 1 I Homogènes I.1 Introduction Une structure est arborescente quand, à chaque élément, on associe d’autres éléments de manière hierarchique : • fils • descendants • sous–ensembles • ... Le modèle, qui a donné le nom, est celui d’un arbre où les branches se divisent en d’autres branches. On suppose que le nombre d’éléments est fini et qu’il n’existe pas de cycle . Il existe alors au moins un élément qui n’est le descendant d’aucun autre. On appellera arbre une structure dans laquelle il n’y a qu’un seul tel élément : la racine. Comme il n’existe pas de cycle chaque élément est associé à la racine par un nombre fini de dépendances. Contrairement à la vision de la nature les arbres sont représentés à l’envers. I.2 Définition Un arbre non vide est un ensemble fini d’objets : les nœuds liés par une relation n est le fils de m ou m est le père de n telle que page 2 Arbres • il existe un unique élément sans père : la racine • tout élément, sauf la racine, a un père unique • tout élément est descendant de la racine. Un nœud sans fils est une feuille de l’arbre ou nœud externe ; les autres sont les nœuds internes. L’ensemble des nœuds qui sont les descendants d’un nœud n0 forme un arbre dont n0 est la racine: c’est le sous–arbre de racine n0 . Un arbre peut donc être défini récursivement comme • soit l’arbre vide • soit un ensemble d’arbres fils d’une racine. L’ensemble des fils n’est pas, dans ce cas général, ordonné. I.3 Exemples • • Arbre généalogique Plan du cours d’informatique Informatique en MP binaires langages complexité arbres ABR rationnels automates AFD AFND I.4 Implémentation Pour définir le type “arbre” on va introduire une définition récursive : un arbre est un nœud associé à une liste d’arbres. type ’f arbre = Noeud of ’f * (’f Arbres arbre list);; page 3 let a = Noeud (4,[]);; let b = Noeud (5,[]);; let c = Noeud (2,[]);; let d = Noeud (3, [a;b;c]);; val d : int arbre = Noeud (3, [Noeud (4, []); Noeud (5, []); Noeud (2, [])]) I.5 Arbres comme graphes On peut aussi considérer les arbres comme des ensembles de sommets (les noeuds) liés par des arêtes avec une hierarchie : ce sont des notions de graphe. • Il y a un sommet particulier : la racine. • Il y a toujours un chemin entre deux sommets. • Ce chemin est unique si on exclut les arêtes parcourures dans les deux sens. Un arbre est un graphe (non orienté) connexe et acyclique avec un sommet privilégié (s’il est non vide). • • Connexe signifie qu’il y a toujours un chemin entre deux sommets. Acyclique signifie qu’il n’y a pas de circuit c’est–à–dire de chemin non trivial entre un sommet et lui–même. • • Si on se donne un tel graphe la racine est le sommet privilégié. Pour tout sommet il a un chemin unique entre la racine et ce sommet (sinon il y aurait un cycle). Le père d’un sommet distinct de la racine est l’avant dernier sommet dans ce chemin. Les fils d’un sommet s sont les sommets extrémités d’une arête d’origine s à l’exclusion du père : on n’obtient jamais ni la racine ni un nœud qui aurait un autre père en raison de l’acyclicité. • • page 4 Arbres II Hétérogènes II.1 Introduction On peut aussi imaginer un arbre comme la description d’une formule. Dans ce cas il y a deux types d’objets • des objets initiaux ou atomiques : dans l’arbre ils seront représentés aux extrémités, ce sont les feuilles • des assemblages effectués par des opérations sur des objets déjà construits, ce sont les nœuds de l’arbre. Exemple : représentation parun arbre de 5 × (2 − e3 ) + (2 × sin(π) + × × − 5 2 2 sin exp π 3 II.2 Implémentation On voit en fait qu’il y a plusieurs types de nœuds qui diffèrent selon leur arité, le nombre de variables qu’ils modifient. En général il n’y a que des opérateurs binaires : ∪, ou, ×, . . . ou unaires : complémentaire, non, ln, ... type (’f,’n) arbre2 = Feuille of ’f |Noeud1 of (’n * (’f,’n) arbre2) |Noeud2 of ((’f,’n) arbre2 * ’n *(’f,’n) arbre2);; Arbres page 5 III Binaires III.1 Définition Un arbre binaire est un arbre dans lequel chaque nœud admet au plus 2 fils avec, de plus, un ordre dans ceux–ci. Il peut être défini de manière récursive : un arbre binaire est • soit l’arbre vide • soit un arbre formé d’une racine et de deux fils, nommés fils droit et fils gauche, qui sont eux–même des arbres binaires. Remarque Un arbre avec deux nœuds est défini de manière unique en tant qu’arbre général p f mais admet deux structures d’arbre binaire : p f p f III.2 Implémentation type ’n arbre_bin = Vide |Noeud of (’n arbre_bin*’n *’n arbre_bin);; On construit un arbre à partir de ses composantes en invoquant simplement le constructeur. Par exemple l’arbre d’entiers à un seul nœud, de valeur 5, est défini par let a = Noeud(Vide,5,Vide);; Un tel nœud est un nœud terminal. page 6 Arbres III.3 Fonctions élémentaires Le traitement d’un arbre se fera, comme sa définition, récursivement ; on traite le cas de l’arbre vide puis on séparera un arbre, en général en filtrant : let filsG = function |Vide -> raise(Failure "Arbre vide") |Noeud (g,_,_) -> g;; let filsD = function |Vide -> raise(Failure "Arbre vide") |Noeud (_,_,d) -> d;; let racine = function |Vide -> raise(Failure "Arbre vide") |Noeud (_,n,_) -> n;; III.4 Dénombrements La taille d’un arbre est le nombre de nœuds : let rec taille = function |Vide -> 0 |Noeud (g,_,d) -> 1 + taille g + taille d;; La profondeur d’un nœud est le nombre total de relations de parenté entre la racine et lui. • La racine est à une profondeur de 0, ses fils ont une profondeur de 1. • La profondeur des fils d’un nœud est égale à la profondeur du nœud augmentée de 1. La hauteur d’un arbre est la profondeur de nœud maximale. Elle est atteinte pour une nœud terminal. Par convention la hauteur de l’arbre vide est -1. let rec hauteur = function |Vide -> -1 |Noeud (g,_,d) -> 1 + max (hauteur g) Arbres (hauteur d);; page 7 III.5 Encombrement Cas minimal Quand chaque nœud non terminal n’a qu’un seul fils l’arbre est en fait une liste. Si la taille de l’arbre est n il y a un nœud unique à chaque profondeur de 0 à n − 1 donc la hauteur est n − 1. Pour tout autre arbre de hauteur n − 1 il y a au moins un nœud de plus donc la taille est minorée pas la hauteur augmentée de 1. Cas maximal Chaque nœud d’un arbre binaire admet au plus deux fils donc, si ni est le nombre de nœuds de profondeur i, on a ni+1 6 2ni . De plus n0 = 1 donc ni 6 2i . Si la hauteur est h la taille de l’arbre est donc majorée par 1 + 2 + · · · + 2h = 2h+1 − 1. La taille n et la hauteur h d’un arbre binaire vérifient h + 1 6 n 6 2h+1 − 1 On a alors log2 (n + 1) 6 h + 1 d’où h + 1 > dlog2 (n + 1)e. Si 2 p 6 n 6 2 p+1 − 1 on a p < log2 (n + 1) 6 p + 1 donc dlog2 (n + 1)e = p + 1 puis h > p. Or l’entier p tel que 2 p 6 n 6 2 p+1 − 1 est p = blog2 (n)c d’où La taille n et la hauteur d’un arbre binaire vérifient blog2 (n)c 6 h 6 n − 1 III.6 Arbres parfaits Un arbre binaire est parfait s’il est vide ou si, pour tout nœud, les hauteurs des sous–arbres droit et gauche sont égales. page 8 Arbres • • Les arbres parfaits sont les arbres tels que n = 2h+1 − 1 où h est la hauteur et n la taille. Les arbres parfaits sont les arbres pour lesquels tous les nœuds terminaux sont à la même profondeur. La condition précédente sélectionne des arbres très compacts : ils contiennent le maximum de nœuds pour une hauteur donnée. On peut généraliser en considérant les arbres qui ont une hauteur minimale pour une taille donnée ; c’est le cas, entre autres, des arbres dont les niveaux sont remplis au maximum sauf le dernier. Mais cette définition n’est pas très souple. III.7 Arbre équilibré Un arbre binaire est équilibré s’il est vide ou si, pour tout nœud, les hauteurs des sous–arbres droit et gauche diffèrent de 1 au plus. Pour construire un arbre équilibré de hauteur h il suffit d’assembler des arbres équilibrés de hauteur h − 1 tous les deux ou l’un de hauteur h − 1 et l’autre de hauteur h − 2. En particulier s’il existe un arbre équilibré de hauteur h − 1 et de taille n et un arbre équilibré de hauteur h − 2 et de taille m alors il existe arbre équilibré de hauteur h et de taille n + m + 1. La hauteur h d’un arbre équilibré de taille n vérifie h = O ln(n) Si on note fk le nombre minimal de nœuds dans un arbre équilibré de hauteur k on a f0 = 1, f1 = 2 et, pour k > 2, fk = fk−1 + fk−2 + 1. fk + 1 est alors égal au nombre de Fibonacci Fk+2 où (Fn ) définie par F0 = F1 = 1 et Fn+2 = Fn+1 + Fn . Ainsi n > Fh+2 − 1 donc ln(n) > ln(Fh+2 − 1) ∼ Ah. Arbres page 9 III.8 Parcours Parcourir un arbre (binaire) c’est donner une énumération de ses éléments. Cela revient à transformer la structure bidimensionnelle de l’arbre en une liste linéaire. Il y a plusieurs types de parcours selon que l’on privilégie la hierarchie de la hauteur (parcours en largeur) ou celle provenant de l’ordre des fils, dans ce dernier cas on différenciera encore selon la position de la racine. Pour parcourir (ou traiter) un arbre binaire en ordre on doit parcourir le fils gauche avant le fils droit à chaque hauteur. Il reste à traiter la racine. Il y a trois possibilités. • Ordre préfixe : on traite la racine puis les fils. C’est l’écriture des composées de fonctions de deux variables. • Ordre infixe : on traite le fils gauche puis la racine puis le fils droit. C’est l’écriture des expressions arithmétiques. • Ordre postfixe (ou suffixe) : on traite les fils puis la racine. let infixe a = let rec infx = function |Vide -> print_string "-" |Noeud (Vide,n,Vide) -> print_int n |Noeud (g,n,d) -> print_string "("; infx g; print_string ","; print_int n; print_string ","; infx d; print_string ")" in infx a; print_newline;; let prefixe a = let rec prefx = function |Vide -> print_string "-" |Noeud (Vide,n,Vide) -> print_int n |Noeud (g,n,d) -> print_int n; print_string "("; prefx g; print_string ","; prefx d; print_string ")" in prefx a; print_newline;; page 10 Arbres let suffixe a = let rec sufx = function |Vide -> print_string "-" |Noeud (Vide,n,Vide) -> print_int n |Noeud (g,n,d) -> print_string "("; sufx g; print_string ","; sufx d; print_string ")"; print_int n in sufx a; print_newline;; Exemple 11 1 15 4 8 5 7 3 13 12 # infixe aa;; ((4, 15, (5, 8, 3)), 11, (7, 1, (12, 13, −))) # prefixe aa;; 11(15(4, 8(5, 3)), 1(7, 13(12, −))) # suffixe aa;; ((4, (5, 3)8)15, (7, (12, −)13)1)11 Arbres page 11 IV Exercices IV.1 Exemple L’arbre A est 12 8 11 5 6 3 2 7 4 IV.2 Longueur de cheminement La longueur de cheminement d’un arbre est la somme des hauteurs de tous les nœuds. Déterminer une fonction récursive qui calcule la longueur de cheminement d’un arbre. Prouver que les arbres de taille n qui minimisent la longueur de cheminement sont ceux qui ont 2k nœuds de profondeur k pour tout k < blog2 (n + 1)c. En déduire que, si LC est la longueur de cheminement d’un arbre de taille n, on a n X n(n − 1) blog2 (k)c 6 LC 6 . 2 k=1 IV.3 Arbres de Fibonacci Écrire une fonction qui, pour tout n, renvoie un arbre équilibré de hauteur n et de taille Fn − 1 où Fn est défini par F0 = 1, F1 = 2, Fn+2 = Fn+1 + Fn IV.4 Symétrisation Écrire une fonction qui transforme un arbre en son symétrique. Par exemple A devient page 12 Arbres 12 8 7 11 2 6 4 5 3 IV.5 Hiérarchie Prouver que le nœud a et un descendant du nœud b dans un arbre si et seulement si nœud b est avant nœud a dans le parcours préfixe et nœud a est avant nœud b dans le parcours postfixe. IV.6 Reconstruction d’un arbre Montrer que si on connait le parcours préfixe (ou postfixe) et le parcours infixe d’un arbre alors on peut reconstituer l’arbre. IV.7 Représentation binaire On peut représenter la structure d’un arbre par la liste des entiers associés à chaque nœud par • la racine de l’arbre est associée à 1 • le fils gauche (resp. droit) d’un nœud associé à n, s’il existe, est associé à 2n (resp. à 2n + 1). Prouver que si on écrit les nombres en base 2 le parcours infixe d’un arbre consiste à énumérer les nombre dans l’ordre lexicographique. IV.8 Parcours en largeur Caml définit le module Queue qui impémente la structure de file d’attente (FIFO) d’une suite d’objets. On peut le charger par open Queue;;. Il contient les définitions suivantes • le type ‘a t, type des files d’attentes • la fonction is_empty : ’a t -> bool qui teste si une file est vide • la fonction create : unit -> ’a t qui crée une file vide • la fonction push : ’a -> ’a t qui ajoute un élément à une file • la fonction pop : ’a t -> ’a qui retire et retourne le premier élément d’une file. Arbres page 13 À l’aide de cette structure écrire une procédure qui parcourt un arbre en largeur. A devra donner la suite (12,11,8,5,6,2,7,3,4). IV.9 Profondeur minimale Déterminer la profondeur minimale d’une feuille dans un arbre équilibré de hauteur n. IV.10 Tas Un tas est un arbre d’entiers tel que la valeur de chaque nœud est strictement supérieure à celle de tous ses descendants. Par exemple A est un tel arbre. À toute suite d’entiers distincts, x, on associe le tas a par : • si x est la suite vide (de longueur 0), alors a=Vide. • si x = (x0 , . . . , xn−1 ) alors a est l’arbre dont la racine est le nœud de valeur xk = max{xi ; 0 6 1 < n}, dont le sous–arbre gauche (resp. droit) est l’arbre associé à la suite x0 = (x0 , . . . , xk−1 ) (`resp. x00 = (xk+1 , . . . , xn−1 )`. a) Dessiner l’arbre associé à la suite x = (3, 8, 1, 5, 9, 4, 7). b) Écrire la fonction de conversion vecteur_de_tas qui reçoit un tas a et renvoie la suite x qui lui est associée. Par exemple A donnerait (5, 11, 3, 6, 4, 12, 2, 8, 7). On utilisera un parcours adéquat de l’arbre. c) Écrire la fonction ajoute qui reçoit un entier t et le tas a, associé à la suite (x0 , . . . , xn−1 ) et renvoie l’arbre associé à la suite (x0 , . . . , xn−1 , t) d’éléments distincts. d) Écrire la fonction tas_de_vecteur qui reçoit une suite x et renvoie le tas associé cette suite. (En partant de l’arbre vide, on ajoute successivement les xi .) e) Écrire la fonction fusion qui reçoit les deux arbres a et b associés aux suites x = (x0 , . . . , xn−1 ) et y = (y0 , . . . , ym−1 ) et renvoie l’arbre associé à la suite concaténée z = (x0 , . . . , xn−1 , y0 , . . . , ym−1 ) d’éléments distincts. f) Écrire la fonction supprime qui reçoit un élément t et un arbre a associé à une suite x = (x0 , . . . , xn−1 ) et qui renvoie a si t ne figure pas dans x ou renvoie l’arbre associé à la suite x = (x0, . . . , xk−1 , xk+1 , . . . , xn−1 ) si t = x[k]. page 14 Arbres