Carole Blanc Année 2014-2015 Carole Blanc : [email protected] Site WEB : http://dept-info.labri.u-bordeaux.fr/~blanc/ENS/ASD/ 4 groupes de TD : ◦ 1-G Mélançon , 2-C. Blanc, 3- C Gavoille, 4- S. Gueorguieva 3 DS en séance de TD (coef 0,4). 1 examen en fin de semestre (coef 0,6). ◦ Note= 0.6*EX +0.4*max(CC, EX) 2 sessions (janvier et juin). Complexité Récursivité Type abstrait Containeur Implémentation Définition 1.1 L'efficacité d'un algorithme est mesurée par son coût (complexité) en temps et en mémoire. La complexité d'un algorithme se mesure en calculant : ◦ le nombre d'opérations élémentaires, ◦ la taille de la mémoire nécessaire, pour traiter une donnée de taille n. On considèrera dans ce cours que la complexité des instructions élémentaires les plus courantes sur un ordinateur a un temps d'exécution constant égal à 1. Centre d’intérêt pour l'algorithmique c'est l'ordre de grandeur au voisinage de l'infini de la fonction qui exprime le nombre d'instructions ou la taille de la mémoire. Question : L’infini de quoi ? Définition 1.2 On définit les trois complexités suivantes : ◦ Complexité dans le pire des cas : C>A(n)=max{CA(d),d donnée de taille n} ◦ Complexité dans le meilleur des cas : C<A(n)=min{CA(d),d donnée de taille n} ◦ Complexité en moyenne : C A ( n) Pr (d ) C A (d ) d instance de A où Pr(d) est la probabilité d'avoir en entrée une instance d parmi toutes les données de taille n. Cas Particulier : Problème NP-complet C’est un problème pour lequel on ne connait pas d'algorithme correct efficace : réalisable en temps et en mémoire. L'ensemble des problèmes NP-complets ont les propriétés suivantes : Si on trouve un algorithme efficace pour un problème NP complet alors il existe des algorithmes efficaces pour tous, Personne n'a jamais trouvé un algorithme efficace pour un problème NP-complet, Personne n'a jamais prouvé qu'il ne peut pas exister d'algorithme efficace pour un problème NP-complet particulier. Le plus célèbre est le problème du voyageur de commerce. Définition 1.3: Lorsqu'un algorithme contient un appel à lui-même, on dit qu'il est récursif. Lorsque deux algorithmes s'appellent l'un l'autre on dit que la récursivité est croisée Complexité Un algorithme récursif nécessite de conserver les contextes récursifs des appels. La récursivité peut donc conduire à une complexité mémoire plus grande qu'un algorithme itératif. Exemple : calcul de la fonction factorielle : fonction facRecur(val :entier):entier; debut si n==0 alors retourner(1) sinon retourner(n*facRec(n-1)) finsi fin finfonction Exemple : calcul de la fonction factorielle fonction facIter(val n:entier):entier; debut var i,p:entier; p=1: pour i=2 à n faire p=p*i; finpour retourner(p) fin Finfonction La fonction factIter est meilleure en temps et mémoire (voir). Définition 1.4 : Un algorithme récursif présente une récursivité terminale si et seulement si la valeur retournée par cet algorithme est une valeur fixe, ou une valeur calculée par cet algorithme. L'algorithme facRecur ne présente pas de récursivité terminale. L'algorithme facRecur ne présente pas de récursivité terminale. fonction facRecur(val n:entier):entier; debut si n==0 alors retourner(1) sinon retourner(n*facRec(n-1)) finsi fin finfonction Exemple fonction facRecurTerm(val n:entier; val res:entier):entier; debut si n==0 alors retourner(res) sinon retourner(facRecurTerm(n-1,n*res)) finsi fin finfonction L'algorithme facRecurTerm présente une récursivité terminale. La factorielle se calcule par l'appel facRecurTerm(n,1) Intérêt : les compilateurs détectent cette propriété et optimisent le stockage de l'environnement de la fonction. Ainsi facRecurTerm aura une complexité identique à facIter. ATTENTION. Dans le cas d'un algorithme présentant deux appels récursifs, rendre la récursivité terminale ne permet pas obligatoirement au compilateur d'obtenir une complexité inférieure. Définition 1.5 : Un type abstrait est un triplet composé : ◦ d'un nom, ◦ d'un ensemble de valeurs, ◦ d'un ensemble d'opérations (souvent appelé primitives) définies sur ces valeurs. D'un point de vue complexité, on considère que les primitives (à part celle d'initialisation si elle existe) ont une complexité en temps et en mémoire en O(1). Pour désigner un type abstrait on utilise une chaine de caractères. Exemple Les nombres complexes ne sont pas des types de bases. On peut les définir comme un type abstrait : ◦ nom : nombreComplexe ◦ ensemble de valeur : réel×réel ◦ primitives : multiplication : (nombreComplexe × nombreComplexe) → nombreComplexe addition : (nombreComplexe × nombreComplexe) → nombreComplexe module : nombreComplexe → réel Fin 1er cours Définition 1.6 : Un containeur est un type abstrait permettant de représenter des collections d'objets ainsi que les opérations sur ces objets. Les collections que l'on veut représenter peuvent être ordonnées ou non, numériques ou non. L'ordre est parfois fourni par un évènement extérieur. Les collections d'objets peuvent parfois contenir des éléments identiques. Primitives : Accès valeur : containeur → objet Modification creerContaineur: containeur → vide ajouter : containeur X objet → vide supprimer : containeur X objet → vide detruireContaineur : containeur → vide Exemple Un ensemble de nombres complexes peut être défini par un containeur dont les objets sont des nombreComplexe. Définition 1.7 : L'implémentation consiste à choisir une structure de données et les algorithmes associés pour réaliser un type abstrait La structure de données utilisée pour l'implémentation peut elle-même être un type abstrait. L'implémentation doit respecter la complexité des primitives à part celle d'initialisation (celle-ci ne s'exécutera qu'une fois). Exemple Le type abstrait nombreComplexe peut être implémenté de la manière suivante : nombreComplexe=structure nombre Complexe r:réel; Nom Type i:réel; r réel Finstructure i var c : nombreComplexe; Nom variable c Type nombreComplexe c.r réel c.i réel réel Exemple Le type abstrait Etudiant peut être implémenté de la manière suivante : Etudiant=structure Nom variable Type nom:chainedecar; prenom:chainedecar; E.note reel numero:entier E.numero entier note:reel E.nom chaine Finstructure var E1 : Etudiant; Var E2 :Etudiant; E1.numero>E2.numero Exemple Le type abstrait nombreComplexe peut être implémenté de la manière suivante : nombreComplexe=structure r:réel; i:réel; Finstructure fonction op::*(val a,b:nombreComplexe):nombreComplexe; var c:nombreComplexe; debut c.r=a.r*b.r-a.i*b.i; c.i=a.r*b.i+a.i*b.r; retourner(c) fin Exemple fonction op::+(val a,b:nombreComplexe):nombreComplexe; var c:nombreComplexe; debut c.r=a.r+b.r; c.i=a.i+b.i; retourner(c) fin fonction module(val a:nombreComplexe):réel; debut retourner(sqrt(a.r*a.r+a.i*a.i)) fin Exemple Un containeur de nombreComplexe peut être implémenté par un tableau de nombreComplexe. containeur d'objet=tableau[1..N]de structure v:objet; b:booleen; Finstructure T 1 2 3 4 5 6 N v objet b booléen Modification fonction creerContaineur(ref C:containeur de nombreComplexe):vide; var i:entier; debut pour i allant de 1 à N faire C[i].b=faux; finPour; C fin 1 2 3 4 5 v … N nombreComplexe b F F F F F F…F F F booléen Complexité : ? Modification fonction ajouter(ref C: containeur de nombreComplexe;val x:nombreComplexe):vide var i:entier; debut i=1; tant que i<=N et C[i].b faire i=i+1; fintantque si i<=N alors C[i].v=x; C C[i].b=vrai 1 2 3 4 5 … N finsi fin v c1 c21 c5 c2 nombreComplexe b V V V V F F…F F F booléen Complexité : ? Modification fonction supprimer(ref C: containeur de nombreComplexe;val x:nombreComplexe):vide var i:entier; debut i=1; tant que i<=N et C[i].v!=x faire i=i+1; fintantque C si i<=N alors 1 2 3 4 5… N C[i].b=faux v c1 c21 c5 c2 nombreComplexe finsi b V V F V F…F F F booléen fin Complexité : ? Modification fonction supprimer(ref C: containeur de nombreComplexe;val x:nombreComplexe):vide var i:entier; debut i=1; tant que i<=N faire si C[i].v == x et C[i].b alors C[i].b=Faux break finsi i=i+1; fintantque fin Complexité : ? Modification fonction detruireContaineur(ref C : containeur de nombreComplexe):vide debut pour i allant de 1 à N faire C[i].b=faux; finPour; fin Complexité : ? Accès fonction valeur(ref C : containeur de nombreComplexe):nombreComplexe; /* retourne le 1er nombre complexe présent/* var i:entier; 2 conditions d’arrêt debut i=1; tant que i<=n et !C[i].b faire i=i+1; fintantque Test en sortie de boucle si i<=n alors retourner(C[i].v) sinon retourner(NULL) finsi fin Complexité : ? Définition Liste simplement chainée Liste doublement chainée Implémentation par un tableau du type listeSC Implémentation par allocation dynamique du type listeSC Définition 2.1 Une liste est un containeur tel que le nombre d'objets (dimension ou taille) est variable, L'accès aux objets se fait indirectement par le contenu d'une clé qui le localise de type curseur. Un curseur est un type abstrait dont l'ensemble des valeurs sont des positions permettant de localiser un objet dans le containeur. Dans le cas où il n'y a pas de position, la valeur par défaut est NIL. Si c est un curseur, les primitives considérées dans ce cours sont les suivantes : accès à l'élément désigné par le curseur : contenu(c): curseur → objet accès à la valeur du curseur : getCurseur(c) : curseur → valeur_de_curseur positionnement d'un curseur : setCurseur(c,valeur) : curseur X valeur_de_curseur → vide existence d'un élément désigné par le curseur : estVide(c) : curseur → {vrai,faux} La manipulation des éléments de la liste dépend des primitives définies comme s'exécutant en temps O(1). Définition 2.2 Une liste est dite simplement chainée si les opérations suivantes s'effectuent en O(1). accès fonction valeur(val L:liste d'objet):objet; /* si la clé==NIL alors le résultat est NULL */ fonction debutListe(ref L:liste d'objet); /* positionne la clé sur le premier objet de la liste */ fonction suivant(ref L:liste d'objet); /* avance la clé d'une position dans la liste */ fonction listeVide(val L:liste d'objet): booleen; /* est vrai si la liste ne contient pas d'élément */ fonction getCléListe(val L: liste d'objet):curseur; /* permet de récupérer la clé de la liste */ Modification fonction supprimerEnTete(ref L:liste d'objet):vide; /* supprime un objet en debut de liste, la clé est positionnée*/ /*sur la tête de liste */ fonction setCléListe(ref L: liste d'objet, val c:curseur):vide; /* permet de positionner la clé de la liste*/ fonction detruireListe(ref L:liste d'objet):vide; Modification fonction creerListe(ref L:liste d'objet):vide; fonction insererApres(ref L:liste d'objet, val x:objet;):vide; /* insère un objet après la clé, la clé ne change pas */ fonction insererEnTete(ref L:liste d'objet, val x:objet):vide; /* insère un objet en debut de liste, la clé est positionnée sur la tête de liste */ fonction supprimerApres(ref L:liste d'objet):vide; /* supprime l'objet après la clé, la clé ne change pas */ Détection fin de liste fonction estFinListe(val L:liste d'objet):booléen; debut retourner(valeur(L)==NULL) fin Chercher un élément dans une liste fonction chercher(ref L:liste d'objet; ref x:objet): booleen; debut debutListe(L); tant que !estFinListe(L) et valeur(L)!=x faire suivant(L); fintantque retourner (!estFinListe(L)) /* la clé vaut NIL ou est positionné sur l'objet */ fin Finfonction Complexité: minimum : O(1) maximum : O(n) Supprimer un élément dans la liste s'il existe fonction supprimer(ref L:liste d'objet; ref x:objet): vide; var tmp:curseur; /* on suppose que l'objet se trouve dans la liste */ debut debutListe(L); tmp=NIL; /*on cherche l’objet dans la liste */ tant que !estFinListe(L) et contenu(getCléListe(L))!=x faire tmp= getCléListe(L); suivant(L); fintantque . . . Supprimer un élément dans la liste s'il existe fonction supprimer(ref L:liste d'objet; ref x:objet): vide; . . . /* 2 cas en sortie de la boucle tantque */ si tmp==NIL alors /*la clé est sur la tête de liste */ supprimerEnTete(L) sinon setCléListe(L,tmp); /*la clé est sur l'objet précédent l'objet à supprimer*/ supprimerAprès(L); finsi fin finfonction Complexité: minimum : O(1) maximum : O(n) fonction supprimer(ref L:liste d'objet; ref x:objet): vide; var tmp:curseur; debut debutListe(L); tmp=NIL; tant que !estFinListe(L) et contenu(getCléListe(L))!=x faire tmp= getCléListe(L); suivant(L); fintantque si tmp==NIL alors supprimerEnTete(L) sinon setCléListe(L,tmp); supprimerAprès(L) finsi fin finfonction Exercice: Réfléchir aux problèmes que soulèvent l'introduction de getCléListe et surtout setCléListe? Que faut-il en déduire? Doit-on vraiment les garder? Définition 2.2: Une liste doublement chainée est une liste pour laquelle les opérations en temps O(1) sont celles des listes simplement chainées auxquelles on ajoute les fonctions d'accès fonction finListe(ref L:liste d'objet):vide; /* positionne la clé sur le dernier objet de la liste */ fonction precedent(ref L::liste d'objet): vide; /* recule la clé d'une position dans la liste */ Supprimer un élément fonction supprimer(ref L:liste d'objet; ref x:objet): booleen; debut si chercher(L,x) alors précédent(L); si valeur(L)!=NULL alors supprimerAprès(L); sinon supprimerEnTete(L) fin retourner(vrai) sinon retourner(faux) finsi fin Complexité : finfonction minimum : O(1) maximum : O(n) Chaque élément du tableau est une structure ◦ objet ◦ indexSuivant Le champ indexSuivant désigne une entrée du tableau. Ainsi l'accès au suivant est en complexité O(1). Pour une liste de caractère la zone de stockage peut donc être décrite par : stockListe = tableau[1..tailleStock] d'elementListe ; elementListe=structure valeur : car; indexSuivant : entier ; finstructure; Dans ce contexte, le type curseur est un entier compris entre 1 et tailleStock. Il faut coder la valeur NIL : on peut par exemple choisir 0 La valeur du champ indexSuivant est donc un entier compris entre 0 et tailleStock. Le premier élément doit être accessible en O(1), il faut donc conserver son index. On peut donc représenter une liste par la structure suivante : listeSC_Car=structure tailleStock:entier; vListe:stockListe; premier:curseur; cle:curseur; finstructure; Le tableau de stockage étant grand mais pas illimité, il faudra prévoir que l'espace de stockage puisse être saturé. Primitives d'accès Ces fonctions sont immédiates. fonction debutListe(ref L:listeSC_Car):vide; debut L.cle=L.premier; fin finfonction fonction suivant(ref L:listeSC_Car):vide; debut L.cle=L.vListe[L.cle].indexSuivant; fin finfonction Primitives d'accès fonction listeVide(ref:listeSC_Car): booléen; debut retourner(L.premier==0); fin finfonction Gestion de l'espace de stockage Pour ajouter un élément, il faut pouvoir trouver un élément "libre" dans le tableau. Une solution compatible avec la complexité des primitives consiste à gérer cet espace de stockage en constituant la liste des cellules libres (voir un exemple) On modifie donc en conséquence la description de listeSC_Car : listeSC_Car=structure tailleStock:entier; vListe:stockListe; premier:curseur; premierLibre:curseur; cle:curseur; finstructure; Gestion de l'espace de stockage Par convention, l'espace de stockage sera saturé lorsque l'index premierLibre vaut 0 (la liste des cellules libres est vide). On définit donc la fonction de test : fonction listeLibreVide(ref L:listeSC_Car):booléen; debut retourner(L.premierLibre==0); fin finfonction Gestion de l'espace de stockage On définit deux primitives liées à la gestion de la liste des libres : 1. mettreCellule : insère une cellule libre en tête de la liste des cellules libres L’opération correspondante est de type insererEnTete fonction mettreCellule (ref L:listeSC_Car,val P:curseur):vide; debut L.vListe[P].indexSuivant=L.premierLibre; L.premierLibre=P; fin finfonction Gestion de l'espace de stockage On définit deux primitives liées à la gestion de la liste des libres : 2. prendreCellule : prend la cellule libre en tête de la liste des cellules libres. L’opération correspondante est de type supprimerEnTete. fonction prendreCellule(ref L:listeSC_Car):curseur; var nouv:curseur; debut nouv=L.premierLibre; L.premierLibre=L.vListe[nouv].indexSuivant; retourner nouv; fin finfonction Deux primitives de modifications fonction creer_liste(ref L:listeSC_Car):vide; var i:curseur; debut L.tailleStock=tailleMax; L.premier=0; L.premierLibre=1; pour i allant de 1 à L.tailleStock-1 faire L.vListe[i].indexSuivant=i+1; finpour L.vListe[tailleStock].indexSuivant=0; L.cle=0; fin finfonction Deux primitives de modifications fonction insérerAprès(ref L:listeSC_Car;val x:car):booléen; var tmp,nouv:curseur; debut si L.cle==0 ou L.premierLibre==0 alors retourner faux; sinon tmp=L.cle; nouv=prendreCellule(L); L.vListe[nouv].valeur=x; L.cle=L.vListe[L.cle].indexSuivant; /*suivant(L)*/ L.vListe[nouv].indexSuivant=L.cle; L.vListe[tmp].indexSuivant=nouv; L.cle=tmp; retourner vrai; finsi; fin ; finfonction Réfléchir aux problèmes que soulèvent l'introduction de getCléListe et surtout setCléListe? Que déduire? Faut-il vraiment les garder? Dans ce choix d’implémentation on a curseur=entier. Supposons tailleStock=1000. La séquence suivante mènera à une incohérence. setCléListe(L,10000); suivant(L); Le fait d'avoir introduit ces primitives permet lors de l'utilisation du type abstrait de modifier la clé de type curseur et donc par la même de pouvoir rendre la structure de donnée incohérente en cas de mauvaise utilisation. Pointeur : Définition et syntaxe Définition : Un pointeur est une variable qui contient une adresse mémoire. Pour déclarer un pointeur on écrit : nom_pointeur=curseur; Par convention un pointeur qui ne donne accès à aucune adresse contient la valeur NIL. Pour accéder à l'emplacement mémoire désigné par le pointeur on écrit : nom_pointeur^ Pointeur : Définition et syntaxe La primitive new permet d'allouer dynamiquement de la mémoire au cours d'une exécution. On écrira: new(nom_pointeur); Lorsque la mémoire n'est plus utilisée par le pointeur il faut impérativement la libérer. La primitive delete permet de libérer la mémoire allouée par l'intermédiaire d'un pointeur, on écrira : delete(nom_pointeur); Chaque élément de la liste est une structure : (valeurElement,pointeurSuivant) Le champ pointeurSuivant est une adresse en mémoire, par suite, l'accès au suivant est en complexité O(1). Dans ce contexte le type curseur est un pointeur vers un élément. La zone de stockage peut donc être décrite par : cellule=structure valeurElement:car; pointeurSuivant:^cellule; finstructure; curseur=^cellule; La zone de stockage peut donc être décrite par : cellule=structure valeurElement:car; pointeurSuivant:^cellule; finstructure; curseur=^cellule; listeSC_car=structure premier:curseur; cle:curseur; finstructure NIL correspond donc à l'absence d'élément suivant. Primitives d'accès fonction listeVide(val L:listeSC_car):booléen; debut retourner L.premier==NIL; fin finfonction listeVide est utilisée si nécessaire avant les autres. fonction valeur(val L:listeSC_car):car; debut retourner L.cle^.valeurElement; fin finfonction Primitives d'accès fonction premier(val L:listeSC_car):vide; debut L.cle=L.premier; fin; finfonction fonction suivant(val L:listeSC_car):vide; debut L.cle=L.cle^.pointeurSuivant; fin finfonction Trois primitives de modifications fonction creer_liste(ref L:listeSC_Car):vide; debut L.premier=NIL; L.cle =NIL; fin finfonction Trois primitives de modifications On suppose que la clé est au debut de la liste fonction supprimerEnTete(ref L:listeSC_Car):vide; var P:curseur; debut P=L.premier; suivant(L); L.premier=L.cle; delete(P); fin finfonction Trois primitives de modifications fonction insererApres(val x:car; ref :listeSC_Car):vide; var nouv:curseur; debut new(nouv); nouv^.valeurElement=x; nouv^.pointeurSuivant=L.cle^.pointeurSuivant; L.cle^.pointeurSuivant=nouv; fin finfonction L'implémentation dans un tableau permet d'avoir un bloc contigu de mémoire ce qui va minimiser les accès disques. Ceci n'est pas le cas pour l'implémentation par pointeurs. L'implémentation dans un tableau nécessite de fixer au préalable le nombre maximum de cellules qui va contraindre fortement les applications : la structure de donnée peut avoir beaucoup trop de cellules ou au contraire trop peu. L'implémentation par pointeur va être très dépendante de l'implémentation des modules d'allocation dynamique du langage choisi Définitions Primitives de piles, exemples Primitives de files, exemples Implémentation des piles Implémentation des files Les piles et les files sont des containeurs dans lesquels on ne peut accéder qu'à un objet particulier. Définition 3.1. : Dans une pile, l’objet accessible est le dernier inséré (LIFO, Last-In, First-Out). Définition 3.2: Dans une file, l‘objet accessible est le plus ancien dans la file (FIFO, First-In, First-Out). On écrira pour déclarer des variables : type_pile=pile de objet; type_file=file de objet; Une pile est définie par les opérations suivantes : accès fonction valeur(val P:pile de objet):objet; fonction pileVide(val P:pile de objet):booléen; modification fonction creerPile(ref P:pile de objet):vide; fonction empiler(ref P:pile de objet; val:objet):vide; fonction depiler (ref P:pile de objet):vide; fonction detruirePile(ref P:pile de objet):vide; fonction listeInverse(ref L:listeSC de objet):listeSC de objet; var P:pile de objet; var LR:liste de objet; debut creerListe(LR); creerPile(P); debutListe(L); tant que !finListe(L) faire empiler(P,valeur(L)); suivant(L); fintantque insererEnTete(LR,valeur(P)) depiler(P); tant que non(pileVide(P)) faire insererApres(LR,valeur(P)); suivant(LR); depiler(P); fintantque; detruirePile(P) retourner(LR); fin finfonction Une file est définie par les opérations suivantes : Accès : fonction valeur(val F:file de objet):objet; fonction fileVide(val F:file de objet):booléen; Modification : fonction creerFile(ref F:file de objet):vide; fonction enfiler(ref F:file de objet; val v:objet):vide; fonction defiler(ref F:file de objet):vide; fonction detruireFile(ref F:file de objet):vide; fonction compteFile(ref F:file de entier): entier; var v,compt:entier; debut compt=0; enfiler(F,0); tant que valeur(F)!=0 faire compt=compt+1; v=valeur(F); defiler(F); enfiler(F,v); fintantque; defiler(F); retourner(compt); fin finfonction fonction inverserFile(ref F:file de entier):file d'entier; var P:pile d'entier; var FS:file d'entier; debut creerPile(P); tant que non(pileVide(P)) creerFile(FS); faire enfiler(F,0); v=valeur(P); tant que valeur(F)!=0 enfiler(FS,v); faire depiler(P); v=valeur(F); fintantque; defiler(F); detruirePile(P); enfiler(F,v); retourner(FS); empiler(P,v); fin fintantque finfonction defiler(F); Chaque objet de la pile est un élément du tableau. On doit de plus avoir un champs qui permet d'accéder au sommet de pile. pile d'objet=structure taille:entier; sommet:entier; pile:tableau[1..taille] d'objets; finstructure; accès fonction valeur(ref P:pile de objet):objet; debut retourner(P.pile[P.sommet]); fin finfonction fonction pileVide(ref P:pile de objet):booléen; debut retourner(P.sommet==0); fin finfonction modification fonction empiler(ref P:pile de objet; x:objet):booleen; /* l'espace de stockage peut être saturé */ debut si P.sommet==P.taille alors retourner(FAUX) sinon P.sommet=P.sommet+1; P.pile[P.sommet]=x; retourner(VRAI) finsi fin finfonction modification fonction depiler(ref P:pile de objet):vide; debut P.sommet=P.sommet-1; fin finfonction fonction creerPile(ref P:pile de objet):pile de objet; debut P.sommet=0; fin finfonction Chaque objet de la pile est un objet de la listeSC. pile d'objet=listeSC de objet; accès : fonction valeurPile(ref P:pile de objet):objet; debut debutListe(P); retourner(valeurListe(P)); fin finfonction fonction pileVide(ref P:pile de objet):booléen; debut retourner(listeVide(P)); fin finfonction modification fonction empiler(ref P:pile de objet; x:objet):vide; debut insérerEnTete(P,x) fin finfonction fonction depiler(ref P:pile de objet):vide; debut supprimerEnTete(P); fin finfonction modification fonction creerPile(ref P:pile de objet):vide; debut creerListe(P); fin finfonction Chaque objet de la file est un élément du tableau. On utilise le tableau de manière circulaire avec un pointeur donnant le premier et un autre donnant le dernier. file d'objet=structure taille : entier; premier : entier; dernier : entier; plein : booléen; file : tableau[0..taille-1] d'objets; finstructure; accès fonction valeur(ref F:file de objet):objet; debut retourner(F.file[F.premier]); fin finfonction fonction fileVide(ref F:file de objet):booléen; debut retourner(F.premier==F.dernier & non(F.plein)); fin finfonction Modification fonction enfiler(ref F:file de objet; x:objet):booleen; debut si F.plein alors retourner(FAUX) sinon F.file[F.dernier]=x; F.dernier=(F.dernier+1) mod F.taille; F.plein=F.dernier==F.premier; retourner(VRAI) finsi fin finfonction Modification fonction creerFile(ref F:file de objet):file de objet; debut F.premier= 0; F.dernier= 0; F.plein=FAUX; fin finfonction Modification fonction defiler(ref F:file de objet):vide; debut F.premier=(F.premier+1) mod F.taille; F.plein=Faux fin finfonction Chaque objet de la file est un objet de la liste_DC car il faut un accès au dernier. file d'objet=liste_DC de objet; Accès : fonction fileVide(ref F:file de objet):booléen; debut retourner(listeVide(F)); fin finfonction modification fonction enfiler(ref F:file de objet; x:objet):vide; debut dernier(F); insererApres(F,x); fin finfonction fonction defiler(ref F:file de objet):vide; debut supprimerEnTete(F); fin finfonction modification fonction creerFile(ref F:file de objet):vide; debut creerListe(F); fin finfonction cellule=structure valeurElement:objet; pointeurSuivant:^cellule; finstructure; curseur=^cellule; file d'objet=structure premier:curseur; dernier:curseur finstructure accès fonction valeurFile(ref F:file de objet):objet; debut retourner(F.premier^.valeurElement); fin finfonction fonction fileVide(ref F:file de objet):booléen; debut retourner(F.premier==NIL); fin finfonction modification fonction enfiler(ref F:file de objet; x:objet):vide; var c:curseur; debut new(c); c^.valeurElement=x; c^.pointeurSuivant=NIL; si F.premier==NIL alors F.premier=c finsi F.dernier^.pointeurSuivant=c; F.dernier=c; fin finfonction modification fonction defiler(ref F:file de objet):vide; var c:curseur; debut c=F.premier; si F.premier=F.dernier alors F.dernier=NIL finsi F.premier=c^.pointeurSuivant; delete(c) fin finfonction modification fonction creerFile(ref F:file de objet):vide; debut F.premier=NIL; F.dernier=NIL; fin finfonction modification fonction detruireFile(ref F:file de objet):vide; debut tantque !fileVide(F) faire defiler(F) fintantque fin finfonction Arbre et arborescence Arbres binaires Parcours d'un arbre binaire Implémentation d'un arbre binaire Retour sur les arborescences Parcours d'un arbre planaire Implémentation d'un arbre planaire Définition 4.1: Un arbre est un graphe connexe sans cycle. Un sous arbre est un sous graphe d'un arbre. Propriété 4.1: Si un arbre a n sommets alors il a n-1 arêtes. Idée de la démonstration: ceci s'appuie sur les deux propriétés suivantes des graphes connexes. Tout graphe connexe ayant n sommets a au moins n-1 arêtes. Tout graphe connexe ayant n sommet et au moins un cycle a au minimum n arêtes. La taille d'un arbre est le nombre de sommets de l'arbre. Propriété 4.2: Entre deux sommets quelconques d'un arbre, il existe une unique chaîne les reliant. Idée de la démonstration : Pour deux sommets quelconques Il ne peut exister deux chaines différentes les reliant sinon il y aurait un cycle dans l'arbre. Il existe au moins une chaine puisque un arbre est un graphe connexe. Définition 4.2: Une arborescence est définie à partir d'un arbre en choisissant un sommet appelé racine et en orientant les arêtes de sorte qu'il existe un chemin de la racine vers tous les autres sommets. Arbre Arborescence Définitions 4.3 : On appelle fils d'un sommet s tout sommet s' tel que (s,s') est une arête de l'arbre. On notera qu'une arborescence est un exemple d'ensemble partiellement ordonné (relation d'ordre "fils de"). On appelle feuille de l'arbre un sommet qui n'a pas de successeur . Tout autre sommet est appelé sommet interne. On appelle hauteur d'un sommet de l'arbre la longueur du chemin de la racine à ce sommet. Définition 4.4: Un arbre planaire est défini en ordonnant les arêtes sortantes de chaque sommet. On notera qu'un arbre planaire est un exemple d'ensemble totalement ordonné (relation "est fils ou est frère à droite"). Définition 4.5: Un arbre binaire est un arbre planaire dont chaque sommet a au plus deux fils. Définition 4.6: Un arbre binaire complet est un arbre binaire dont chaque sommet interne a exactement deux fils. . Arbre planaire Racine Premier Fils Frère droit Arbre Binaire Arbre Binaire complet Propriété 4.3: Tout sommet x d'un arbre binaire vérifie l'une des deux propriétés suivantes: x est une feuille, x a un sous arbre binaire dit gauche de racine G(x) et un sous arbre binaire droit de racine D(x). X feuille G(X) D(X) Définition 4.7: Un arbre binaire parfait est un arbre binaire complet dans lequel toutes les feuilles sont à la même hauteur dans l'arbre. Théorème 4.1: Un arbre binaire de taille n a une hauteur moyenne de log2(n). Théorème 4.2: Il existe une bijection qui transforme un arbre planaire ayant n sommet en un arbre binaire complet ayant 2n+1 sommets. Du fait de ce théorème, on ne considère dans un premier temps que le type arbre binaire que l'on nommera arbreBinaire. Chaque sommet permet d'accéder à deux sommets : le fils gauche . le fils droit. Ce type sera nommé sommet. Chaque sommet permet également d'accéder à l'objet qu'il stocke. Un arbre binaire peut être vu comme un curseur indiquant le sommet racine. De la même manière un sommet est un curseur. On a donc : arbreBinaire=curseur; sommet=curseur; Le type sommet présente les primitives suivantes : Accès : fonction getValeur(val S:sommet):objet; /* vaut NULL si le sommet n'existe pas /* fonction filsGauche(val S:sommet):sommet; /* vaut NIL si S n'a pas de fils gauche */ fonction filsDroit(val S:sommet):sommet; /* vaut NIL si S n'a pas de fils droit */ fonction pere(val S:sommet):sommet; /* vaut NIL si S est la racine de l'arbre */ Modification : fonction setValeur(ref S:sommet;val x:objet):vide; /* affecte au sommet S la valeur x */ fonction ajouterFilsGauche(ref S:sommet,val x:objet):vide; /* filsGauche(S)==NIL doit être vérifié */ fonction ajouterFilsDroit(ref S:sommet,x:objet):vide; /* filsDroit(S)==NIL doit être vérifié */ fonction supprimerFilsGauche(ref S:sommet):vide; /* filsGauche(S) est une feuille */ fonction supprimerFilsDroit(ref S:sommet):vide; /* filsDroit(S) est une feuille */ fonction detruireSommet(ref S:sommet):vide; /* S est une feuille */ Modification : fonction creerArbreBinaire(val Racine:objet):sommet; fonction detruireArbreBinaire(ref A:arbreBinaire d'objet):vide; Détection de feuille fonction estFeuille(val S:sommet):booléen; debut retourner(filsGauche(S)==NIL et filsDroit(S)==NIL) fin Le parcours d'un arbre binaire consiste à donner une liste de sommets dans l'arbre. Le prototype d'algorithme suivant permet d'effectuer les parcours selon les algorithmes associés aux traitements à partir d'un sommet de l'arbre. 1e 3 1 r3 1a 3 2 2 1 i 3 1z 3 2 2 2 1g 3 2 1m 3 2 fonction parcoursArbreBinaire(val A:arbreBinaire d'objet):vide; // Déclarations locales debut // traitement1; si estFeuille(A)alors // traitement2; sinon // traitement3; si filsGauche(A)!=NIL alors // traitement4; parcoursArbreBinaire(filsGauche(A)); // traitement5; finsi // traitement6; si filsDroit(A)!=NIL alors // traitement7; parcoursArbreBinaire(filsDroit(A)); // traitement8; finsi // traitement9; finsi // traitement1O; fin Complexité: Si le traitementi a pour complexité ci(n), soit c(n) = La complexité intrinsèque de l'algorithme est : 10 𝑖=1 𝑐𝑖 (𝑛) O(n c(n)). On distingue quatre parcours qui conditionnent les algorithmes sur les arbres binaires. Le parcours hiérarchique ou en largeur Parcours préfixe : on affiche la racine, les sommets du sous arbre gauche, puis les sommets du sous arbre droit. Parcours infixe: on affiche les sommets du sous arbre gauche, puis la racine puis les sommets du sous arbre droit. Parcours suffixe : on affiche les sommets du sous arbre gauche, puis les sommets du sous arbre droit puis la racine. 1e 3 1r 3 2 1g 3 2 2 1a 3 1 i 3 1m 3 2 2 2 1z 3 2 Parcours hiérarchique Parcours préfixe Parcours infixe Parcours postfixe : : : : e,r,g,a,i,m,z e,r,a,i,z,g,m a,r,z,i,e,g,m a,z,i,r,m,g,e Affichage des valeurs des sommets pour un parcours donné Soit un arbre étiqueté par des caractères. On considère que l'on dispose de la fonction qui affiche un caractère : fonction afficher(val n:entier):vide; Affichage dans le parcours préfixe pas de déclarations locales. traitement 2, 3 : afficher(valeur(A)); Affichage dans le parcours infixe : pas de déclarations locales. traitement 2, 6: afficher(valeur(A)); Affichage dans le parcours postfixe pas de déclarations locales. traitement 2, 9 : afficher(valeur(A)); Lister les étiquettes d'un arbre dans un tableau fonction arbre2Tableau(val A:arbreBinaire d'entier; ref T:tableau[1..N] d'entier; ref i:entier):vide; debut i=i+1; T[i]=valeur(A); si !estFeuille(A)alors si filsGauche(A)!=NIL alors arbre2Tableau(filsGauche(A),T,i); finsi si filsDroit(A)!=NIL alors arbre2Tableau(filsDroit(A),T,i); finsi finsi fin Hauteur d'un arbre binaire fonction hauteurArbreBinaire(val s:sommet):entier debut si estFeuille(s)alors retourner(0) sinon var tmp1,tmp2:entier; tmp1=0; tmp2=0; si filsGauche(s)!=NIL alors tmp1= hauteurArbreBinaire(filsGauche(s)); finsi si filsDroit(s)!=NIL alors tmp2=hauteurArbreBinaire(filsDroit(s)); finsi retourner(1+max(tmp1,tmp2)); finsi fin Hauteur d'un arbre binaire : Autre version fonction hauteurArbreBinaireSimp(val s:sommet):entier debut si s==NIL alors retourner(-1) sinon retourner(1+ max(hauteurArbreBinaireSimp(filsGauche(s)), hauteurArbreBinaireSimp(filsDroit(s)))); finsi fin Remarque : La fonction hauteurArbreSimp (même si elle est plus courte) a une complexité plus grande que la fonction hauteurArbreBinaire notamment en nombre d'appel récursif. Elle est moins performante. Taille d'un sous-arbre d'un arbre binaire complet. fonction tailleArbreBinaire(val A: arbreBinaire):entier; debut si estFeuille(A) alors retourner(1) sinon retourner(1+tailleArbreBinaire(filsGauche(A)) +tailleArbreBinaire(filsDroit(A)) finsi fin L’implémentation se fait par allocation dynamique. On définit cellule=structure info:objet; gauche:sommet; droit:sommet; pere:sommet; finstructure sommet=^cellule; gauche info droit Pere accès fonction getValeur(val S:sommet):objet; debut retourner(S^.info); fin fonction filsGauche(val S:sommet):sommet; debut retourner(S^.gauche) fin modification fonction creerArbreBinaire(val racine:objet):sommet; var tmp:sommet; debut new(tmp); tmp^.info=racine; tmp^.gauche=NIL; tmp^.droit=NIL; tmp^.pere=NIL; retourner(tmp) fin modification fonction ajouterFilsGauche(ref S:sommet,val x:objet):vide; var tmp:sommet; debut new(tmp); tmp^.info=x; tmp^.gauche=NIL; tmp^.droit=NIL; tmp^.pere=S; S^.gauche=tmp; fin modification fonction supprimerFilsGauche(ref S:sommet):vide; var tmp:sommet; debut tmp=S^.gauche; S^.gauche=NIL; delete(tmp); fin modification fonction detruireArbreBinaire(ref A:arbreBinaire d'objet):vide; debut si estFeuille(A)alors delete(A) sinon si filsGauche(A)!=NIL alors detruiresArbreBinaire(filsGauche(A)); finsi si filsDroit(A)!=NIL alors detruireArbreBinaire(filsDroit(A)); finsi delete(A); finsi fin On peut définir un type abstrait sommetArbrePlanaire par les primitives suivantes: accès fonction getValeur(val S:sommetArbrePlanaire):objet; fonction premierFils(val S:sommetArbrePlanaire):sommetArbrePlanaire; fonction frere(val S:sommetArbrePlanaire):sommetArbrePlanaire; fonction pere(val S:sommetArbrePlanaire):sommetArbrePlanaire; modification fonction creerArbrePlanaire(val racine:objet): sommetArbrePlanaire; fonction ajouterFils(ref S:sommetArbrePlanaire, val x:objet):vide; /* ajoute un fils comme cadet */ fonction supprimerSommet(ref S: sommetArbrePlanaire):vide; /* le sommet doit être une feuille */ fonction detruireArbrePlanaire(ref S: sommetArbrePlanaire):vide; Un arbre planaire est de type sommetArbrePlanaire. C'est un curseur. Le parcours d'un arbre planaire consiste à donner une liste de tous les sommets. fonction parcoursArbrePlanaire(val A:sommetArbrePlanaire):vide; // Déclarations locales var f: sommetArbrePlanaire; debut // traitement1; f= premierFils(A); tant que f!=NIL faire // traitement2; parcoursArbrePlanaire(f); // traitement3; f=frere(f); // traitement4 fintantque // traitement5 fin On distingue trois parcours qui conditionnent les algorithmes sur les arbres planaires : Le parcours hiérarchique qui s'effectue grâce à une file. Le Parcours préfixe : on liste la racine, les sommets de chaque sous arbre dans l'ordre où les sous arbres apparaissent. Le Parcours suffixe : on liste les sommets des sous arbres en ordre inverse puis la racine. 6 7 3 1 12 2 0 4 10 9 8 5 Implémentation dans le type arbreBinaire L'implémentation dans le type arbreBinaire découle du théorème 4.2 On a alors arbrePlanaire=arbreBinaire. Pour un noeud donné : la primitive filsGauche donne accès au premier fils du noeud. la primitive filsDroit donne accès au frère du noeud. la primitive pere donne accès soit au père du noeud soit à son frère précédent. Il faut donc redéfinir la primitive pere pour les arbres planaires. fonction pereArbrePlanaire(val S: sommetArbrePlanaire):sommetArbrePlanaire; var T: sommetArbrePlanaire; debut T=filsGauche(pere(S)); tant que T!=S faire S=pere(S) T=filsGauche(pere(S)); fintantque retourner(pere(S)) fin En utilisant le théorème 4.1, cette dernière primitive à une complexité moyenne O(log2n). Implémentation par allocation dynamique Cette implémentation permet de diminuer le temps d'accès au père. cellule=structure info:objet; premierFils:sommet; frere:sommet; pere:sommet; Finstructure Dans cette implémentation, on initialise parfois le champ frere du dernier frère à l'adresse du premier fils. On peut vérifier aisément que les primitives sont toutes réalisables en O(1). L'espace mémoire est le même que celui occupé par une implémentation dans le type arbreBinaire Accès : fonction getValeur(val s:sommetArbrePlanaire):objet; début retourner(s^.info) fin fonction premierFils(val s:sommetArbrePlanaire): sommetArbrePlanaire; début retourner(s^.premierFils) fin Accès : fonction frere(val s : sommetArbrePlanaire) : sommetArbrePlanaire; début retourner(s^.frere) fin fonction pere(val s : sommetArbrePlanaire): sommetArbrePlanaire; début retourner(s^.pere) fin Modification : fonction creerArbrePlanaire(val racine:objet):sommetArbrePlanaire; var tmp:^cellule; début new(tmp); tmp^.info=racine; tmp^.premierFils=NIL; tmp^.frere=NIL; tmp^.pere=NIL; retourner(tmp) fin Modification : fonction detruireArbrePlanaire(ref s:sommetArbrePlanaire):vide; var tmp,f: sommetArbrePlanaire; début f= premierFils(s); tant que f!=NIL faire detruireArbrePlanaire(f); f=frere(f); fintantque supprimerSommet(s) fin fonction ajouterFils(ref s:sommetArbrePlanaire,val x:objet):vide; /* ajoute un fils comme cadet cette fonction n'est pas en O(1) */ var tmp:^celluleAP; début new(tmp); tmp^.info=x; tmp^.frere=NIL; tmp^.pere=s; tmp^.premierFils=NIL si s^.premierFils==NIL alors s^.premierFils=tmp; sinon r=premierFils(s); tantque frere(r)!=NIL faire r=frere(r) fintantque r^.frere=tmp fin fonction supprimerSommet(ref s:sommetArbrePlanaire):vide; /* le sommet doit être une feuille */ var p,r,tmp:^celluleAP; début r=premierFils(pere(s)); si r==s alors p=pere(s); p^.premierFils=s^.frere; sinon tantque frere(r)!=s faire r=frere(r); fintantque r^.frere=s^.frere; finsi delete(s) fin Arbre binaire de recherche Modification d'un arbre binaire de recherche Equilibrage Définition 5.1 : Dans un arbre binaire de recherche, quel que soit x un sommet interne d'étiquette val(x), soit LG(x) (resp. LD(x)), l'ensemble des étiquettes du sous arbre gauche (resp. droit) de x. On a : ∀𝑦 ∈ 𝐿𝐺 𝑥 , ∀𝑧 ∈ 𝐿𝐷 𝑥 , 𝑦 ≤ 𝑣𝑎𝑙(𝑥) < 𝑧 Propriété 5.1: Si on parcourt un arbre binaire de recherche en ordre infixe, on obtient une séquence d'étiquettes triées en ordre croissant. Corollaire 5.1: Si n est le nombre de sommets d'un arbre binaire de recherche, on obtient la liste triée en O(n). On utilise les primitives des arbres binaires. fonction recherche(val x:sommet, val e:objet):sommet; var tmp:objet; debut si x==NIL alors retourner(NIL) sinon tmp= getValeur(x); si tmp==e alors retourner(x); sinon si e <=tmp alors retourner(recherche(filsGauche(x),e)); sinon retourner(recherche(filsDroit(x),e)); finsi finsi Complexité finsi minimum : fin maximum : fonction recherche(val x:sommet, val e:objet):sommet; var tmp:objet; debut si x==NIL alors retourner(NIL) sinon tmp= getValeur(x); si tmp==e alors retourner(x); sinon si e <=tmp alors retourner(recherche(filsGauche(x),e)); sinon retourner(recherche(filsDroit(x),e)); finsi finsi Complexité finsi minimum : fin maximum : fonction cherchePlusPetit(val x:sommet):sommet; debut tantque filsGauche(x)!=NIL faire x=filsGauche(x); fintantque retourner(x); fin Complexité minimum : maximum : Dans l’ordre croissant, où se trouve le suivant d’un élément x dans un ABR ? 8 14 6 10 2 0 4 3 27 18 5 32 fonction chercheSuivant(val x: sommet, val e : objet):sommet; sinon var p:sommet; p=pere(x); debut tantque p!=NIL faire x=cherche(x,e); si filsGauche(p)==x alors si x==NIL alors retourner(p) retourner(NIL); sinon sinon x=p; si filsDroit(x)!=NIL alors p=pere(p); return(cherchePlusPetit(filsDroit(x)) Complexité minimum : maximum : finsi fintantque retourner(NIL); finsi finsi fin Les primitives ajouter et supprimer des objets permettent de faire évoluer un ABR. fonction ajouter(ref x:sommet, sinon val e:objet):vide; s=filsDroit(x); var s:sommet; si s==NIL alors debut ajouterFilsDroit(x,e); si e <= valeurSommet(x) alors sinon s=filsGauche(x); ajouter(s,e); finsi si s==NIL alors finsi ajouterFilsGauche(x,e); fin sinon ajouter(s,e); finsi Complexité minimum : maximum : fonction supprimer(ref x:sommet):vide; /*Tous les elts sont différents */ sinon var p,f,y:sommet; f=filsDroit(x); debut si f!=NIL si estFeuille(x) alors y=cherchePlusPetit(f); p=pere(x); sinon /*traiter le cas racine*/ f=filsGauche(x); si filsGauche(p)==x alors y=cherchePlusGrand(f); finsi supprimerFilsGauche(p) var v : valElement; sinon v=getValeur(y); supprimerFilsDroit(p) supprimer(y); finsi setValeur(x,v); finsi fin Complexité minimum : maximum : Dans le cas général la fonction supprimer applique l’algorithme suivant : Si le sommet à supprimer : est une feuille on l'enlève a 1 fils on le remplace par son fils a 2 fils on remplace sa valeur par la valeur précédente dans l’ordre croissant et on supprime le sommet qui portait cette valeur dans l’ABR. La complexité des opérations sur un ABR dépendant de la hauteur de l'arbre, il est important qu'un ABR reste aussi proche que possible d'un arbre binaire parfait de manière à ce que la hauteur soit minimum. L'équilibrage d'un ABR peut-être obtenu par un algorithme de type "diviser pour régner". On récupère la liste des éléments triés dans un tableau T[1..N] où N est la taille de l'arbre de départ et on reconstruit l'arbre. fonction lister(val x:sommet, ref T:tableau[1..N] d'objet, ref i:entier):vide debut si estFeuille(x)alors i=i+1; T[i]= getValeur(x); sinon si filsGauche(x)!=NIL alors lister(filsGauche(x),T,i); finsi i=i+1; T[i]= getValeur(x); si filsDroit(x)!=NIL alors lister(filsDroit(x),T,i); finsi finsi fin L'appel : liste(A,T,0) fournit, en utilisant l'ordre infixe, dans le tableau T la liste des valeurs dans l'ordre croissant de l'arbre A. fonction detruireArbreBinaire(ref A:arbreBinaire d'objet):vide; debut si s!=NIL alors detruireArbreBinaire(filsGauche(s)); supprimerFilsGauche(s); detruireArbreBinaire(filsDroit(s)); supprimerFilsDroit(s); finsi fin fonction equilibre(ref A:arbreBinaire de objet):vide; var N:entier; var T:tableau[1..N]d'objet; debut N=tailleArbre(A); lister(A,T,0); detruireArbreBinaire(A); delete(A); A=construire(T,1,N)) fin fonction construire(ref T:tableau[1..N]d'objet,ref d,f:entier):sommet; var m:entier; var c,s:sommet; si s!=NIL alors debut s^.pere=c; si d≤f alors finsi m=(d+f)//2; s=construire(m+1,f); new(c); c^.droit=s setValeur(c,T[m]); si s!=NIL alors si d==f alors s^.pere=c; c^.gauche=NIL; finsi c^.droit=NIL; finsi retourner(c); retourner(c); sinon sinon s=construire(d,m-1); retourner(NIL) c^.gauche=s; finsi fin Définition Ajouter et supprimer dans un tas Max Implémentation Files de priorité Définition 6.1 : Un tas max (resp.tas min) T est un arbre binaire quasi-parfait étiqueté par des objets comparables (ie : il existe un ordre total) tel que tout nœud a une étiquette plus grande ou égale (resp. plus petite) que ses fils. Propriété 6.1 : La hauteur d'un tas est O(log(n)). L’ajout d’un élément se fait en conservant la structure ABQP Un tas est un containeur et un arbre binaire, il dispose donc des primitives des arbres binaires ainsi que ceux d'un containeur : fonction valeur(ref T:tas d'objet): objet; // renvoie l'objet stocké à la racine de l'arbre fonction ajouter(ref T:tas de objet, val v:objet):vide; // ajoute l'objet dans le tas fonction supprimer(val T:tas de objet):vide; // suppression de la racine et tassement de l'arbre fonction creerTas(ref T:tas,val:v:objet):vide; fonction detruireTas(ref T:tas):vide; Pour ajouter une valeur v dans un tas, on crée une nouvelle feuille dans l'arbre quasi-parfait en lui affectant la valeur v. Soit (r=s0,...,sk) le chemin de la racine à cette nouvelle feuille. Pour i allant de k à 1 si la valeur stockée dans si est plus grande que celle stockée dans si-1 alors on échange ces valeurs. On fait une réorganisation montante : un exemple est ici Dans un tas supprimer un élément consiste toujours à supprimer la racine. Pour supprimer la valeur dans un tas, on remplace la valeur de la racine par la valeur v de la dernière feuille de l'arbre. On supprime cette feuille. On fait descendre la valeur v dans l'arbre par échange avec la valeur la plus grande d'un des fils si celle ci est plus grande. On fait une réorganisation descendante : un exemple est ici On utilise la numérotation des nœuds dans le parcours hiérarchique d’un AB. La racine est numérotée 1. Le fils gauche de la cellule numéro i a pour numéro 2*i Le fils droit de la cellule numéro i a pour numéro 2*i +1 1 3 2 4 5 6 7 8 9 6 27 2 0 10 7 32 5 1 2 3 4 8 6 9 2 5 6 7 8 9 27 0 10 10 11 12 13 14 15 7 32 16 17 18 19 5 20 21 On utilise cette propriété pour représenter un tas dans un tableau. De plus dans les opérations d'ajout et suppression des valeurs, on devra pouvoir parcourir l'arbre. Un curseur sera donc utile. tas=structure arbre:tableau[1..tailleStock] d'objet; tailleTas:entier; finstructure; curseur=entier; sommet=entier; De ce fait, les primitives arbre binaire prennent comme paramètre un tas et non un sommet. Les fonction ajouter et supprimer sont spécifiques au tas. accès fonction getValeur(ref T:tas d'objet; Val s:sommet):objet; debut retourner(T.arbre[s]); fin; fonction valeur(ref T:tas d'objet):objet; debut retourner(T.arbre[1]); fin fonction filsGauche(val s:sommet):sommet; debut retourner(2*s); fin Les fonction ajouter et supprimer sont spécifiques au tas. accès fonction filsDroit(val s:sommet):sommet; debut retourner(2*s+1); fin fonction pere(val s:sommet):sommet; debut retourner(partieEntiere(s/2)); fin fonction tasPlein(ref T:tas d'objet):booleen; debut retourner(T.tailleTas==tailleStock) fin modification fonction setValeur(ref T:tas d'objet; val s:sommet; val x:objet):vide; debut T.arbre[s]=x; fin fonction creerTas(ref T:tas d'objet; val x:objet):vide; debut T.arbre[1]=x; T.tailleTas=1; fin Gestion du tas fonction ajouter(ref T:tas d'objet, val v:entier):vide debut T.tailleTas=T.tailleTas+1; T.arbre[T.tailleTas]=v; reorganiseTasMontant(T,tailleTas); fin Gestion du tas fonction reorganiseTasMontant(ref T: tas d'objet; val x:sommet):vide; var p:sommet; var signal:booléen; debut p=pere(x); signal=vrai; tantque x!=1 et signal faire si getValeur(T,x)>getValeur(T,p) alors échanger(T.arbre[p],T.arbre[x]) x=p; p=pere(x); sinon signal=faux finsi fintantque fin Gestion du tas fonction supprimer(ref T:tas d'objet):vide; var r:objet; debut T.arbre[1]=T.arbre[T.tailleTas]; T.tailleTas=T.tailleTas-1; reorganiseTasDesc(T,1); fin Gestion du tas fonction reorganiseTasDesc(ref T:tas d'objet, val x:sommet):vide; var g,d:sommet; debut g= filsGauche(x); d= filsDroit(x); si g!=NIL alors si d!=NIL alors si getValeur(T,d)>getValeur(T,g) alors g=d; finsi; finsi si getValeur(T,x)<getValeur(T,g) alors échanger(T.arbre[x],T.arbre[g]); reorganiseTasDesc(T,g) finsi finsi Définition 6.2 : Une file de priorité est un tas dans lequel on a la possibilité de modifier les valeurs des sommets. On dispose donc d'une primitive supplémentaire : fonction changeValeur(ref T:tas d'objet, val s:sommet, val v:objet):vide; debut setValeur(T,s,v); si v > getValeur(T,pere(s)) alors reorganiseTasMontant(T,s) sinon si v < getValeur(T,filsDroit(s)) ou v < getValeur(T,filsGauche(s)) alors reorganiseTasDesc(T,s) finsi Complexité : finsi fin Définitions Fonction de hachage Adressage chainé Adressage ouvert Réorganisation d'une table de hachage Soient a et b deux entiers. Soient q et r respectivement leur quotient et reste dans la division Euclidienne, on a : a=b*q + r. On dit que a est égal à r modulo b et on écrira a=r[b]. Définition 7.1 : Soit K un ensemble de valeurs et m un entier naturel, une fonction de hachage hm est une fonction définie de K dans {0,...,m-1}. hm : K {0,...,m-1} Les éléments de K sont appelées clés. Si k est une clé, h(k) est dite valeur de hachage de k. Définition 7.2 : Soit m un entier naturel et hm une fonction de hachage, une table de hachage est un containeur tel que : le nombre d'éléments de la table (dimension ou taille) est fixe, l'accès aux éléments s'effectue indirectement par la valeur de hachage de la clé. Table de hachage Exemple 0 K 1 2 k0 ki … hm(ki) kj ki m-1 Remarques Les tables de hachage sont très utiles dans le cas où l'amplitude des clés est grande (structure de tableau non utilisable) et les éléments à gérer sont "assez figés" ce qui permet d'espérer un temps d'accès à l'information proche de O(1) (structure de liste trop pénalisante). Pour une séquence d'élément, il est clair qu'il peut exister deux clés k1 et k2 de K telles que hm(k1)=hm(k2), on dit alors qu'il y a collision des clés k1 et k2. Soit p dans [0..m-1], il est possible qu'il n'existe pas de clé k dans K telle que hm(k)=p. La structure de données pour représenter une table de hachage est un tableau de dimension [0..m-1]. Pour une clé k de K, T[hm(k)] donne l'accès à l'élément de clé k. Plus qu'à la clé elle-même le plus souvent on souhaite accéder aux informations associées à la clé. La clé est donc stockée ainsi que l'adresse permettant d'accéder aux informations. On précisera ces moyens d'accès aux paragraphes concernant la gestion des collisions : adressage chainé , adressage ouvert. On nommera le type abstrait tableHash. La valeur de hachage d'une clé est un curseur. Les primitives d'accès à une table de hachage sont : accès fonction chercher(ref T:tableHash_de_cle, val v:cle):curseur; modification fonction creerTablehachage(ref T: tableHash_de_cle, ref h:fonction):vide; fonction ajouter(ref T:tableHash de cle, val x:cle):booleen; fonction supprimer(ref T:tableHash de cle, val :cle):vide; fonction detruireTablehachage(ref T:tableHash de cle): vide; Elles sont définies en fonction de hm et du mode de gestion des collisions. Les fonctions de hachage doivent pouvoir s'appliquer sur des objets de n’importe quel type de base (entier, car, réel, etc.) Les types numériques peuvent aisément être ramené à un entier. Pour les chaines de caractères, il est toujours possible de leur associer de manière bijective un entier. Considérons la fonction asc qui a un caractère associe son code ASCII (codé sur 7 bits). Soit c0...cp une suite de p caractères alors la valeur entière associée bijectivement est : 𝑝 𝑎𝑠𝑐 𝑐𝑖 ∗ 128𝑖 𝑖=0 Dans la suite on ne s'intéressera donc qu'à des clés entières. Méthode de division Soit m un entier premier pas trop proche d'une puissance de 2 : ∀ 𝑘 ∈ ℕ, ℎ𝑚 𝑘 = 𝑘[𝑚] Méthode de multiplication Soit m=2p et A dans ]0..1[, ∀ 𝑘 ∈ ℕ, ℎ𝑚,𝐴 𝑘 = (𝑚 𝑘𝐴 − 𝑘𝐴 ) Cette méthode fonctionne mieux pour certaines valeurs de A que pour d’autres. D.E. Knuth a étudié le problème et suggère 5−1 𝐴~ 2 Famille de hachage universel Définition 7.3 : Soit H un ensemble de fonction de hachage de U dans [0..m[. On dit que la famille H est universelle si pour toutes clés k1, k2 dans U, le nombre de fonctions h de H telles que h(k1)=h(k2) est égal à card(H)/m. Probabilité de collision : 1/m Soit m un entier naturel. Soit p un nombre premier grand tel que : ∀𝑘 ∈ 𝐾, 𝑘 ∈ 0 … 𝑝 − 1 On définit la famille de fonction : Hp,m ∀𝑘 ∈ 𝐾, ℎ𝑎,𝑏 𝑘 = ( 𝑎𝑘 + 𝑏 𝑝 ) 𝑚 Théorème 7.1 : La famille de fonction Hp,m est universelle De ce fait, on peut montrer que l'exécution de n primitives a une complexité moyenne en O(n). Définition 7.4 : L'adressage d'une table de hachage est dit chainé si l'élément i du tableau donne accès à une structure de données permettant de stocker les clés k telles que hm(k)=i. Les valeurs sont donc stockées à l'extérieur de la table qui est un tableau de pointeurs. Par suite, si il n'existe pas de clé k telle que hm (k)=i alors T[i]=NIL. L'espace de stockage des clés peut être une liste doublement chainée. On a dans ce cas tableHash de clé=structure table:tableau[0..m-1] de listeDC de clé; h:fonction(val v:clé):entier finstructure Primitives d’accès : fonction chercher(ref T:tableHash_de_cle, val e:entier):curseur; debut retourner(chercherListe(T.table[T.h(e)],e)) fin Primitives de modification : fonction creerTableHach(ref T: tableHash_de_cle, ref h:fonction):vide; var i:entier; debut pour i allant de 0 à m-1 faire creerListe(T.table[i]) finpour T.h=h; fin fonction ajouter(ref T:tableHash_de_cle,val e:entier):booleen; debut insererEnTete(T.table[T.h(e)],e); retourner(vrai): fin Primitives de modification : fonction supprimer(ref T:tableHash_de_cle;val e:entier):vide; debut supprimer(T.table[T.h(e)],e)) fin Théorème 7.2 : Dans une table de hachage à adressage chainé, une recherche fructueuse ou infructueuse prend en moyenne O(1+n/m) si chaque élément à les mêmes chances d'être haché vers l'une quelconque des cases indépendamment des autres éléments (hachage uniforme). Définition 7.5 : L'adressage d'une table de hachage est dit ouvert si les éléments du tableau sont les clés elles mêmes. Les éléments de la table sont donc initialisés à NULL. La gestion de la collision se fait en trouvant une place libre dans la table. Pour cela on utilise une seconde fonction s(x,i) à valeur dans [0..m] que l'on compose avec hm(k). Cette seconde fonction dite de sondage doit vérifier les propriétés suivantes : s(hm(k),0)=hm (k) s(hm (k),i))i=0…m-1 est une permutation de la séquence (i) i=0…m-1 La méthode d'insertion consiste, en cas de collision pour une clé k, à sonder les places disponibles s(hm(k),i) à partir de i=0 jusqu'à m-1. Si au bout de m sondages, aucune place disponible n'a été détectée, la table est dite saturée. Quelques méthodes de sondage : Sondage linéaire : s(k,i)=(h(k)+i)[m] Sondage quadratique : s(k,i)=(h(k)+c1 i+c2 i2)[m] Double hachage : s(k,i)=(h(k)+i h'(k))[m] où h' est une seconde fonction de hachcode telle que ∀𝑘 ∈ 𝐾, ℎ′ 𝑘 𝑒𝑠𝑡 𝑝𝑟𝑒𝑚𝑖𝑒𝑟 𝑎𝑣𝑒𝑐 𝑚 L'espace de stockage des clés est alors un tableau. On a dans ce cas : tableHash_de_cle=structure table:tableau[0..m-1] de cle; h : fonction(val v : cle) : entier s : fonction(val v : cle; val i : entier) : entier finstructure Primitives d’accès : fonction chercher(ref T:tableHash_de_cle, val e:entier):curseur; var i:entier; debut i=0; tant que T.table[T.s(T.h(e),i)]!=e et i<m faire i=i+1 fintantque si i==m alors retourner(NULL) sinon retourner(i) finsi fin Primitives de modification fonction creerTablehachage(ref T: tableHash_de_cle, ref h : fonction(var x:entier):entier), ref s : fonction(var x:entier):entier):vide; var i:entier; debut pour i allant de 0 à m-1 faire T.table[i]=NULL; finpour T.h=h; T.s=s; fin Primitives de modification fonction ajouter(ref T:tableHash_de_cle, val e :entier):booleen; var i:entier; debut i=0; tant que T.table[T.s(T.h(e),i)]!=NULL et i<m faire i=i+1 fintantque si i==m alors retourner(faux) sinon T.table[T.s(T.h(e),i)]=e; retourner(vrai) finsi fin Primitives de modification fonction supprimer(ref T:tableHash_de_cle, val e:entier):vide; var p:curseur; debut p=chercher(T,e); T.table[p]=NULL fin Théorème 7.3 : Etant donné une table de hachage de facteur de remplissage r, une recherche infructueuse ou l'insertion d'une clé se fait en moyenne en 1/(1-r), si chaque élément a les mêmes chances d'être haché vers l'une quelconque des cases indépendamment des autres éléments (hachage uniforme ). Théorème 7.4 : Etant donné une table de hachage de facteur de remplissage r, une recherche fructueuse se fait en moyenne en (1/r)*ln(1/(1-r)), si chaque élément a les mêmes chances d'être haché vers l'une quelconque des cases indépendamment des autres éléments (hachage uniforme ). Le taux d'occupation de la table doit rester relativement faible pour minimiser le risque de collision sinon le nombre de collision augmente et la performance se dégrade. Ceci veut dire qu'on ajoute à chaque type abstrait un champ "taux de remplissage" égal au rapport du nombre d'éléments divisé par la taille de la table. Dès que ce taux dépasse un seuil fixé, la fonction "ajouter" effectue automatiquement l'appel à la fonction de redimensionnement de la table. Définition Implémentation Opérations d'ajout et suppression Un dictionnaire est une structure de donnée permettant de stocker des mots. Dans ce cours, on considère que le mot est stocké dans une variable de type "mot" dont on peut connaitre la longueur par la fonction "longueur". fonction longueur(ref M:mot):entier; Si M est un mot, M[i] est le ième caractère de M. Un dictionnaire est donc un containeur dont les primitives sont les suivantes. accès : fonction appartient((ref d:dictionnaire,val M::mot):booléen; modification : fonction fonction fonction fonction creerDictionnaire(ref d: dictionnaire):vide ajouter(ref d:dictionnaire,val M::mot):vide; supprimer(ref d:dictionnaire,val M:mot):vide; detruireDictionnaire(ref d:dictionnaire):vide; On considère que l'implémentation se fait dans le type arbreBinaire. On munit le dictionnaire d'un curseur de façon à ne pas passer ou déclarer un pointeur à chaque appel. type dico=structure a:sommet; /* l'arbre*/ p:sommet; :* le curseur */ finstructure Le principe est le suivant : Descendre sur le fils gauche correspond à passer à la lettre suivante dans un mot, Descendre sur le fils droit correspond à passer à une autre lettre en même position. Tous les mots se terminent par un même caractère que nous noterons "\0" et qui est stocké dans l'arbre. c a i s r m \0 r r e e e e \0 e \0 i r \0 \0 e \0 Pour ajouter un mot M dans un dictionnaire : on recherche le plus long préfixe du mot M contenu dans l'arbre à partir de la racine, on détermine ainsi un sommet S de l'arbre correspondant au i premières lettres du mot. On parcourt à partir de S la branche droite de l'arbre de manière à trouver deux sommets tels que : ◦ S2==filsDroit(S1), ◦ valeur(S1)<M[i+1]<valeur(S2) On crée un arbre "baguette" (tous les fils droits sont vide) B étiqueté par toutes lettres de M à partir du (i+1)ème caractère. on insère B entre les sommets S1 et S2 dans l'arbre Un exemple est ici Pour supprimer un mot M dans un dictionnaire : on recherche la feuille correspondante au mot, elle se trouve au bout d'un arbre "baguette" B, on détruit l'arbre B. Un exemple est ici fonction chercher_branche_droite(ref d : dico; ref c:car): booleen; var v: car; debut v=valeur(d.p) tant que filsDroit(d.p)<>NIL et v<c faire d.p=filsDroit(d.p) v= valeur(d.p) fintantque si v==c alors /*d.p est positionné sur la lettre c*/ retourner VRAI sinon si filsGauche(pere(d.p))!=d.p et filsDroit(d.p)<>NIL alors d.p=pere(d.p) finsi /* dans le cas ou d.p est le premier de la branche droite le sommet contient une valeur >C */ /* sinon d.p est sur l'elt précédant la place possible pour un ajout à droite */ retourner FAUX finsi fin fonction prefixe(ref d: dico; ref M:Mot;ref i:entier) :booleen; /*->VRAI M est dans le dico*/ /*->FAUX M[i] est la dernière lettre du prefixe dans le dico debut cas où cas M[i]=="\0" et valeur(d.p)=="\0" : /*le mot existe dans le dico*/ retourner VRAI cas M[i]==valeur(d.p) /* filsGauche(d.p)!=NIL nécessairement*/ d.p=filsGauche(d.p) i=i+1 /* On continue à chercher le préfixe /* retourner(prefixe(d,M,i)) cas chercher_branche_droite(d,M[i]) alors /*d.p est sur le caractère correspondant à M[i] dans dico on*/ /*repart de là prefixe se charge de descendre à gauche*/ retourner(prefixe(d,M,i)) autre cas : /* on a le prefixe max au i-1eme caractère. d.p est sur l'elt /*précédant la place possible*/ i=i-1 retourner FAUX fincasou fin fonction creer_arbre_baguette(ref M:mot;val i:entier):sommet; var s,tmp,c:sommet var j:entier; debut s=creerArbreBinaire(M[i]); tmp=s; pour j=i+1 a longueur(M) faire ajouterFilsGauche(tmp,M[j]); tmp=filsGauche(tmp) finpour retourner(s) fin fonction ajouter(ref d:dico,ref M:mot):vide debut d.p=d.a i=1 si !prefixe(d,M,i) alors s=creer_arbre_baguette(M, i+1) cas où : cas i=0 et pere(d.p)==NIL : /* cas de la racine*/ s^.droit=d.a s^.pere=nil d.a^.pere=s d.a=s cas M[i]>valeur(d.p) : /* insertion en milieu de branche droite */ s^.droit=d.p^.droit s^.pere=d.p si filsDroit(d.p)!=NIL alors /* !fin de branche droite */ s^.droit^.pere=s finsi d.p^.droit=s autre cas s^.droit=d.p; s^.pere=d.p^.pere; d.p^.pere=s; s^.pere^.gauche==s fincasou fin Arbre AVL Facteur d'équilibrage et rotation Implémentation Définition : Un arbre AVL est un arbre binaire éventuellement vide tel que la différence de hauteur entre le sous arbre gauche et le sous arbre droit d'un nœud diffère d'au plus 1. les arbres gauches et droits d'un sommet sont des arbres AVL Le nom AVL vient du nom des auteurs G.M. Adelson-Velsky et E.M.Landis (1982). Propriété : Si n est le nombre de nœuds d'un arbre AVL alors sa hauteur est log2(n) Chaque nœud contient un entier appelé facteur d'équilibrage qui permet de déterminer si il est nécessaire de rééquilibrer l'arbre. Définition : Soit s un sommet ayant pour sous arbre gauche (resp. droit) Gs (resp. Ds). Le facteur d'équilibrage eq(s) du sommet s est défini par : eq(s)= h(Ds)-h(Gs) avec h(NIL)=0 et si A est un arbre h(A)=hauteur(A)+1. Le facteur d'équilibrage d'un nœud d'un arbre AVL vaut 0,1 ou -1. Lors d'insertion ou suppression, l'arbre peut se déséquilibrer (valeur 2 ou -2), on utilise alors des rotations pour rééquilibrer l'arbre. Définition : Une rotation droite autour du sommet y d'un arbre binaire de recherche consiste à faire descendre le sommet y et à faire remonter son fils gauche x sans invalider l'ordre des éléments. L'opération inverse s'appelle rotation gauche autour du sommet x. Dans la rotation seuls les facteur d'équilibrage de X et Y sont modifiés. Notons eq'(X) et eq'(Y) les facteurs d'équilibrage après rotation. Propriété : Après une rotation droite autour du sommet Y, on a : eq'(X)=eq(X)+1+max(eq'(Y),0) eq'(Y)=eq(Y)+1-min(eq(X),0) Propriété : Après une rotation gauche autour du sommet X, on a : eq'(X)=eq(X)-1-max(eq(Y),0) eq'(Y)=eq(Y)-1+min(eq'(X),0) Les opérations d'insertion et suppression sont celles d'un arbre binaire de recherche dans lesquelles on ajoute la gestion du facteur d'équilibrage. Insertion : L'insertion aboutit à la création d'une feuille f. Considérons le premier ancêtre y de cette feuille qui viole la condition AVL, on a eq(y)=-2 ou eq(y)=2 et ces deux cas sont symétriques. Supposons que eq(y)=-2 et soit x le fils gauche de y. On a deux cas. soit eq(x)=-1 ou 0, on effectue une rotation droite, soit eq(x)=1, alors x a un sous arbre droit de racine z, qui a deux sous arbres. On effectue une rotation gauche autour de x puis une rotation droite autour de y. La suppression aboutit à la suppression d'une feuille . Considérons le premier ancêtre y de cette feuille qui viole la condition AVL, on a : eq(y)=-2 ou eq(y)=2 et ces deux cas sont symétriques. Supposons que eq(y)=-2 Soit x le fils gauche de y et t le fils droit de y. La feuille a été supprimée dans le sous arbre de racine t. On a trois cas : soit eq(x)=-1 , on effectue une rotation droite, soit eq(x)=0 , on effectue aussi une rotation droite, soit eq(x)=1, alors x a un sous arbre droit de racine z, qui a deux sous arbres. On effectue une rotation gauche autour de x puis une rotation droite autour de y. La hauteur du nouveau sous arbre a même hauteur que le sous arbre de départ si le facteur d'équilibrage de la racine est 0. Dans ce cas, il suffit donc d'une opération d'équilibrage pour que l'arbre soit AVL. Dans les autres cas il faut recommencer la même opération en remontant dans l'arbre. Le cas eq(y)=2 est obtenue par symétrie. Figures complètes ici L'intérêt du facteur d'équilibrage est que cette information nécessite 2 bits. Néanmoins les algorithmes permettant la gestion de eq sont délicats. Dans la suite, une cellule pour un arbre AVL doit contenir un champ supplémentaire qui est la hauteur. type celluleAVL=structure info:objet; hauteur:entier; gauche:sommet; droit:sommet; père:sommet; finstructure sommetAVL=^celluleAVL; On accède à ce nouveau champ par les deux primitives suivantes : fonction getHauteur(ref S:sommet):entier; /* 1 pour une feuille; 0 si NIL/* fonction setHauteur(ref S:sommet; val h:entier):entier; /* 1 pour une feuille; 0 si NIL/* Les fonctions implémentant les rotations sont immédiates. fonction rotationDroite(ref y:sommet):vide; var x,p:sommet; debut x=filsGauche(y); p=pere(y); si p!=NIL alors si y=filsGauche(p) alors p^.gauche=x; sinon p^.droit=x; finsi finsi x^.pere=p; y^.pere=x; y^.gauche=x^.droit; x^.droit=y; setHauteur(y,max(getHauteur(filGauche(y)),getHauteur(filsDroit(y)))+1); setHauteur(x,max(getHauteur(filGauche(x)),getHauteur(filsDroit(x)))+1); fin Les fonctions implémentant les rotations sont immédiates. fonction rotationGauche(ref x:sommet):vide; var y,p:sommet; début y=filsDroit(x); p=pere(x); si p!=NIL alors si x=filsGauche(p) alors p^.gauche=y; sinon p^.droit=y; finsi finsi y^.pere=p; x^.pere=y; x^.droit=y^.gauche; y^.gauche=x; setHauteur(x,max(getHauteur(filGauche(x)),getHauteur(filsDroit(x)))+1); setHauteur(y,max(getHauteur(filGauche(y)),getHauteur(filsDroit(y)))+1); fin Les fonctions d'insertion et suppression doivent prendre en compte le calcul du facteur d'équilibrage ainsi que le maintien de cet équilibre. fonction ajouter(ref x:sommet, val e:objet):vide; var s:sommet; début si e ≤ getValeur(x) alors s=filsGauche(x); si s==NIL alors ajouterFilsGauche(x,e); setHauteur(filsGauche(x),1) equilibreAprèsInsertion(filsGauche(x)) sinon ajouter(s,e); finsi sinon s=filsDroit(x); si s==NIL alors ajouterFilsDroit(x,e); setHauteur(filsDroit(x),1); equilibreAprèsInsertion(filsDroit(x)) sinon ajouter(s,e); finsi finsi fin fonction supprimer(ref x:sommet):booléen; var p,f,y:sommet; début si estFeuille(x) alors p=pere(x); si filsGauche(p)==x alors supprimerFilsGauche(p) setHauteur(p,getHauteur(filsDroit(p)+1)) sinon supprimerFilsDroit(p) setHauteur(p,getHauteur(filsGauche(p)+1)) finsi equilibreAprèsSuppression(p) sinon f=filsDroit(x); si f!=NIL y=cherchePlusPetit(f); sinon f=filsGauche(x); y=cherchePlusGrand(f); finsi setValeur(x,getValeur(y)); supprimer(y); finsi fin fonction equilibreAprèsInsertion(ref x:sommet):vide; var eq:entier; var s,p:sommet; debut eq=0; p=x; tantque pere(p)!=NIL et eq!=2 et eq!=2 faire s=p; p=pere(s); setHauteur(p,max(getHauteur(filGauche(p)),getHauteur(filsDroit(p)))+1); eq=getHauteur(filsDroit(p))-getHauteur(filsGauche(p)); fintantque si eq==2 ou eq==-2 alors equilibreUnSommet(p,s); finsi fin fonction equilibreUnSommet(ref p,s:sommet):vide; début si s==filsGauche(p) alors si getEquilibre(p)==-2 alors si getEquilibre(s)<1 alors rotationDroite(p); sinon rotationGauche(s); rotationDroite(p); finsi finsi sinon si getEquilibre(p)==2 alors si getEquilibre(s)>-1 alors rotationGauche(p) sinon rotationDroite(s); rotationGauche(p); finsi finsi finsi fin Dans le cas de suppression, on remonte à partir de la feuille supprimée dans l'arbre et de la même manière on détecte les nœuds déséquilibrés. Il est possible que plusieurs rotations soient nécessaires. En effet un déséquilibre peut en entrainer un autre. fonction equilibreAprèsSuppression(ref x:sommet):vide; var eq:entier; var s,p:sommet; début eq=1; p=x; p=pere(s); tantque p!=NIL et eq!=0 faire s=p; p=pere(p); setHauteur(p,max(getHauteur(filGauche(p)),getHauteur(filsDroit(p)))+1); eq=getHauteur(filsDroit(p))-getHauteur(filsGauche(p)); equilibreUnSommet(p,s); fintantque fin Définition Liste simplement chainée Quelques exemples d'algorithmes Liste doublement chainée Définition Une liste est un containeur tel que le nombre d'objets (dimension ou taille) est variable, L'accès aux objets se fait indirectement par le contenu d'une clé qui le localise de type curseur. Par exemple : curseur=^type_predefini; liste de type_predefini = curseur L NIL Définition : Une liste est dite simplement chainée si les opérations suivantes s'effectuent en O(1). Accès fonction premier(ref L: Liste):curseur; fonction suivant(ref L: Liste; val P:curseur):curseur; fonction listeVide(ref L:Liste):booléen; Modification fonction fonction fonction fonction fonction creerliste(ref L:Liste):vide; insererApres(ref L:Liste; val x:objet; ref P:curseur):vide; insererEnTete(ref L:Liste; val x:objet):vide; supprimerApres(ref L:Liste; val P:curseur):vide; supprimerEnTete(ref L:Liste):vide; Test de fin de liste fonction estDernier(ref L:Liste; ref P:curseur):booléen; début retourner(suivant(L,P)=NIL) fin Complexité: O(1). Chercher un élément dans une liste fonction chercher(ref L: Liste; ref E:objet):curseur; var p: curseur; début si listeVide(L) alors retourner(NIL) sinon p=premier(L); tant que non(estDernier(L,p)) et (contenu(p)!=e) faire p=suivant(L,p); fintantque si (contenu(p)!=e) alors retourner(NIL) sinon retourner(p) finsi finsi fin Complexité: O(n). Chercher un élément dans une liste fonction chercher(ref L: Liste; ref E:objet):curseur; var p: curseur; début si listeVide(L) alors retourner(NIL) si contenu(L)==E alors retourner(L) chercher(suivant(L,premier(L)),E) Complexité: O(n). Trouver le dernier élément fonction trouverDernier(ref L:liste):curseur; var p: curseur; debut si listeVide(L) alors retourner(NIL) sinon p=premier(L); tant que non(estDernier(L,p)) faire p=suivant(L,p); fintantque retourner(p) finsi fin Complexité: O(n). Trouver le dernier élément fonction trouverDernier(ref L:liste):curseur; var p: curseur; debut si listeVide(L) alors retourner(NIL) finsi p=premier(L); si(estDernier(L,p)) retourner(p) finsi trouverDernier(suivant(L,p)) fin Complexité: O(n). Calculer la taille d’une liste fonction taille(val L:Liste):entier; var p;curseur; var t:entier; debut si listeVide(L) alors retourner(0) sinon retourner(1+taille(suivant(L,premier(L)))) finsi fin finfonction Complexité: O(n). Insérer dans une liste triée On suppose la liste triée dans l'ordre croissant fonction insertionTrie(ref L:liste; val e: objet):vide; var p:curseur; début si listeVide(L) alors insererEnTete(L,e) sinon si contenu(premier(L))>e alors insererEnTete(L,e) sinon insererTrie(suivant(L,premier(L)),e) finsi finsi fin Complexité: O(n). Décrire l’algorithme pour supprimer un objet e dans une liste L. Définition : Une liste doublement chainée est une liste pour laquelle les opérations en temps O(1) sont celles des listes simplement chainées auxquelles on ajoute les fonctions d'accès fonction dernier(val L:Liste_DC):curseur; fonction precedent(val L:ListeDC; val P:curseur):curseur;