Algorithmique et Structure de Données DUT MCW1 1 Qu'est ce que l'algorithmique ? Un algorithme est une "spécification d'un schéma de calcul sous forme d'une suite finie d'opérations élémentaires obéissant à un enchaînement déterminé", ou encore : la description des étapes à suivre pour réaliser un travail. DUT MCW1 2 Programmation ? Un programme est la traduction d'un algorithme dans un langage accepté par la machine. problème Analyse Algorithme Programmation Programme évolué Compilation Un algorithme, à l'inverse d'un programme, est indépendant du langage de programmation (et donc de la machine). Programme en langage machine Édition des liens Programme exécutable Tests Fonctionne correctement ? Rapide ? Programme accepté DUT MCW1 3 Le langage algorithmique Pour communiquer, deux personnes utilisent un langage commun. Les informaticiens ont aussi besoin d'un langage plus ou moins codifié pour se comprendre, il faut donc se définir un langage algorithmique. Ce langage doit être: spécialisé (pour écrire des algorithmes, pas des poèmes ni des recettes de cuisine) de haut niveau (déchargé de détails techniques, ce n'est pas un langage de programmation concis ("si ça ne tient pas en une page, c'est que c'est trop long") modulaire typé DUT MCW1 4 Les variables Un algorithme manipule des données qui sont stockées dans des variables. Chaque variable possède un nom, donné par celui qui écrit l'algorithme. Elle possède également un emplacement mémoire. On peut voir la mémoire d'un ordinateur comme une suite d'emplacements, chacun identifié par une adresse. Les variables nécessitent des emplacement différents l’un de l’autre. Le type de la variable est donc important. L'adresse d'une variable est en fait l'emplacement de là où elle commence. Type de données de base : entiers (12, 732), caractères ('a', 'g', '3'), réels (-12.3 0,5 ), chaînes (de caractères), booléens (vrai ou faux). DUT MCW1 5 Les instructions (1) L'affectation : variable expression Lecture d'une donnée : lire( une variable ) Affichage d'une expression : écrire( liste d'expressions, de variables ou de constantes ) Sélection si : si expression-logique alors instruction(s) à effectuer si l'expression est vraie finsi DUT MCW1 6 Les instructions (2) Sélection si-sinon : si expression-logique alors instruction(s) à effectuer si l'expression est vraie sinon instruction(s) à effectuer si l'expression est fausse finsi DUT MCW1 7 Les instructions (3) Itération répéter : répéter instruction(s) à répéter jusqu‘à expression-logique Itération tant-que : Tant que expression-logique instruction(s) à répéter fintantque Itération pour : Pour variable val-init à val-fin faire instruction(s) à répeter finpour DUT MCW1 8 Écriture d'un algorithme On donnera un nom à tout algorithme, afin de pouvoir le réutiliser. Il est essentiel de préciser (s'il y en a) : les variables en entrée (les données du problèmes); les variables en sorties (les résultats); les variables à la fois en entrée et en sorties (les données modifiées par l'algo); les variables locales (utilisées juste dans cet algo). Les variables autres que les variables locales sont les paramètres formels de l'algorithme. Lorsqu'un autre algo appelle cet algo, c'est avec des paramètres effectifs. DUT MCW1 9 Exemple d’algorithme Algorithme Bonjour() // simple ecriture d'un message de bienvenue écrire("Bonjour les pogrammeurs !!") Fin Algo Multiplication(entier x, entier y) : retourne un entier /* qui fait la multiplication des 2 entiers x et y avec des additions */ variables locales : entiers i, result ; début result <- 0; pour i<- 1 à y faire result <- result + x; finpour retourner result; Fin Algo Principal() a, b : entiers ; début Bonjour(); écrire ("donnez deux entiers : "); lire(a,b); écrire (" leur multiplication donne : ", Multiplication(a,b) ); fin DUT MCW1 10 Récursivité Un algorithme est dit récursif lorsqu'il s'appelle lui même. Attention, la récursivité peut être cachée si par exemple un algorithme A appelle un algorithme B qui appelle l'algorithme A. Algo Factorielle(entier n) : retourne un entier début si n=1 alors retourner 1; sinon retourner n*factorielle(n-1); fin DUT MCW1 11 exemples d'algos récursifs (1) Algo pgcd(a,b:entier) : retourne un entier // plus grand diviseur commun début si a=b alors retourner a; sinon si a>b alors retourner pgcd(a-b,b); sinon retourner pgcd(b-a,a); finsi finsi fin DUT MCW1 12 exemples d'algos récursifs (2) Algo puissance(x:reel, n:entier) :retourne un reel // x^n d'une maniere moins habituelle début si n=0 alors retourner 1; sinon si n=1 alors retourner x; sinon si n=2 alors retourner x*x; sinon si n est pair alors retourner puissance(x*x,n/2); sinon retourner x*puissance(x*x,(n-1)/2); finsi finsi finsi finsi fin DUT MCW1 13 Comment écrire un algo récursif : exprimer le problème de "taille" n en fonction du même problème de taille(s) inférieur(s) mettre une condition d'arrêt : lorsque le problème est de taille suffisamment petite pour être résolu sans appel récursif. Attention : penser à tous les cas d'arrêts !! (on peut constater sur les exemples que c'est le cas à chaque fois) Un algorithme récursif est plus lent qu'un algorithme itératif car il y a la gestion des appels de fonctions (empilement et dépilement du contexte). DUT MCW1 14 Qualités d'un algorithme Qualité d'écriture : un algorithme doit être structuré, indenté, modulaire, avec des commentaires, etc. Terminaison : le résultat doit être atteint en un nombre fini d'étapes. Il ne faut donc pas de boucles infinies, il faut étudier tous les cas possibles de données, ... Validité : le résultat doit répondre au problème demandé. Performance : étude du coût (complexité) en temps et en mémoire. La complexité en mémoire (c'est à dire la place mémoire prise par l'algorithme) est un problème de moins en moins primordial vu les capacités techniques actuelles. Remarque : ne pas confondre un problème de mémoire saturée qui vient du programme avec la complexité mémoire de l'algorithme. On distingue la complexité en moyenne et la complexité dans le pire des cas et parfois on s'intéresse aussi au meilleur des cas. DUT MCW1 15 Complexité (1) Algo Somme(entier N) : retourne un entier locales : ... Début /* somme des N premiers entiers*/ S 0; nb 1; tant que nb<=N faire S S + nb; nb nb+1; fintantque; retourner S; fin DUT MCW1 Cet algorithme demande: 2 affectations; N+1 comparaisons; N sommes et affectations; N sommes et affectations Ce qui fait au total 5N+3 opérations élémentaires. Ce qui nous intéresse c'est un ordre de grandeur donc : la complexité de cet algo est de l'ordre de N, ou encore linéaire en N. 16 Complexité (2) Algo Somme(entier N) : retourne un entier; locales : ...; Début // somme des N premiers entiers S N*(N+1)/2; retourner S; fin Même problème résolu en temps constant (indépendant des données) ! DUT MCW1 17 Structure de données : liste linéaire (Liste chaînée) Une liste linéaire est la forme la plus courante d'organisation des données. On l'utilise pour stocker des données qui doivent être traitées de manière séquentielle. La structure doit également être évolutive, c'est à dire que l'on doit pouvoir ajouter et supprimer des éléments. Définition : Une liste est une suite finie (éventuellement vide) d'éléments de même type repérés selon leur rang dans la liste. On remarque que l'ordre des éléments est fondamental. Mais attention, il ne s'agit pas d'un ordre sur les valeurs des éléments mais d'un ordre sur les places dans la liste (rang) ! Chaque élément est rangé à une certaine position. Il ne faut pas confondre le rang et la position ! DUT MCW1 18 Primitives (1) On définit le type abstrait de données par le définition des primitives qui permettent de le manipuler : liste créer_liste() : creation d'une liste vide position début (liste L) : retourne la position du premier élément de la liste, INCONNUE si la liste est vide position fin (liste L) : retourne la position du dernier élément de la liste, INCONNUE si la liste est vide position suivante (position p, liste L) : retourne la position de l'élément qui suit celui en position p, INCONNUE si on sort de la liste position précédente (position p, liste L) : retourne la position de l'élément qui précède celui en position p, INCONNUE si on sort de la liste élément accès (position p,liste L) : retourne l'élément en position p DUT MCW1 19 Primitives (2) entier longueur (liste L) : retourne le nombre d'éléments contenus dans la liste insérer (element e, rang r, liste L) : rajoute l'élément dans la liste (qui est donc modifiée) au rang r supprimer (rang r, liste L) : supprime l'élément de rang r dans la liste (qui est donc modifiée) booléen liste_est_vide (liste L) : teste si la liste est vide, retourne VRAI ou FAUX elément ieme (rang r, liste L) : retourne l'element de rang r ajouter (element e, position p, liste L) : rajoute l'élément dans la liste (qui est donc modifiée) après celui en position p enlever (position p, liste L) : supprime l'élément de position p dans la liste (qui est donc modifiée) DUT MCW1 20 Exemples d'utilisation : Algo Afficher_Liste(liste L) locales : position i; Début i<-debut(L); tant que i<>INCONNUE faire afficher(acces(i,L)); i <- suivante(i,L); fin tant que Fin Algo Nb_Occurence(liste L,element e) : retourne un entier locales : position i; entier nb; Debut i<-debut(L); nb<-0; tant que i<>INCONNUE faire si (element_egaux(e,acces(i,L)) alors nb<-nb+1; i <- suivante(i,L); fin tant que retourner nb; fin DUT MCW1 21 Implémentation contiguë (1) Les éléments sont rangés les uns à côté des autres, dans un tableau. La i-ème case du tableau contient le ième élément de la liste. Le rang est égal à la position (des entiers tout simples) ! type position = entier type liste = tableau[1..N] d‘éléments DUT MCW1 22 Implémentation contiguë (2) Algo acces (position p, liste L):retourne un element debut retourner L[p]; fin Algo debut (liste L):retourne une position début si N >= 1 alors retourner 1; sinon retourner INCONNUE; finsi fin DUT MCW1 23 Implémentation contiguë (3) Algo suivante(position p,liste L) : retourne une position début si p<N alors retourner p+1; sinon retourner INCONNUE; finsi fin DUT MCW1 24 Implémentation contiguë (4) Algo inserer(element e, rang r, liste L) locales rang i début // décaler pour faire un trou pour iN à r (en décrémentant i) faire L[i+1] <- L[i] finpour // mettre l'element L[r] creer_element(e) /* changer la taille car il y a un elt de plus */ ... PROBLEME !!! ... DUT MCW1 25 Implémentation contiguë (5) Comment changer le nombre d'éléments dans le tableau ? Il est où dans la structure de données ce nombre d'éléments ? Avec tableau[1..N] d'éléments on dit bien qu'il y a N éléments mais on n'offre pas la possibilité de modifier ce N. C'est plus précis de faire : type liste = { tab : tableau d'éléments; N : entier // nb effectifs d'éléments } Attention, on ne parle plus de L[i] mais de L.tab[i]. Pas de N mais de L.N DUT MCW1 26 Implémentation contiguë (6) Algo longueur (liste L) retourner L.N; fin Algo acces (position p, liste L): retourne un element retourner L.tab[p]; fin Algo fin (liste L) : retourne une position retourner L.N; fin Algo est_vide (liste L) : retourne un booleen si L.N=0 alors retourner VRAI; sinon retourner FAUX; fin DUT MCW1 27 Implémentation contiguë (7) Algo inserer(element e, rang r, E?S liste L) locales rang, i; début // decaller pour faire un trou pour iL.N à r (en decrementant i) faire L.tab[i+1] L.tab[i]; finpour // mettre l'element L.tab[r] creer_element(e); // changer la taille car il y a un elt de plus L.N (L.N) + 1; fin L.tab est un tableau d'éléments. Mais un tableau de combien d'éléments ? On est obligé de se poser la question car il faut une zone de mémoire contigüe et donc il faut la réserver. DUT MCW1 28 Implémentation contiguë : Tableau de taille fixe On réserve à la création pour MAX éléments. Il faut néanmoins penser à tester lors des insertions que l'on ne dépasse pas ce MAX. Algo creer_liste_vide () : retourne une liste locales liste L; début L.t allouer_memoire(MAX); L.N 0; fin Algo inserer (element e, rang r, E/S liste L) locales rang, i; début si L.N=MAX alors écrire "erreur !!« ; sinon ..... // la suite normale fin DUT MCW1 29 Implémentation chaînée (1) Les éléments ne sont pas rangés les uns à coté des autres. On a besoin de connaître, pour chaque élément, la position (l'adresse) de l'élément qui le suit. La position n'a donc plus rien à voir avec le rang, mais c'est l'adresse d'une structure qui contient l'élément ainsi que la position du suivant . DUT MCW1 30 Implémentation chaînée (2) type cellule = { valeur : élément; suivant : adresse d'une cellule } type liste = adresse d'une cellule Une adresse s'appelle aussi un pointeur. On note l'adresse d'un objet par &objet. On note l'objet qui est à l'adresse ad par *ad. L'adresse nulle sera notée VIDE. DUT MCW1 31 Implémentation chaînée (3) Algo debut (liste L) : retourne une position début // adresse de la 1ere cellule retourner L; fin Algo creer_liste_vide () : retourne une liste début retourner VIDE; fin Algorithme est_vide (liste L) : retourne un booléen début retourner L=VIDE; fin DUT MCW1 32 Implémentation chaînée (4) Algo suivante (position p,liste L) : retourne une position debut retourner (*p).suivant Fin Algo acces (position p,liste L) : retourne un element debut retourner (*p).valeur fin Algo longueur (liste L) : retourne un entier locales :position p, entier l; debut l 0; p L; tant que p<>VIDE faire p (*p).suivant; l l+1; fintantque retourner l; fin DUT MCW1 33 Implémentation chaînée (5) Algo inserer (element e, rang k,E/S liste L) locales : position p, nouv : adresse d'une cellule, entier(rang) i; debut //allouer de la memoire pour une cellule et y mettre e dans valeur nouv creer_cellule(e) si k=1 alors // cas particulier : insertion en tete (*nouv).suiv L L nouv ; // ATTENTION : L est modifie ! sinon // se positionner sur le k-1 ieme élément ATTENTION : s'il existe! p L i 1 tant que p<>VIDE et i<k-1 faire p (*p).suivant i i+1 fintantque si p=VIDE alors erreur "la liste ne comporte pas assez d'éléments" sinon (*nouv).suivant (*p).suivant (*p).suivant nouv finsi finsi fin DUT MCW1 34 Implémentation chaînée : Remarques on est obligé de traiter le cas particulier d'insertion en tête mais l'algorithme marche bien aussi si on insère en queue. Dans le cas d'insertion en tête, L est modifiée. Il faut préciser dans l'écriture de l'algorithme que le paramètre L est en entrée et sortie. On peut écrire par exemple : Algo inserer(element e,rang k, E/S liste L) Pour cette raison, on pourra préférer retourner la nouvelle liste, et donc déclarer l'algorithme : Algo inserer(element e, rang k, liste L) : retourne une liste et terminer l'algo par retourner L; DUT MCW1 35 Liste chaînée avec cellule de tête (1) Lors d'une insertion ou d'une suppression, il faut traiter à part le cas de la première cellule. Pour éviter ce traitement à part, on peut mettre une cellule bidon en tête de liste. L n'est plus la position de la première cellule, mais celle d'une cellule bidon qui est toujours là. En particulier, une liste vide (donc lors de la création !) est une liste contenant cette seule cellule bidon. DUT MCW1 36 Liste chaînée avec cellule de tête (2) algorithme creer_liste_vide() : retourne une liste locales adresse de cellule : bidon début bidon <- creer_cellule(n_importe_quoi); //si ce n'est pas déjà fait dans creer_cellule bidon.suivant <- VIDE; retourner bidon; fin algorithme est_vide (liste L) : retourne un booleen début si (*L).suivant = VIDE alors retourner VRAI; sinon retourner FAUX; fin DUT MCW1 37 Liste chaînée avec cellule de tête (3) Et donc l'insertion ne demande plus de cas particulier en tête : Algo inserer (element e, rang k,E/S liste L) locales : position p; ad d'une cellule nouv; entier(rang) i; debut /* allouer de la mémoire et y mettre e dans le chp valeur*/ nouv creer_cellule(e); // se positionner sur le k-1 ième élément p L; i 1; tant que p<>VIDE et i<k-1 faire p <- (*p).suivant; i<-i+1; fintantque // (si k=1 on n'a pas bouge !) si p=VIDE alors erreur "la liste ne comporte pas assez d'éléments"; sinon (*nouv).suivant (*p).suivant; (*p).suivant nouv; finsi fin DUT MCW1 38 Listes doublement chaînées (1) Certaines actions peuvent demander de parcourir la liste à l'envers. Si c'est une action souvent demandée, il peut être avantageux de rajouter un pointeur vers l'élément précédent dans la structure de données. DUT MCW1 39 Listes doublement chaînées (2) type cellule = { valeur : element; suivant : adresse d'une cellule; precedent : adresse d'une cellule; } type liste = adresse d'une cellule; Il faut bien sûr penser à le mettre à jour comme il faut dans toutes les primitives ! DUT MCW1 40 Listes chaînées circulaires Et on peut bien sûr faire des listes circulaires doublement chaînées. DUT MCW1 41 Structure de données : pile DUT MCW1 42 Structure de données : pile Une pile est une liste linéaire particulière : on ne peut accéder (= consulter, ajouter, supprimer) qu'au dernier élément, que l'on appelle le sommet de la pile. Dans une pile, il est impossible d'accéder à un élément au milieu ! Les piles sont aussi appelées structures LIFO pour Last In First Out : dernier-entré-premiersorti. C'est une structure très utilisée en informatique. Par exemple, les appels de fonctions d'un programme utilisent une pile pour sauvegarder toutes les informations (variables, adresse de l'instruction de retour, etc. ). DUT MCW1 43 Primitives (1) pile créer_pile() : création d'une pile vide pile créer_pile (entier taille_max ) : creation d'une pile vide d'au maximum taille_max éléments empiler (element, pile) : met l'élément en sommet de pile, erreur si la taille est limitée et la pile pleine dépiler (pile) : enlève de la pile l'élément en sommet de pile, erreur si la pile est vide DUT MCW1 44 Primitives (2) élément dépiler (pile) : enlève l'élément en sommet de pile et le retourne, erreur si la pile est vide élément sommet (pile) : retourne l'élément en sommet de pile, erreur si la pile est vide booléen pile_est_vide (pile) : teste si la pile est vide booléen pile_est_pleine (pile) : teste si la pile est pleine (seulement dans le cas d'une taille limitée) DUT MCW1 45 Implémentation d'une pile par un tableau (1) type pile = { tab : tableau d'éléments; s : entier; // indice du sommet } Algorithme créerPile() : retourne une pile locales : pile P; début P.s 0; // alloc du tableau si nécessaire retourner P; fin DUT MCW1 46 Implémentation d'une pile par un tableau (2) Algorithme empiler (élément e, E/S pile p) début p.s p.s+1; //si la pile n'est pas pleine ! p.tab[p.s] e; fin Algorithme sommet (pile P): retourne un element début si P.s <> 0 alors retourner P.tab[P.s]; sinon erreur "on ne peut pas acceder au sommet d'une pile vide !!« ; finsi fin DUT MCW1 47 Implémentation d'une pile par un tableau (3) Le problème est le problème des tableaux, c'est à dire qu'on est limité par la taille. L'avantage des tableaux sur les listes chaînées c'est l'accès direct à un élément, or ça ne nous intéresse pas dans les piles. Donc c'est mieux d'implémenter par une liste chaînée. DUT MCW1 48 Implémentation d'une pile par liste chaînée (1) Exemple : une liste chaînée avec lien vers le premier (pour ne pas perdre la pile) et lien vers le sommet : DUT MCW1 49 Implémentation d'une pile par liste chaînée (2) type cellule = { valeur : element; suivant : adresse d'une cellule; } type pile = { prem : adresse d'une cellule; sommet : adresse d'une cellule; } DUT MCW1 50 Implémentation d'une pile par liste chaînée (3) Algorithme empiler (element e, E/S pile p) locales nouv : adresse d'une cellule; début //allocation et initialisation des champs nouv = creer_cellule(); (*nouv).valeur e; (*nouv).suivant VIDE; // faire les liens si p.sommet <> VIDE alors // la pile pas vide (*(p.sommet)).suivant nouv; p.sommet nouv; sinon // la pile est vide p.sommet nouv; p.prem nouv; finsi fin DUT MCW1 51 Implémentation d'une pile par liste chaînée (4) Ou bien aussi, puisqu'on fait des ajouts en tête, on peut se passer du lien vers le premier DUT MCW1 52 Implémentation d'une pile par liste chaînée (5) type cellule = { valeur : élément; suivant : adresse d'une cellule; // le PRECEDENT en fait ... } type pile = adresse d'une cellule; // le sommet DUT MCW1 53 Implémentation d'une pile par liste chaînée (6) Algo empiler (élément e, E/S pile p) // attention p est modifie !! locales nouv : adresse d'une cellule; début /* allouer de la mémoire et initialiser les champs de la nouvelle cellule */ nouv=creer_cellule(); (*nouv).valeur e; // faire les liens : ajout en tête (*nouv).suivant p; p nouv; fin DUT MCW1 54 exemple d'utilisation d'une pile : Expressions arithmétiques postfixées et infixées Les expressions arithmétiques sont habituellement écrites de manière infixe, c'est à dire l'opérateur entre ses deux opérandes (ou avant si c'est un opérateur unaire). On trouve aussi une notation postfixée, que l'on appelle également notation polonaise : l'opérateur est placé après ses opérandes. Par exemple : 13 + 8 s'écrit en postfixe : 13 8 + (3+5) x 9 s'écrit en prefixe : x + 3 5 9 L'intérêt de cette notation est qu'il n'y a plus besoin de connaître les priorités des opérateurs, et donc plus besoin de parenthèses (qui servent à contrer les priorités). DUT MCW1 55 Structure de donnée : file Une File est une liste linéaire particulière: on ne peut ajouter qu'en queue, consulter qu'en tête, et supprimer qu'en tête. Comme pour une file d'attente ... ! Les files sont aussi appelées structures FIFO pour First In First Out, cad premier-entrépremier-sorti DUT MCW1 56 File : Primitives file créer_file () : création d'une file vide file créer_file (entier taille_max ) : creation d'une file vide d'au maximum taille_max éléments enfiler (element, file) : met l'élément à la fin de la file, erreur si la taille est limitées et la pile pleine défiler (file) : enlève le premier élément de la file, erreur si la file est vide élément consulter (file) : retourne le premier élément de la file , erreur si la file est vide booléen file_est_vide (file) : teste si la file est vide booléen file_est_pleine (file) : teste si la file est pleine (seulement dans le cas d'une taille limitée) DUT MCW1 57 File : Implémentations par un tableau type file = { t : tableau[1..MAX] d'elements; deb : entier; fin : entier; } enfiler (element e, E/S file F) debut si F.fin < MAX alors F.fin F.fin +1; F.t[F.fin] e; sinon file pleine ???; finsi fin defiler (E/S file F) debut si F.debut <= F.fin MAX alors F.debut F.debut +1; sinon erreur "file vide« ; finsi fin DUT MCW1 58 File : Implémentation par un tableau circulaire DUT MCW1 59 File : Implémentation par un tableau circulaire enfiler (element e, E/S file F) debut si F.fin < MAX alors F.fin F.fin +1; sinon F.fin 1; finsi F.t[F.fin] e; fin Ou encore : F.fin ((F.fin+1) modulo (MAX+1) ) -1 DUT MCW1 60 File : Implémentation par un tableau circulaire Problème : comment savoir quand la file est pleine ? deux cas : Elle est pleine quand fin=deb-1 ou deb=1 et fin=MAX quand est-elle vide ? (elle devient vide après avoir eu un seul élément qu'on a défilé) Elle est vide quand fin=deb-1 ou deb=1 et fin=MAX DUT MCW1 61 File : Implémentation par un tableau circulaire Comment différencier les deux ? Idée 1 : rajouter un booléen vide dans la structure, initialisé à VRAI lors de la création, qui devient FAUX dès qu'on enfile, et redevient VRAI si l'on défile lorsque deb=fin. Idée 2 : et si on faisait une liste chaînée ? On ne sera plus dérangé avec des limitations de taille ! DUT MCW1 62 Implémentation d'une file par une liste chaînée type file { debut : adresse d'une cellule;//la première fin : adresse d'une cellule; //la dernière } DUT MCW1 63 Arbre binaire Définitions Primitives Primitives de construction/modifications Parcours Arbres binaires particuliers Implémentations Tableaux FG et FD Listes chaînées DUT MCW1 64 Arbre binaire La structure d'arbre est l'une des plus importantes et des plus spécifiques de l'informatique. Par exemple : organisation des fichiers dans les systèmes d'exploitation, représentation des programmes traités par un ordinateur, d'une table des matières, d'un questionnaire, d'un arbre généalogique... C'est une structure de données qui permet d'écrire des algorithmes très performants. Nous verrons tout d'abord les arbres binaires, qui sont un cas particulier des arbres généraux. Une propriété intrinsèque de cette structure est la récursivité. DUT MCW1 65 Définitions Soit un ensemble de sommets (ou encore noeuds) auxquels sont associés des "valeurs" (les éléments à stocker : entier, chaîne, structure, ...). Un arbre binaire est défini récursivement de la manière suivante : un arbre binaire est composé : soit d'un seul sommet appelé racine, soit d'un sommet racine à la gauche duquel est accroché un sous-arbre binaire gauche soit d'un sommet racine à la droite duquel est accroché un sous-arbre binaire droit soit d'un sommet racine auquel sont accrochés un sous-arbre binaire droit et un sous-arbre binaire gauche. DUT MCW1 66 Terminologie fils gauche de x = le sommet (s'il existe) accroché à la gauche de x fils droit de x = le sommet (s'il existe) accroché à la droite de x fils de x = le ou les deux sommets accrochés sous x père de x = le sommet p tel que x est fils de p frère de x = un sommet (s'il existe) qui a le même père sommet interne = un sommet qui a au moins un fils ( gauche ou droit ou les deux) DUT MCW1 67 Terminologie feuille = un sommet qui n'a pas de fils branche = un chemin de fils en fils de la racine vers une feuille branche gauche = la branche de fils gauche en fils gauche branche droite = la branche de fils droit en fils droit hauteur d'un sommet x = la longueur (en nb d'arcs) du plus long chemin de x à une feuille hauteur d'un arbre = la hauteur de la racine profondeur d'un sommet x = la longueur (en nb d'arcs) du chemin de la racine au sommet Remarque : la racine de l'arbre n'a pas de père et c'est le seul sommet comme ça. DUT MCW1 68 Primitives sommet racine (arbre B) : retourne la racine de l'arbre sommet fils_gauche (sommet x,arbre B) : retourne le sommet fils gauche de x ou VIDE sommet fils_droit (sommet x,arbre B) : retourne le sommet fils droit de x ou VIDE sommet pere (sommet x,arbre B) : retourne le sommet père de x ou VIDE element val (sommet x,arbre B) : retourne l'élément stocké au sommet x booleen est_vide (arbre B) : retourne vrai ou faux DUT MCW1 69 Primitives de construction/modifications arbre créer_arbre_vide () : création d'un arbre initialisé comme il faut sommet créer_sommet(element e, arbre B) : retourne le sommet faire_FG(sommet père,sommet fg,arbre B) faire_FD(sommet père,sommet fd,arbre B) faire_racine (sommet rac, arbre B) supprimer_feuille (sommet f, arbre B) ... DUT MCW1 70 Exemple de manipulation d'un arbre binaire : arbre B sommet s1,s2; B creer_arbre_vide(); s1 creer_sommet("chose",B); faire_racine(s1,B); s2 creer_sommet("Autre chose",B); faire_FG(s1,s2,B); faire_FD(s2,creer_sommet("chose3",B),B); Peindre_en_Rouge(B); ... DUT MCW1 71 Parcours But : passer en revue (pour un traitement quelconque) chaque sommet une et une seule fois. //parcours de tous les sommets de l'arbre B Algorithme Parcours (arbre B) début Parcours_Aux(racine(B),B); Fin // parcours du sous-arbre de racine r Algorithme Parcours_Aux (sommet r,arbre B) début si r<>VIDE alors Parcours_Aux(fils_gauche(r,B),B); Parcours_Aux(fils_droit(r,B),B); finsi fin DUT MCW1 72 Arbres binaires particuliers On appelle arbre binaire complet un arbre binaire tel que chaque sommet possède 0 ou 2 fils. Un arbre binaire complet possède 2P+1 sommets (nombre impair) dont P sommets internes et P+1 feuilles On appelle arbre binaire parfait un arbre binaire (complet) tel que chaque sommet soit père de deux sous arbres de même hauteur Un arbre binaire parfait possède 2h+1-1 sommets, où h est la hauteur de l'arbre. On appelle arbre binaire quasi-parfait un arbre binaire parfait éventuellement grignoté d'un étage en bas à droite. DUT MCW1 73 Arbre binaire sous forme de tableaux FG et FD type sommet = entier type arbre = adresse de { nbSommets : entier; FG : tableau [1..MAX] de sommets; FD : tableau [1..MAX] de sommets; VAL : tableau [1..MAX] d'éléments; } DUT MCW1 74 Exemples de primitives : Algorithme fils_gauche(sommet x,arbre B):retourne un sommet debut retourner (*B).FG[x]; fin Algorithme pere (sommet x,arbre B):retourne un sommet locales entier p; debut p 1; tant que (p <= BnbSommets) et (BFG[p] != x) et (BFD[p] != x) faire p p+1; fintantque si (p<= BnbSommets) retourner p; sinon retourner VIDE; finsi fin DUT MCW1 75 Exemples de primitives : Algorithme racine (arbre B) : retourne un sommet /* la racine est le seul sommet qui n'est fils de personne*/ locales : entier i; VU : tableau de booleen /*marquage des sommets(vu ou pas)*/ debut // allouer tableau de marquage VU= allouer_memoire (B->nbSommets * booleens); // init de ce tableau pour i1 a (B->nbSommets) faire VU[i] faux; // aucun sommet n'a ete rencontre finpour // marquage des sommets rencontres dans FG ou FD pour i<-1 a (B->nbSommets) faire VU[B->FG[i]] vrai; VU[B->FD[i]] vrai; finpour // chercher celui qui n'est pas marque (qui existe forcemment) i 1; tant que VU[i] faire i i+1; fin tant que retourner i; fin DUT MCW1 76 Amélioration Pour réduire la complexité de cette implémentation, on peut rajouter dans la structure une place pour stocker la racine : type sommet = entier; type arbre = adresse de { racine : sommet; nbSommets : entier FG : tableau [1..MAX] de sommets; FD : tableau [1..MAX] de sommets; VAL : tableau [1..MAX] d'éléments; } DUT MCW1 77 Primitives améliorées // l'arbre est modifie, on precise donc bien E/S !! Algorithme fait_racine (sommet r, E/S arbre B) debut (*B).racine r; Fin Algorithme creer_vide () : retourne un arbre locales : arbre B; entier i; debut B allouer_memoire(necessaire); (*B).nbSommets 0; (*B).racine VIDE; retourner B; Fin Algorithme fait_FG (sommet p,sommet fg, E/S arbre B) debut (*B).FG[p] <- fg; fin DUT MCW1 78 Primitives améliorées Algorithme creer_sommet(E/S arbre B,element e): retourne un sommet /* ajout du sommet sans liens retourne le sommet créé pour ensuite pouvoir faire les liens */ debut (*B).nbSommets (*B).nbSommets +1; (*B).VAL[(*B).nbSommets] creer_element(e); //allouer memoire pour un element si necessaire (*B).FG[(*B).nbSommets] VIDE; (*B).FD[(*B).nbSommets] VIDE; //(*B).PERE[(*B).nbSommets] VIDE si besoin retourner (*B).nbSommets; fin DUT MCW1 79 suppression Si on effectue des suppressions de sommets, il va falloir faire des décalages pour éviter les trous dans le tableau. Pas simple car il ne faut pas seulement décaler, il faut mettre à jour les bons numéros !!! Il faut cependant modifier creer_sommet pour trouver la première place libre dans le tableau (qui n'est pas forcemment nbSommets). Sinon on va consommer beaucoup de place mémoire qu'on n'utilisera pas. En outre, qu'est ce qu'une "place vide" ?? Il faut un élément spécial element_vide, ce qui n'est pas toujours possible suivant la nature des éléments (ex : qu'est ce qu'un nombre vide ? 0 ? Non, 0 peut être un de ces nombres ! ). DUT MCW1 80 suppression Algorithme supprime_feuille (sommet f, E/S arbre B) // f DOIT être une FEUILLE ! locale : sommet p début // supprimer le lien du pere p pere(f,B) si ((*B).FG[p] = f) alors (*B).FG[p] VIDE ; sinon (*B).FD[p] VIDE ; finsi /* "supprimer" la feuille = ne plus la compter dans l'arbre et noter la place comme libre */ (*B).VAL[f] ELEMENT_VIDE ; (*B).nbSommets (*B).nbSommets -1 ; fin DUT MCW1 81 Arbre binaire sous forme de listes chaînées DUT MCW1 82 Arbre binaire sous forme de listes chaînées type sommet = adresse de { FG : adresse d'un sommet; FD : adresse d'un sommet; val : élément; } type arbre = sommet; alias VIDE=adresse NULL; Un arbre est donc donné par l'adresse de sa racine. Un sommet ou un arbre c'est la même chose. DUT MCW1 83 primitives Algorithme racine(arbre B) : retourne un sommet retourner B; fin Algorithme fils_gauche(sommet x,arbre B) : retourne un sommet retourner (*x).FG; fin Algorithme valeur(sommet x,arbre B) : retourne un element retourner (*x).val; fin Algorithme est_vide(arbre B) : retourne un booleen retourner (B=VIDE); fin Algorithme est_feuille(sommet f,arbre B) : retourne un booleen retourner ( (*f).FG=VIDE et (*f).FD=VIDE); fin DUT MCW1 84 primitives Algorithme pere(sommet x,arbre B) : retourne un sommet // recherche RECURSIVE du pere de x dans le sous arbre (de racine) B // rappel : arbre=sommet dans cette implémentation locales sommets : tmp,p; debut si (est_vide(B)) alors retourner VIDE sinon p B // racine de B en fait si (p=x) alors retourner VIDE //cas particulier pere de la racine sinon si ((*p).FG=x) ou ( (*p).FD=x) alors retourner p sinon tmp <- pere(x, (*p).FG) // recherche recursive dans le sous arbre gauche si (tmp=VIDE) alors // s'il n'est pas a gauche, c'est qu'il est a droite tmp <- pere(x, (*p).FD) finsi retourner tmp finsi finsi finsi fin DUT MCW1 85 Primitives améliorées Pour réduire la complexité, il est préférable de rajouter un pointeur vers le père dans la structure type sommet = adresse de { FG : adresse d'un sommet; FD : adresse d'un sommet; PERE : adresse d'un sommet; val : élément; } type arbre = sommet; DUT MCW1 86 Primitives améliorées Algorithme fait_racine (sommet r,E/S arbre B) // attention je rappelle que B est modifié B r; fin Algorithme creer_vide () : retourne un arbre retourner VIDE; fin Algorithme fait_FG (sommet p,sommet fg,arbre B) /*avec cette implémentation B en fait n'est pas modifie */ Debut (*p).FG fg; // (*fg).pere p fin DUT MCW1 87 Primitives améliorées Algorithme creer_sommet(element e,arbre B) :retourne un sommet //avec cette implémentation B en fait n'est pas modifie locales sommet nouv; début nouv= allouer_memoire(un sommet); (*nouv).FG VIDE; (*nouv).FD VIDE; // (*nouv).pere VIDE; // alloc mémoire pour l'élément si besoin (*nouv).val creer_element(e); retourner nouv; fin DUT MCW1 88 Primitives améliorées Algorithme supprime_feuille (sommet f,E/S arbre B) // larbre peut devenir vide donc etre modifie !!!! locales : sommet p; début si (f=B) alors // la feuille = la racine de l'arbre // ==> l'arbre ne contenait que cette feuille Liberer_memoire(f); B VIDE sinon p pere(f); // p (*f).pere ? si (*p).FG=f alors // feuille a gauche (*p).FG VIDE; sinon // feuille a droite (*p).FD VIDE; finsi liberer-_memoire(f); finsi fin DUT MCW1 89 Arbre binaire de Recherche Définitions Recherche Ajout Suppression Arbres binaires de recherche équilibrés Définitions Rotations Arbres DUT MCW1 AVL 90 Définition Un arbre binaire de recherche est un arbre binaire tel que la valeur stockée dans chaque sommet est: supérieure à celles stockées dans son sous-arbre gauche inferieure à celles de son sous-arbre droit. Remarque : les éléments stockés dans l'arbre peuvent être compliqués. Quand on parle de valeur de l'élément il faut alors comprendre valeur de la clé de l'élément. DUT MCW1 91 Recherche Algorithme Recherche (élément e, arbre B) : retourne un booléen debut retourner RechercheAux(e,racine(B),B); fin DUT MCW1 92 Recherche Algorithme RechercheAux (element e, sommet r, arbre B) :retourne un booleen /*vérifie si e appartient au sous-arbre de racine r (qui peut être vide)*/ debut si r=VIDE Alors // l'element n'est pas dans l'arbre retourner FAUX; sinon si elements_egaux(val(r,B),e) alors // on l'a trouve retourner VRAI; sinon si elements_inferieurs(e,val(r,B)) alors // s'il existe, c'est dans le sous-arbre gauche retourner RechercheAux(e,fils_gauche(r,B),B) ; sinon // s'il existe, c'est dans le sous-arbre droit retourner RechercheAux(e,fils_droit(r,B),B); Finsi Finsi Finsi fin DUT MCW1 93 Ajout Algorithme Ajout (element e,E/S arbre B) // ajoute la element e dans l'arbre // l'arbre peut etre vide ! // l'arbre est modifie locales : sommets r,y début si est_vide(B) Alors // l'arbre est vide !!! y creer_sommet(e,B); fait_racine(y,B); sinon r=racine(B); si elements_inferieurs(e,val(r,B)) alors Ajout_aux(e,fils_gauche(r,B),r,gauche,B); sinon Ajout_aux(e,fils_droit(r,B),r,droit,B); finsi finsi fin DUT MCW1 94 Ajout Algorithme Ajout_aux (element e,sommet r, sommet pere, entier lien,E/S arbre B) // ajout dans le sous arbre de racine <r> qui est fils de <pere> // <lien> egal droit ou gauche suivant que <r> est fils gauche ou droit de son pere locale : sommet nouv; debut si r=VIDE alors // c'est ici qu'il faut l'ajouter nouv creer_sommet(e,B); si lien=droit alors faire_FD(pere,nouv,B); sinon faire_FG(pere,nouv,B); finsi sinon si elements_inferieurs(e,val(r,B)) alors Ajout_aux(e,fils_gauche(r,B),r,gauche,B); sinon Ajout_aux(e,fils_droit(r,B),r,droit,B); finsi finsi fin DUT MCW1 95 Ajout On ajoute le sommet en tant que feuille. On pourrait aussi ajouter en tant que racine : couper l'arbre en deux sous-arbres (binaires de recherche) G et D, G contenant tous les éléments de valeur plus petite que celle à ajouter, et D contenant tous les éléments de valeur plus grande que celle à ajouter, créer un nouveau sommet de valeur voulue en lui mettant G comme sous-arbre gauche et D comme sous-arbre droit. DUT MCW1 96 Ajout Exemple : ajout à la racine du sommet 12 dans un abr DUT MCW1 97 Suppression La suppression commence par la recherche de l'élément. Puis : si c'est une feuille, on la vire sans problèmes si c'est un sommet qui n'a qu'un fils, on le remplace par ce fils si c'est un sommet qui a deux fils, on a deux solutions : - le remplacer par le sommet de plus grande valeur dans le sous-arbre gauche - le remplacer par le sommet de plus petite valeur dans le sous-arbre droit puis supprimer (récursivement) ce sommet ! DUT MCW1 98 Arbres binaires de recherche équilibrés La complexité de tous ces algorithmes est majorée par la hauteur de l'arbre. On a donc intérêt à avoir des arbres les moins hauts possible. Ainsi, pour améliorer ces algos, on pourrait donc faire un rééquilibrage de temps à autres, c'est à dire : récupérer tous les éléments par un parcours symétrique, puis reconstruire un arbre quasi-parfait à partir de ces éléments (tout en gardant son critère d'arbre binaire de recherche). DUT MCW1 99 Arbres binaires de recherche équilibrés DUT MCW1 100 Arbres binaires de recherche équilibrés Mais il y a encore mieux : on peut chercher à avoir tout le temps un arbre de moindre hauteur, c'est à dire un arbre tel que les hauteurs des sous-arbres de chaque sommet diffèrent d'au plus 1 et donc rééquilibrer comme il faut après chaque ajout ou suppression. On pourrait appeler un tel arbre arbre binaire de recherche quasi-quasi-parfait, mais il a déjà un nom : arbre AVL, du nom de leurs inventeurs Adelson, Velskii et Landis. DUT MCW1 101 Arbres binaires de recherche équilibrés :Définitions Définition 1. Un arbre binaire est dit Héquilibré si en tout sommet de l'arbre les hauteurs des sous-arbres gauche et droit diffèrent au plus de 1. Définition 2. Un arbre AVL est un arbre binaire de recherche H-équilibré Soit la fonction de déséquilibre d'un sommet deseq(sommet) = hauteur(sous-arbre gauche)hauteur(sous-arbre droit) Un arbre est H-équilibré si et seulement si les déséquilibres sont égaux à 0, 1 ou DUT MCW1 102 Arbres binaires de recherche équilibrés :Définitions Exemple : l'arbre suivant n'est pas Héquilibré car il y a un déséquilibre de -2. DUT MCW1 103 Rotations Après chaque ajout ou suppression, il faut regarder si cela a entraîné un déséquilibre et si c'est le cas on rééquilibre par des rotations. Rotation droite DUT MCW1 104 Rotations Algorithme rd (sommet b, E/S arbre B) // rotation droite du sous arbre de racine b locales : sommets a,v,pb; coté = droit/gauche; debut // garder infos pour raccrocher correctement le sous arbre dans B pb pere(b,B); si pb<>VIDE alors si b=fils_gauche(pb,B) alors coté gauche; sinon coté droit finsi; finsi // === la rotation du sous arbre a fils_gauche(b,B); v fils_droit(a,B); fait_FG(b,v,B); fait_FD(a,b,B); DUT MCW1 // maintenant on raccroche dans l'arbre si pb<>VIDE alors si coté=gauche alors fait_FG(pb,a,B) sinon fait_FD(pb,a,B); finsi sinon fait_racine(a,B); finsi fin 105 Rotation gauche DUT MCW1 106 Rotation gauche droite DUT MCW1 107 Rotation droite gauche DUT MCW1 108 Arbres A.V.L. Après un ajout dans un AVL, soit il n'y a rien à faire (l'arbre est encore H-équilibré), soit on fait des rotations qui rééquilibrent l'arbre, et ce qui est très fort, c'est qu'une seule rotation suffit !!! Théorème Si B est un AVL et si on ajoute un sommet à B, alors si cela ne provoque aucun déséquilibre, on a toujours un arbre Héquilibré on est content sinon soit x le sommet le plus bas déséquilibré (ie de déséquilibre 2 ou -2) : si x a un déséquilibre de 2 et est tel que son fils gauche a un déséquilibre de 1, alors une rotation rd(x,B) permet de rééquilibrer l'arbre a un déséquilibre de -1, alors une rotation rgd(x,B) permet de rééquilibrer l'arbre si x a un déséquilibre de -2 et est tel que son fils droit a un déséquilibre de -1, alors une rotation rg(x,B) permet de rééquilibrer l'arbre a un déséquilibre de 1, alors une rotation rdg(x,B) permet de rééquilibrer l'arbre pas d'autres cas possible andouille DUT MCW1 109