Cours 3 : Arbres suite Marie-Pierre Béal Université Paris-Est Marne-la-Vallée
Plan 1 Arbres binaires de recherche Definition Exemple Recherche d un élément Ajout d un élément Retrait d un élément 2 Tas ou File de priorité Definition Exemple Ajout d un élément Retrait du minimum Tri par tas Création d un tas 3 Arbres AVL
Définition Définition Un arbre binaire est un arbre binaire de recherche si pour tout nœud, l étiquette du nœud est supérieure ou égale aux étiquettes des nœuds du sous-arbre gauche et est inférieure ou égale aux étiquettes des nœuds du sous-arbre droit.
Exemple 33 15 47 10 20 Les étiquettes des nœuds sont croisssantes dans l ordre infixe. Exercice : écrire un code qui teste si un arbre binaire est un arbre binaire de recherche en temps linéaire.
Recherche d un élément Rechercher(Arbre A, Element e) 1 if A est vide 2 then return false 3 if Elem(A) = e 4 then return true 5 if e < Elem(A) 6 then 7 if FilsGauche(A) est vide 8 then return false 9 else return Rechercher(FilsGauche(A), e) 10 else 11 if FilsDroit(A) est vide 12 then return false 13 else return Rechercher(FilsDroit(A), e) La recherche d un élément prend un temps au plus de la hauteur de
Rechercher en C typedef struct node { int val; struct node* left; struct node* right; Node, *Tree; int rechercher(tree a, int e) { if (a == NULL) return 0; if (a-> val == e) return 1; if (e < a-> val ) { if (a-> left == NULL) return 0; return rechercher(a->left, e); else { if (a-> right == NULL) return 0; return rechercher(a->right, e);
Ajout d un élément Ajouter(Arbre A, Element e) :Arbre 1 if A est vide 2 then 3 A nouvelle feuille d étiquette e 4 return A 5 if e < Elem(A) 6 then FilsGauche(A) Ajouter(FilsGauche(A), e) 7 else FilsDroit(A) Ajouter(FilsDroit(A), e) 8 return A
Ajouter en C /* renvoie NULL en cas d erreur d allocation */ Tree create_leaf (int val) { return create_node(val, NULL, NULL); void ajouter(tree *a, int n) { if (*a == NULL) { *a = create_leaf(n); return; if (n <= (*a) -> val) { ajouter(&((*a) -> left), n); return; ajouter(&((*a) -> right), n);
Exemple 33 15 47 10 12 20 40
Enlever un élément Enlever(Arbre A, Element e) :Arbre 1 if A est vide 2 then 3 erreur( element non present ) 4 return A 5 if e < Elem(A) 6 then FilsGauche(A) Enlever(FilsGauche(A), e) 7 else if Elem(A) < e 8 then FilsDroit(A) Enlever(FilsDroit(A), e) 9 else 10 A Enlever Racine(A) 11 return A
Enlever en C /* renvoie l adresse du noeud extrait */ /* renvoie NULL en cas d echec */ Tree extract(tree *a, int n) { if (*a == NULL) return NULL; /* ici a est non vide */ if (n < (*a) -> val) { return extract(&((*a)->left), n); if (n > (*a) -> val) { return extract(&((*a)->right), n); /* n == (*a) -> val */ return extract_root(a);
Enlever la racine Enlever Racine(Arbre A) :Arbre 1 if FilsGauche(A) est vide 2 then 3 p A 4 A FilsDroit(A) 5 return p 6 p Enlever Max(FilsDroit(A)) 7 temp Elem(A) 8 Elem(A) Elem(p) 9 Elem(p) temp 10 return p
Enlever la racine en C Tree extract_root(tree *a) { Tree t = NULL; if ((*a) -> left == NULL) { t = *a; *a = (*a) -> right; t -> right = NULL; return t; /* on extrait le max du fils gauche non vide */ t = extract_max(&((*a) -> left)); int tmp = (*a) -> val; (*a) -> val = t -> val; t -> val = tmp; return t;
Enlever le max Enlever Max(Arbre A) :Arbre 1 if FilsDroit(A) est vide 2 then 3 p A 4 A FilsGauche(A) 5 return p 6 return Enlever Max(FilsDroit(A))
Enlever le max en C /* enleve le max sur un arbre non vide */ /* ne renvoiejamais NULL */ Tree extract_max(tree *a) { Tree t = NULL; if ((*a)-> right == NULL) { t = *a; *a = (*a) -> left; t -> left = NULL; return t; return extract_max(&((*a) -> right));
Exemple 33 20 15 47 15 47 10 20 40 50 10 40 50
Temps de calcul Temps de calcul de aouter dans le pire des cas Insertion de 1,2,...,n en O(n/2). Temps moyen d un ajout Insertion successive de i,j : permutation de 1,2,...,n. On considère que toutes les permutations sont équiprobables. P(n) = moyenne du nombre de nœuds par chemin
Temps de calcul i i 1 éléments n i éléments P(n) = 1 n n 1 n + i 1 n i (P(i 1)+1)+ n n (P(n i)+1) i=1 = 1+ 1 n n 2 (i 1)P(i 1)+(n i)p(n i) i=1 = 1+ 2 n 1 n 2 ip(i) i=1 1,39log 2 n
Résolution On va montrer que P(n) = 2(1+ 1 n )H n 3 où H n = 1+ 1 2 +...+ 1 n H n = O(logn) car est le n-ième nombre harmonique. Or n 1 H n 1+ 1 x dx = 1+log e n. On aura donc montré que P(n) = O(logn).
On écrit ou encore P(n) = 1+ 2 n 2 n 2 ip(i)+ 2n 2 n 2 P(n 1) i=1 = 1+ n2 1 n 2 (P(n 1) 1)+ 2n 2 n 2 P(n 1) = n2 1 n 2 P(n 1)+ 2n 1 n 2 n n 1 (P(n)+3) = n+1 n (P(n 1)+3)+ 2 n Comme l expression Q(n) = 2(1+ 1 n )H n = 2 n+1 n H n satisfait la relation de récurrence n n 1 Q(n) = n+1 n Q(n 1)+ 2 n on en déduit que (P(n)+3) Q(n) est une constante. Comme P(1)+3 = Q(1) = 4, on obtient P(n) = Q(n) 3.
File de priorité File de priorité Tas Type abstrait : ensemble d éléments comparables Opérations : Ajouter(E,x), Oter min(e). Implémentations : Listes triées ou non, arbres partiellement ordonnés (tas ou heap, tournois) Un tas (min) est un arbre tel que l étiquette de tout nœud est inférieure ou égale à l étiquette de ses fils.
Implémentation par tas i 0 1 2 3 4 5 6 7 dernier T[i] 3 5 9 6 8 9 10 10 18 9 3 5 9 6 8 9 10 10 18 9
Tas pere(i) = i 1 2 si i > 0 filsgauche(i) = 2i +1 si 2i +1 < dernier filsdroit(i) = 2i +2 si 2i +1 < dernier typedef struct { Element table[max]; int dernier; Tas;
Ajouter un élément Ajouter(Tas t, Element e) :Tas 1 t.dernier t.dernier +1 2 i t.dernier 3 t[i] e 4 while ( i > 0 et t.table[i] < t.table[ i 1 2 ]) 5 do 6 échanger t.table[i] et t.table[ i 1 2 ] 7 i i 1 2 8 return t
Ôter le minimum Oter min(tas t non vide) :Tas 1 t.dernier t.dernier 1, d t.dernier, t[0] t[d +1], i 0 2 fin false 3 while not fin 4 do if 2i +2 d 5 then if t.table[2i +1] < t.table[2i +2] 6 then k 2i +1 7 else k 2i +2 8 if t.table[i] > t.table[k] 9 then échanger t.table[i] et t.table[k] 10 i k 11 else fin true 12 else if 2i +1 = d et t.table[i] > t.table[d] 13 then échanger t.table[i] et t.table[d] 14 else fin true 15 return t
Tri par tas trier(liste lst) : Liste 1 t tas vide 2 for e premier to dernier élément de lst 3 do t Ajouter(t,e) 4 result liste vide 5 while not Vide(lst) 6 do result result +Min(t) 7 t Oter Min(t) 8 return result Temps de calcul O(nlogn) si n est la taille de la liste
Temps de calcul de Entas(i) = O(hauteur(i), de Mise en tas = O(n) Création d un tas Mise en tas(element table[n]) : tas 1 for i n 2 2 to 0 2 do Entas(table[i]) 3 return (table,n 1) Entas(Element table[n], indice i) 1 //les sous-arbres de racine k sont des tas pour i < k n 1 2 if 2i +2 = n ou table[2i +1] table[2i +2] 3 then k 2i +1 4 else k 2i +2 5 if table[i] > table[k] 6 then échanger table[i] et table[k] 7 if k n 2 2 8 then Entas(table, k)
Tas en Java import java.util.priorityqueue; public class Heap { public static void main(string[] args) { Set<Integer> set = new HashSet<Integer>(); set.add(6); set.add(3); set.add(1); set.add(5); set.add(2); set.add(4); PriorityQueue<Integer> heap = new PriorityQueue(set); System.out.println(heap); //[1, 2, 3, 4, 5, 6] for (Integer i:heap){ System.out.print(i+ " "); // 1 2 3 4 5 6 while (! (heap.isempty())){ System.out.print(heap.poll() + " "); // 1 2 3 4 5 6 //poll returns null if this queue is empty.
Arbres équilibrés Dans un ABR les opérations sont en temps O(hauteur(arbre)). Ce temps peut être O(n) où n est le nombre de nœuds. Un arbre équilibré est un arbre où la hauteur de l arbre est proche de logn de telle sorte que les opérations sont en temps O(logn) dans le pire des cas et pas seulement en moyenne. Arbres AVL, arbres rouge-noir, a-b-arbres, etc.
Arbres AVL Définition Un arbre AVL (Adelson-Velsky et Landis) est un arbre binaire de recherche tel que pour chaque nœud les hauteurs du sous-arbre gauche et du sous-arbre droit sont différentes de au plus 1. Définition On appelle balance d un nœud la hauteur du sous-arbre droit moins la hauteur du sous-arbre gauche. Dans un AVL la balance de chaque nœud est 1,0 ou +1. Dans un AVL la hauteur de l arbre est O(logn).
Exemple 50 17 72 12 23 54 76 9 14 19 67
Implémentation On ajoute un attribut aux nœuds qui est la balance ou la hauteur du sous-arbre pour que la balance soit calculée en temps constant. Cet attribut est mis à jour à chaque modification sur un nœud.
Implémentation en C typedef int Element; /* structures pour des arbres AVL */ typedef struct node { Element element; int bal; /* balance of a node 0, 1 or -1*/ struct node * leftchild, * rightchild; Node; typedef struct node * Tree; /* add an element in a balanced tree */ /* returns the (signed) variation of the height */ int addtree(element e, Tree *A);
Ajout d un élément On ajoute un élément comme dans un arbre binaire de recherche. Un des sous-arbres peut devenir déséquilibré de une unité (la balance peut devenir 2 ou +2). On remonte en mettant les hauteurs à jour jusqu à rencontrer un arbre déséquilibré. On ré-équilibre le premier sous-arbre qui devient déséquilibré lors de la remontée. Il devient équilibré avec la même hauteur qu avant l ajout. Il est dont inutile de continuer les ré-équilibrages plus haut.
A 2 B 1 Rotation gauche > A 0 B 0
A 2 B 0 Rotation gauche > A 1 B 0
A 2 C 0 Double rotation gauche > B A 1 0 C 0 B 0
A 2 C 1 Double rotation gauche > B A 1 0 C 0 B 1
A 2 C 1 Double rotation gauche > B A 1 1 C 0 B 0
Ré-équilibrage par rotations et doubles rotations /* entree : A arbre tel que bal(a) = -2 ou +2 */ /* et ses sous-arbres gauche et droit sont AVL */ /* sortie : A arbre AVL */ Tree balance(tree A) { /* called on a nonempty tree A */ if (isemptytree(a)) return A; if (A -> bal == 2) { if (A -> rightchild-> bal >= 0) { /* 0 ou +1 */ /* left rotation*/ return leftrotate(a); else { /* double left */ return doubleleftrotate(a); if (A -> bal == -2) {... return A;
Ajout /* returns the (signed) variation of the height of A, */ int addtree(element e, Tree *A) { int dh; if (isemptytree(*a)) { *A = constree(e,0,emptytree(),emptytree()); return 1; if ((*A) -> element == e) { return 0; if (e > (*A) -> element) { dh = addtree(e,&(*a)->rightchild); else { /* e < (*A) -> element*/ dh = - addtree(e,&(*a)->leftchild);
Ajout suite if (dh == 0) { return 0; else { (*A)-> bal += dh; *A = balance(*a); if ((*A) -> bal == 0) return 0; else return 1;
Rotation gauche Tree leftrotate(tree A) { Tree B = A -> rightchild; int a = A -> bal; int b = B -> bal; A -> rightchild = B -> leftchild; B -> leftchild = A; if (b <= 0) { A-> bal = a - 1 ; else { A-> bal = a - b -1; if (A-> bal >= 0) { /* nouveau a */ B-> bal = b - 1; else { B-> bal = A-> bal + b-1; return B;
Doubles rotations Tree doubleleftrotate(tree A) { A -> rightchild = rightrotate(a -> rightchild); return leftrotate(a); Tree doublerightrotate(tree A) { A -> leftchild = leftrotate(a -> leftchild); return rightrotate(a);