Structures de données Dr. Youssef Bou Issa [email protected] http://youssef-bouissa.fr/ Algorithmes • Un algorithme est une suite finie d’instructions – pouvant être exécutées de façon automatique. • Un algorithme prend en entrée des données et produit un résultat. Algorithmes • On peut dire qu’une recette de cuisine est un algorithme – l’entrée étant les ingrédients – et la sortie le plat cuisiné. Algorithmes • On peut distinguer trois façons de décrire un algorithme. – Une première méthode consiste à en donner le principe. – Une deuxième méthode consiste à utiliser du pseudo-code ; – Une troisième méthode consiste à traduire l’algorithme dans un langage de programmation. • Précision des 3 façons Caractéristiques d’un algorithme • Sa simplicité : – un algorithme simple est facile à comprendre, à implémenter et généralement à prouver. • La qualité du résultat obtenu : – par exemple, une recette pour faire une mousse au chocolat peut donner un résultat plus ou moins agréable à déguster. Caractéristiques d’un algorithme • Le temps pris pour effectuer l’algorithme : – Il n’est pas matériellement possible de résoudre un problème à l’aide d’un algorithme nécessitant quelques milliards d’années pour s’exécuter. – On peut aussi préférer un algorithme qui s’exécutera en une seconde plutôt qu’en une heure. Caractéristiques d’un algorithme • La place prise en mémoire : – cette place va dépendre à la fois de l’algorithme et des structures de données retenues ; – il ne faut pas dépasser les capacités de la mémoire de la machine sur laquelle l’algorithme s’exécutera. Caractéristiques d’un algorithme • le fait de déclarer un tableau exige de réquisitionner des emplacements mémoires – inutilisables par d'autres programmes comme votre système d'exploitation. – Tester l'égalité entre deux variables va nécessiter de réquisitionner temporairement le processeur Caractéristiques d’un algorithme • parcourir un tableau de 10 000 cases • en testant chacune des cases exigera donc de réquisitionner au moins 10 000 emplacements mémoires • d'interrompre 10 000 fois le processeur pour faire nos tests. • Tout cela ralentit les performances de l'ordinateur et saturer la mémoire • D'où l'intérêt de pouvoir comparer la complexité de différents algorithmes – pour ne conserver que les plus efficaces, – ou de prédire cette complexité. Notion de complexité • étant donné un algorithme, nous appelons opérations élémentaires : – un accès en mémoire pour lire ou écrire la valeur d’une variable ou d’une case d’un tableau ; – une opération arithmétique entre entiers ou entre réels : addition, soustraction, – multiplication, division, calcul du reste dans une division entière ; – une comparaison entre deux entiers ou deux réels. Notion de complexité (ordre de grandeur) • On considère f(n)>0, g(n)>0 On dit que f(n) a un ordre de grandeur au plus égal à celui de g(n) S’il existe un entier k tel que : f(n)<=k.g(n) on écrira f = O(g) (notation de Landau). Exemple : f(n) = n2 g(n) = 3 n2 + 5 n + 4 ont même ordre de grandeur (voir schéma) Notion de complexité (ordre de grandeur) • Exemple f(n) : l'algorithme qui lit un tableau et teste chacune de ses cases : – si le tableau a 100 cases, l'algorithme effectuera 100 tests ; – si le tableau à 5000 cases, l'algorithme effectuera 5000 tests ; – si le tableau a n cases, l'algorithme effectuera n tests ! On dit que sa complexité est en O(n). Notion de complexité (ordre de grandeur) • un autre exemple g(n): un algorithme qui parcourt lui aussi un tableau à n éléments. • Pour chaque case, il effectue deux tests : – le nombre est-il positif ? – Le nombre est-il pair ? • Combien va-t-il effectuer de tests ? – deux fois plus que le premier algorithme, c'est à dire 2*n. f(n)<=2g(n) – l'algorithme f a toujours une complexité proportionnelle à g et donc ils sont tous les deux en O(n). Notion de complexité (Ordre de grandeur) • Est-ce que tous les algorithmes ont une complexité en O(n)? NON • Un facteur 2, 3, 20,… – peut être considéré comme négligeable. – tout facteur constant peut être considéré comme négligeable. Notion de complexité (Ordre de grandeur) • Reprenons encore notre algorithme : il parcourt toujours un tableau à n éléments, – mais à chaque case du tableau, il reparcourt tout le tableau depuis le début – pour savoir s'il n'y aurait pas une autre case ayant la même valeur. • Combien va-t-il faire de tests ? – Pour chaque case, il doit faire n tests – Et comme il y a n cases, il devra faire en tout n*n tests • Le facteur n'est cette fois pas constant • cet algorithme a une complexité en O(n2) Notion de complexité (Ordre de grandeur) Légende de gauche à droite: Complexité en O(en) (e : exponentielle) Complexité en O(n2) Complexité en O(n*log(n)) Complexité en O(n) Complexité en O(log(n)) Notion de récursivité • Ecrire en C la fonction itérative qui calcule la factorielle d’un nombre • Considérons la fonction factorielle (!). • Elle est définie par : – 0! = 1 – n! = 1 x 2 x … x (n – 1) x n si n > 0 Notion de récursivité • Si l’on remarque que le produit 1 x 2 x … x (n – 1) est égal à (n – 1)! • On peut définir la factorielle de la façon suivante : • n! = 1, si n = 0 • n! = (n – 1)! x n, si n > 0 • Une telle définition est dite récursive car la factorielle est appelée dans sa propre définition. Notion de récursivité • Considérons alors l’exemple : 3! = (2!) x 3 = ((1!) x 2) x3 = (((0!) x 1) x 2) x 3 = ((1 x 1) x 2) x 3 =6 • Une fonction récursive est formée d’une relation de récurrence et d’un ou plusieurs cas de base. Par exemple, dans la définition récursive de la factorielle : – la relation de récurrence est « n! = (n – 1)! X n, si n > 0 » ; – le cas de base est « 0! = 1 ». Définition d’une fonction récursive • Démarche pour définir une fonction récursive: 1-établir la relation de récurrence ; 2-établir les cas de base ; 3-vérifier que l’application de la relation de récurrence convergera vers l’un des cas de base ; 4- écrire en C la définition de la fonction qui – devra tester si l’un des cas de base a été atteint, – et relancer l’application de la relation de récurrence, si ce n’est pas le cas. Définition d’une fonction récursive • Appliquons cette démarche à la définition de la fonction fac,qui, appliquée à un nombre entier n (n ≥ 0) calcule la factorielle de n. 1. Etablir la relation de récurrence. fac(n) = n x fac(n – 1), si n > 0 2. Etablir les cas de base. fac(n) = 1, si n = 0 Définition d’une fonction récursive 3. Vérifier que l’application de la relation de récurrence convergera vers l’un des cas de base : – A chaque appel de la fonction fac on soustrait 1 à n qui deviendra donc égal à 0 au (n + 1)e appel, – ce qui arrêtera la récursion puisque le cas de base sera atteint, – pourvu que la condition n <= 0 ait été respectée lors de l’appel initial. Définition d’une fonction récursive 4. Ecrire en C la définition de la fonction qui devra tester si l’un des cas de base a été atteint, et relancer l’application de la relation de récurrence, si ce n’est pas le cas. Int fac(int n) { if (n <= 0) return 1; else return fac(n - 1) * n; } • Quel est la complexité de cet algorithme? Définition d’une fonction récursive (2) • Ecrire la fonction itérative somme qui calcule la somme des entiers compris entre i et j. • Appliquons maintenant notre démarche de récursivité à la définition de cette fonction Définition d’une fonction récursive (2) 1. Etablir la relation de récurrence. La somme des entiers compris entre i et j (i < j) est égale à i plus la somme des entiers compris entre i + 1 et j : somme(i, j) = i + somme(i + 1, j), si i < j 2. Etablir les cas de base. La somme des entiers compris entre i et i est égale à i : somme(i, j) = i, si i = j Définition d’une fonction récursive (2) 3. Vérifier que l’application de la relation de récurrence convergera vers l’un des cas de base. A chaque appel de la fonction somme on ajoute 1 à i qui deviendra égal à j au (j – i + 1)e appel, ce qui arrêtera la récursion puisque le cas de base sera atteint, pourvu que la condition i <= j ait été respectée lors de l‟appel initial. Définition d’une fonction récursive (2) • 4. Ecrire en C la définition de la fonction qui devra tester si l’un des cas de base a été atteint, et relancer l’application de la relation de récurrence, si ce n’est pas le cas. int somme(int i, int j) { if (i >= j) return i; else return i + somme(i + 1, j); } Quel est la complexité de cet algorithme? Recherche d’un élément dans un tableau • Ecrire en C la fonction qui cherche une valeur donnée dans un tableau trié. • Quel est la complexité de cet algorithme? Recherche dichotomique (principe) • La recherche dichotomique est une méthode de type « diviser pour régner ». • On considère le tableau ci-dessous • Et la valeur recherchée est 25 Recherche dichotomique (principe) • On coupe le tableau en 2 • Le nombre recherché 25 est supérieur à 17, il ne peut donc appartenir qu’au sous tableau de droite. Recherche dichotomique (principe) • On coupe le sous-tableau de droite en deux : • Le nombre recherché 25 est inférieur ou égal à 42, il ne peut donc appartenir qu’au sous tableau de gauche. Recherche dichotomique (principe) • On coupe le sous-tableau de gauche en deux : • Le nombre recherché 25 est inférieur ou égal à 25, il ne peut donc appartenir qu’au soustableau de gauche qui n’a qu’une seule case : Recherche dichotomique algorithme itératif void recherche_dichotomique(int tab[],int finle,int r){ int i=0; int j=finle-1; int m; while(i!=j){ m=(i+j)/2; if(tab[m]>=r){j=m;} else{i=m+1;} } printf("la valeur recherchée est dans la case %d",i); } Recherche dichotomique algorithme récursif int chercher(int r, int tab[], int i, int j) { int m; if (i == j) return tab[i] == r; else { m = (i + j) / 2; if (r <= tab[m]) return chercher(r, tab, i, m); else return chercher(r, tab, m + 1, j); } } Comparaison du Nombre d’itérations dans les algorithmes de recherche x y1 y2 Calcul de la complexité en ordre de grandeur • Pour calculer la complexité, il faut chercher en premier lieu une relation (de récurrence) entre le « coût » de la nième itération et le coût de l’itération précédente. • Dans notre cas, on peut remarquer que : C(n)=C(n/2)+1 * 1= cout d’une itération Calcul de la complexité en ordre de grandeur O(ln (n) ) Algorithmes de tri • Tri à bulles – premier algorithme de tri auquel on pense intuitivement – comparer deux à deux les éléments d'un tableau ou d'une liste à trier et d'échanger leur position s'ils sont mal placés Tri à bulles • Principe (1/3) : – comparer deux valeurs adjacentes et d'inverser leur position si elles sont mal placées. – si un premier nombre x est plus grand qu'un deuxième nombre y – on souhaite trier l'ensemble par ordre croissant, – alors x et y sont mal placés et il faut les inverser. – Si, au contraire, x est plus petit que y, – alors on ne fait rien – et on compare y à z: l'élément suivant. Tri à bulles • Principe (2/3) : – C'est donc itératif. – Et on parcourt ainsi la liste jusqu'à ce qu'on ait réalisé n-1 passages (n représentant le nombre de valeurs à trier) – ou jusqu'à ce qu'il n'y ait plus rien à inverser lors du dernier passage. Tri à bulles • Principe (3/3) – Au premier passage, on place le plus grand élément de la liste au bout du tableau, au bon emplacement. – Pour le passage suivant, nous ne sommes donc plus obligés de faire une comparaison avec le dernièr élément ; – Donc à chaque passage, le nombre de valeurs à comparer diminue de 1. Tri à bulles • Illustration du principe : Considérons les éléments suivants : 6035142 • Nous voulons trier ces valeurs par ordre croissant. Tri à bulles Illustration du principe : Premier passage : 6035142 0635142 0365142 0356142 0351642 0351462 0351426 sage // On compare 6 et 0 : on inverse // On compare 6 et 3 : on inverse // On compare 6 et 5 : on inverse // On compare 6 et 1 : on inverse // On compare 6 et 4 : on inverse // On compare 6 et 2 : on inverse // Nous avons terminé notre premier pas Tri à bulles • Deuxième passage : • refaire un passage mais en omettant la dernière case. 0 3 5 1 4 2 6 // On compare 0 et 3 : on laisse 0 3 5 1 4 2 6 // On compare 3 et 5 : on laisse 0 3 5 1 4 2 6 // On compare 5 et 1 : on inverse 0 3 1 5 4 2 6 // On compare 5 et 4 : on inverse 0 3 1 4 5 2 6 // On compare 5 et 2 : on inverse 0 3 1 4 2 5 6 // Nous avons terminé notre deuxième passage Tri à bulles 0 3 1 4 2 5 6 // On compare 0 et 3 : On laisse 0 3 1 4 2 5 6 // On compare 3 et 1 : On inverse 0 1 3 4 2 5 6 // On compare 3 et 4 : On laisse 0 1 3 4 2 5 6 // On compare 4 et 2 : On inverse 0 1 3 2 4 5 6 // Nous avons terminé notre 3eme passage Tri à bulles 0132456 0132456 0132456 0123456 passage // On compare 0 et 1 : On laisse // On compare 1 et 3 : On laisse // On compare 3 et 2 : On inverse // Nous avons terminé notre 4eme Tri à bulles 0 1 2 3 4 5 6 // On compare 0 et 1 : On laisse 0 1 2 3 4 5 6 // On compare 1 et 2 : On laisse 0 1 2 3 4 5 6 // Nous avons terminé notre passa ge l'algorithme s'arrête • il n'y a plus eu d'échange lors du dernier passage Tri à bulles #include <stdio.h> #include <stdlib.h> #define N 8 int main() { int i,j; int tab[8] = {15, 10, 23, 2, 8, 9, 14, 16}; printf("Avant:"); for(i = 0; i < N; i++) printf("%d, ",tab[i]); Tri à bulles for (i=0 ; i<N ; i++) { int j=0; for (j=0 ; j<(N-i-1) ; j++) { if (tab[j]>=tab[j+1]) { int tampon = tab[j]; tab[j] = tab[j+1]; tab[j+1] = tampon; //on échange les 2 valeurs, en utilisant une case tampon } } } Tri à bulles printf("\nAprès:"); for(i = 0; i < N; i++) printf("%d, ",tab[i]); printf("\n"); system("PAUSE"); return 0; } Complexité du tri à bulles • la complexité du tri à bulles est en O(n²). Tri par selection • Principe : – rechercher le plus grand élément le placer en fin de tableau, recommencer avec le second plus grand, le placer en avant-dernière position et ainsi de suite jusqu'à avoir parcouru la totalité du tableau. Tri par selection • Soit le tableau d'entiers suivant : 6281537940 1. L'élément le plus grand se trouve en 7ème position (si on commence à compter à partir de zéro) 6281537940 2. On échange l'élément le plus grand (en 7ème position) avec le dernier : 6281537049 Tri par selection • Le dernier élément du tableau est désormais forcément le plus grand. • On continue donc en considérant le même tableau, en ignorant son dernier élément : 6241537089 • Et ainsi de suite, en ignorant à chaque fois les éléments déjà triés. Tri par selection Void tri_selection (int tab[],int finle) { Int i,j,imax,temp; For(j=finle-1;j>=0;j--){ imax=0; For(i=1;i<=j;i++){ If(tab[imax]<tab[i]){imax=i;} Temp=tab[j]; Tab[j]=tab[imax]; Tab[imax]=temp; } } Le tri par insertion • Principe – insère un élément dans une liste d'éléments déjà triés • Exemple : jeu de cartes : – main gauche : des cartes triées de la plus petite à la plus grande – Main droite : une carte – Main gauche : 1 3 6 8 ---main droite 5 – il faut la placer après (1 3) et avant (6 8) Le tri par insertion • Principe Le tri par insertion • Principe : – Pour trier entièrement un ensemble de cartes: • placer toutes ses cartes dans la main droite (la main gauche est donc vide), • et d'insérer les cartes une à une dans la main gauche. Le tri par insertion • Dans un tableau, on a du décaler certaines cartes : – 6 était en position 2 avant l'insertion, elle est en position 3 après. – De même, la carte 8 a été décalée. – il faut décaler d’une case vers la droite toutes les cartes plus grandes que la carte à insérer. Le tri par insertion • on décale la carte la plus à droite (8), • puis celle juste à gauche (6), • jusqu'au moment où on tombe sur une carte plus petite que celle qu'on veut insérer, qu'il ne faut pas décaler. • Une fois qu'on a fait ces décalages, on peut insérer la carte, à la position à laquelle on s'est arrêté de décaler Le tri par insertion (Implementation) • La fonction qui insere void inserer(int element_a_inserer, int tab[], int finle_gauche) { int j; for (j = finle_gauche; j > 0 && tab[j-1] > element_a_inserer; j--) On part de la fin de la main gauche, donc de finle_gauche, et on descend (j--) tant que les cartes sont plus grandes que la carte à insérer Le test j > 0 vérifie qu’on ne sort pas du tableau tab[j] = tab[j-1]; //la boucle s’arrete tab[j] = element_a_inserer; On insère alors cet élément juste après la case j-1, donc en j. } La boucle s’arrête quand la carte tab[j-1] devient plus petite que l’élément à insérer Le tri par insertion (implémentation) Le tri par insertion (implémentation) • Remarque : – finle_gauche est la finle de la main gauche, – mais ce n’est pas la finle du tableau tab : – comme on rajoute un élément, on a besoin que le tableau tab ait au moins une case de plus. – Donc finle réelle de tab est toujours strictement supérieure à finle_gauche, – c’est pour cela qu’on a écrit dans tab[finle_gauche] Le tri par insertion (implémentation) void tri_insertion(int tab[], int finle) { int i; for(i = 1; i < finle; i++) Element à inserer inserer(tab[i], tab, i); } L’idée, c’est que je considère que toutes les cartes avant i sont triées, et que toutes les cartes après i ne sont pas triées, tab[i] compris. i est donc la limite entre la main gauche et la main droite. Le tri par insertion (implémentation) void tri_insertion(int tab[], int finle) • L’idée, c’est que je considère que { int i; toutes les cartes avant i sont for(i = 1; i < finle; ++i) triées, inserer(tab[i], tab, i); • et que toutes les cartes après i ne } sont pas triées, tab[i] compris. • i est donc la limite entre la main gauche et la main droite. void inserer(int element_a_inserer, int tab[], • Donc les deux mains ne sont pas int finle_gauche) séparées: – elles sont ensembles dans un seul tableau : • la main gauche est le début du tableau, qui est déjà trié, • et la main droite le reste du tableau. Le tri par insertion (implémentation) Le tri par insertion (implémentation) Code complet (tri par insertion) #include <stdio.h> #include <stdlib.h> void affichage(); void inserer(); void tri_insertion(); int main(int argc, char *argv[]) { int tab[]={15,20,35,10,60,5,3}; int finle=7; affichage(tab,finle); printf("avant"); tri_insertion(tab,finle); printf("apres"); affichage(tab,finle); system("PAUSE"); return 0; } void inserer(int element_a_inserer, int tab[], int finle_gauche) { int j; for (j = finle_gauche; j > 0 && tab[j-1] > element_a_inserer; j--) tab[j] = tab[j-1]; //la boucle s’arrete tab[j] = element_a_inserer; } void tri_insertion(int tab[], int finle){ int i; for(i = 1; i < finle; i++) inserer(tab[i], tab, i); } void affichage(int tab[],int finle){ int i=0; for (i=0;i<finle;i++)printf("tab[%d]=%d\n",i,tab[i]); } Les pointeurs • Un pointeur est une variable qui contient l’adresse d’une autre variable. • Exemple: int x, y, *p; x=4; p=&x //p contient l’adresse de x; Les pointeurs Les pointeurs int x, y, *p; x=4; p=&x //p contient l’adresse de x; Printf(‘’ adresse de x % d’’, p); Printf(‘’ adresse de x % d’’, &x); Printf(‘’ adresse de x % d’’, *p); Printf(‘’ adresse de x % d’’, x); Les pointeurs • • • • x=4; P=&x; y=*p; (*p)++;// incrementer le contenu de l’adresse p par 1. incrementer x par 1; • Printf(‘’ valeur de x est %d,x) //x=5; • Printf(‘’ la valeur de y est %d’’, y);//y=4 Les pointeurs • Exemple 2 : int age = 10; printf("La variable age vaut : %d", age); printf("L'adresse de la variable age est : %p", &age); Les pointeurs int age = 10; int *pointeurSurAge; // 1) signifie "Je crée un pointeur" pointeurSurAge = &age; // 2) signifie "pointeurSurAge contient l'adresse de la variable age" Les pointeurs Les pointeurs int nombre = 0; scanf("%d", &nombre); • Même écriture : int nombre = 0; int *pointeur ; pointeur= &nombre; scanf("%d", pointeur); Les pointeurs • Est-il possible qu’une fonction retourne deux valeurs? Les pointeurs, passage par adresse void decoupeMinutes(int* pointeurHeures, int* pointeurMinutes); void decoupeMinutes(int* pointeurHeures, int* pointeurMinutes) { *pointeurHeures = *pointeurMinutes / 60; *pointeurMinutes = *pointeurMinutes % 60; } Les pointeurs, passage par adresse int main(int argc, char *argv[]) { int heures = 0, minutes = 90; // On envoie l'adresse de heures et minutes decoupeMinutes(&heures, &minutes); // Cette fois, les valeurs ont été modifiées ! printf("%d heures et %d minutes", heures, minutes); return 0; } Tableaux pointeurs et allocation dynamique • • • • void exemple2() { float *f; printf(" f: %x",f); • f=f+1;/* augmente f de 4 octets en fait */ • /* si un float est codé sur 4 octets */ • printf(" f: %x",f); • } Tableaux pointeurs et allocation dynamique void exemple3() { int tab[3]={5,15,20}; int *p; p=&tab[0]; /* <=> p=tab; */ printf ( "*p=%d",*p); printf ( "adresse element 0: %x\n",p); p++; /* p pointe maintenant sur le deuxième élément du tableau */ printf ( "*p=%d",*p); printf ( "adresse element 1: %x\n",p); /* tab++; n'est pas possible car tab est un pointeur constant */ } Tableaux pointeurs et allocation dynamique void exemple4() { int i; char ch1[80], ch2[80]; char *s1, *s2; /* lecture de la chaîne de caractères ch1 */ scanf("%s", ch1); /* passage par adresse, ch1 est déjà une adresse*/ s1=ch1; printf("s1=%c\n",*s1); while(*s1!=0) { *s2 = *s1; s1++; s2++;} } } Tableaux pointeurs et allocation dynamique int *tab; tab=malloc(4*sizeof(int)); tab[0]=5; tab[1]=13; tab[2]=15; tab[3]=25; int i; int *p; p=&tab[0]; for(i=0;i<4;i++){ printf("%d,",*p); p++; } Structure de données typedef struct { char nom; float x, y; } point; Structure de données • • • • point a; a.nom='a'; a.x=5; a.y=6; • • • • point b; b.nom='b'; b.x=5; b.y=10; Structure de données point milieu(point a, point b){ point m; m.nom='m'; m.x=(a.x+b.x)/2; m.y=(a.y+b.y)/2; return m; } Structure de données • point m=milieu(a,b); • • printf("le point m milieu de a et b a pour coordonnees x=%f et y=%f",m.x,m.y); Listes chaînées Listes chaînées • Plusieurs façons d’implémenter des conteneurs : – Tableaux – Listes chaînées Listes chaînées • Tableau : – Éléments placés de façon contiguë en mémoire – éléments de celui-ci sont placés de façon contiguë en mémoire – Pour supprimer un élément au milieu du tableau, il faut recopier les éléments temporairement, réallouer de la mémoire pour le tableau, puis le remplir à partir de l'élément supprimé. Liste chaînée • liste chaînée – les éléments de la liste sont répartis dans la mémoire et reliés entre eux par des pointeurs. – On peut ajouter et enlever des éléments d'une liste chaînée à n'importe quel endroit, à n'importe quel instant, sans devoir recréer la liste entière. Listes chaînées Listes chaînées • Dans une liste chaînée, – la finle est inconnue au départ, la liste peut avoir autant d'éléments que votre mémoire le permet. – impossible d'accéder directement à l'élément i de la liste chainée. – Pour ce faire, il faudra traverser les i-1 éléments précédents de la liste. • Pour déclarer une liste chaînée, – il suffit de créer le pointeur qui va pointer sur le premier élément de la liste chaînée, aucune finle n'est donc à spécifier. – Il est possible d'ajouter, de supprimer, d'intervertir des éléments d'un liste chaînée sans avoir à recréer la liste en entier, mais en manipulant simplement leurs pointeurs. Listes chaînées • Chaque élément d'une liste chaînée est composé de deux parties : – la valeur que vous voulez stocker, – l'adresse de l'élément suivant, s'il existe. S'il n'y a plus d'élément suivant, alors l'adresse sera NULL, et désignera le bout de la chaîne. • Listes chaînées Définition d’une liste chaînée dans le langage C #include <stdlib.h> typedef struct element element; struct element { int val; struct element *nxt; }; typedef element* llist; Listes chaînées • On crée le type element qui est une structure – contenant un entier (val) – et un pointeur sur élément (nxt), qui contiendra l'adresse de l'élément suivant. • Ensuite, il nous faut créer le type llist (pour linked list = liste chaînée) qui est en fait un pointeur sur le type element. • Lorsque nous allons déclarer la liste chaînée, – nous devrons déclarer un pointeur sur element, – l'initialiser à NULL, pour pouvoir ensuite allouer le premier élément. Déclaration d’une liste chaînée #include <stdlib.h> typedef struct element element; struct element { int val; struct element *nxt; }; typedef element* llist; int main(int argc, char **argv) { /* Déclarons 3 listes chaînées de façons différentes mais équivalentes */ llist ma_liste1 = NULL; element *ma_liste2 = NULL; struct element *ma_liste3 = NULL; return 0; } Listes chaînées Ajouter un élément Listes chaînées Ajouter en tête llist ajouterEnTete(llist liste, int valeur) { /* On crée un nouvel élément */ element* nouvelElement =malloc(sizeof(element)); /* On assigne la valeur au nouvel élément */ nouvelElement->val = valeur; /* On assigne l'adresse de l'élément suivant au nouvel élément */ nouvelElement->nxt = liste; /* On retourne la nouvelle liste ca veut dire le pointeur sur le premier élément */ return nouvelElement; } Listes chaînées Ajouter en fin de liste Ajouter en fin llist ajouterEnFin(llist liste, int valeur) { /* On crée un nouvel élément */ element* nouvelElement = malloc(sizeof(element)); /* On assigne la valeur au nouvel élément */ nouvelElement->val = valeur; /* On ajoute en fin, donc aucun élément ne va suivre */ nouvelElement->nxt = NULL; if(liste == NULL) { /* Si la liste est videé il suffit de renvoyer l'élément créé */ return nouvelElement; } else { /* Sinon, on parcourt la liste à l'aide d'un pointeur temporaire et on indique que le dernier élément de la liste est relié au nouvel élément */ element* temp=liste; while(temp->nxt != NULL) { temp = temp->nxt; } temp->nxt = nouvelElement; return liste; } } Listes chaînées Utilisation de la fonction ajouter en fin ma_liste1=ajouterEnFin(ma_liste1, 5); • Exercice 1 : Afficher la liste chaînée : Vous devrez parcourir la liste jusqu'au bout et afficher toutes les valeurs qu'elle contient. void afficherListe(llist liste) { element *tmp = liste; /* Tant que l'on n'est pas au bout de la liste */ while(tmp ->nxt!= NULL) { /* On affiche */ printf("%d ", tmp->val); /* On avance d'une case */ tmp = tmp->nxt; } } Listes chaînées Dans la fonction main llist ma_liste1=NULL; ma_liste1=ajouterEnFin(ma_liste1, 5); ma_liste1=ajouterEnFin(ma_liste1, 10); ma_liste1=ajouterEnFin(ma_liste1, 15); afficherListe(ma_liste1); • Exercice 2 : En utilisant trois fonctions que nous avons vues : • ajouterEnTete • ajouterEnFin • afficherListe Vous devez écrire la fonction main permettant de remplir et afficher la liste chaînée ci-dessous. Vous ne devrez utiliser qu'une seule boucle for. 10 9 8 7 6 5 4 3 2 1 1 2 3 4 5 6 7 8 9 10 Listes chaînées • int main() • { • llist ma_liste = NULL; • int i; • • for(i=1;i<=10;i++) • { • ma_liste = ajouterEnTete(ma_liste, i); • ma_liste = ajouterEnFin(ma_liste, i); • } • afficherListe(ma_liste); • • return 0; • } Listes chaînées • Exercice 3 : Écrivez une fonction qui renvoie 1 si la liste est vide, et 0 si elle contient au moins un élément. int estVide(lliste liste) { if(liste == NULL) { return 1; } else { return 0; } } Dans la fonction main if(estVide(ma_liste)) { printf("La liste est vide"); } else { afficherListe(ma_liste); } Listes chaînées • Exercice 3 : Ecrire un programme qui supprime le premier element de la liste chainee Exercice 4 : Ecrire un programme qui supprime le dernier element de la liste chainee Supprimer en tête de liste llist supprimerElementEnTete(llist liste) { if(liste != NULL) { /* Si la liste est non vide, on se prépare à renvoyer l'adresse de l'élément en 2ème position */ element* aRenvoyer = liste->nxt; /* On libère le premier élément */ free(liste); /* On retourne le nouveau début de la liste */ return aRenvoyer; } else { return NULL; } } Supprimer en fin de liste (1/2) llist supprimerElementEnFin(llist liste) { /* Si la liste est vide, on retourne NULL */ if(liste == NULL) return NULL; /* Si la liste contient un seul élément */ if(liste->nxt == NULL) { /* On le libère et on retourne NULL (la liste est maintenant vide) */ free(liste); return NULL; } Supprimer en fin de liste (2/2) /* Si la liste contient au moins deux éléments */ element* tmp = liste; element* ptmp = liste; /* Tant qu'on n'est pas au dernier élément */ while(tmp->nxt != NULL) { /* ptmp stock l'adresse de tmp */ ptmp = tmp; /* On déplace tmp (mais ptmp garde l'ancienne valeur de tmp */ tmp = tmp->nxt; } /* A la sortie de la boucle, tmp pointe sur le dernier élément, et ptmp sur l'avant-dernier. On indique que l'avant-dernier devient la fin de la liste et on supprime le dernier élément */ ptmp->nxt = NULL; free(tmp); return liste; } • Exercice 5 : • Ecrire un programme qui recherche si une valeur N existe dans la liste chainee Rerchercher un element dans la liste llist rechercherElement(llist liste, int valeur) { element *tmp=liste; /* Tant que l'on n'est pas au bout de la liste */ while(tmp != NULL) { if(tmp->val == valeur) { /* Si l'élément a la valeur recherchée, on renvoie son adresse */ return tmp; } tmp = tmp->nxt; } return NULL; } Exercice 6 • Ecrire un programme qui calcule le nombre d’occurences d’une valeur dans une liste chainee d’entiers. Nombre d’occurences d’une valeur int nombreOccurences(llist liste, int valeur) { int i = 0; /* Si la liste est vide, on renvoie 0 */ if(liste == NULL) return 0; /* Sinon, tant qu'il y a encore un élément ayant la val = valeur */ while((liste = rechercherElement(liste, valeur)) != NULL) { /* On incrémente */ liste = liste->nxt; i++; } /* Et on retourne le nombre d'occurrences */ return i; } • Exercice 7 : • ecrire un programme qui recherche la valeur de la ieme position d’une liste chainee Recherche du ième élément Int element_i(llist liste, int indice) { int i; /* On se déplace de i cases, tant que c'est possible */ for(i=0; i<indice && liste != NULL; i++) { liste = liste->nxt; } /* Si l'élément est NULL, c'est que la liste contient moins de i éléments */ if(liste == NULL) { return NULL; } else { /* Sinon on renvoie l'adresse de l'élément i */ return liste->valeur; } } • Exercice 8 : Compter le nombre d'éléments d'une liste chaînée Compter le nombre d'éléments d'une liste chaînée int finleListe(llist liste) { int i=0; element *tmp = liste; /* Tant que l'on n'est pas au bout de la liste */ while(tmp!= NULL) { i++; tmp = tmp->nxt; } return i; } Listes doublement chaînées • Une liste doublement chainée contient : – Une valeur(ici un simple entier) – Un pointeur vers l'élément suivant (NULL si l'élément suivant n'existe pas) – Un pointeur vers l'élément précédent (NULL si l'élément précédent n'existe pas) Listes doublement chaînées Représentation en C • Typedef struct node • { • int valeur; • struct node *p_next; • struct node *p_prev; • }; Représentation en C typedef struct Dlist { size_t length; //size_t : entier positif struct node *p_debut; struct node *p_fin; } Dlist; Déclaration d'une liste vide • Dlist *list = NULL; /* Déclaration d'une liste vide */ Allouer une nouvelle liste Dlist *dlist_new(void) { Dlist *p_new = malloc(sizeof(Dlist)); if (p_new != NULL) { p_new->length = 0; p_new->p_debut = NULL; p_new->p_fin = NULL; } return p_new; } Ajouter en fin de liste doublement chaînée Ajouter en fin de liste doublement chaînée(1/2) Dlist *dlist_append(Dlist *p_list, int valeur) { if (p_list != NULL) /* On vérifie si notre liste a été allouée */ { struct node *p_new = malloc(sizeof *p_new); /* Création d'un nouveau node */ if (p_new != NULL) /* On vérifie si le malloc n'a pas échoué */ { p_new->valeur = valeur; /* On 'enregistre' notre donnée */ p_new->p_next = NULL; /* On fait pointer p_next vers NULL */ if (p_list->p_fin == NULL) /* Cas où notre liste est vide (pointeur vers fin de liste à NULL) */ Ajouter en fin de liste doublement chaînée(2/2) { p_new->p_prev = NULL; /* On fait pointer p_prev vers NULL */ p_list->p_debut = p_new; /* On fait pointer la tête de liste vers le nouvel élément */ p_list->p_fin = p_new; /* On fait pointer la fin de liste vers le nouvel élément */ } else /* Cas où des éléments sont déjà présents dans notre liste */ { p_list->p_fin->p_next = p_new; /* On relie le dernier élément de la liste vers notre nouvel élément (début du chaînage) */ p_new->p_prev = p_list->p_fin; /* On fait pointer p_prev vers le dernier élément de la liste */ p_list->p_fin = p_new; /* On fait pointer la fin de liste vers notre nouvel élément (fin du chaînage: 3 étapes) */ } p_list->length++; /* Incrémentation de la finle de la liste */ } } return p_list; /* on retourne notre nouvelle liste */ } Ajout en début de liste doublement chaînée (1/2) Dlist *dlist_prepend(Dlist *p_list, int valeur) { if (p_list != NULL) { struct node *p_new = malloc(sizeof *p_new); if (p_new != NULL) { p_new->valeur = valeur; p_new->p_prev = NULL; if (p_list->p_fin == NULL) { p_new->p_next = NULL; p_list->p_debut = p_new; p_list->p_fin = p_new; } Ajout en début de liste doublement chaînée (2/2) else { p_list->p_debut->p_prev = p_new; p_new->p_next = p_list->p_debut; p_list->p_debut = p_new; } p_list->length++; } } return p_list; } Insérer un élément dans une liste doublement chaînée en position Insérer un élément dans une liste doublement chaînée en position • Si nous sommes (cette position) en fin de liste – (p_temp->p_next == NULL), – nous utilisons notre fonction dlist_append • Sinon, si nous sommes (cette position) en début de liste – (p_temp->p_prev == NULL) – nous utilisons notre fonction dlist_prepend • Sinon, nous devons créer un nouvel élément – réaliser notre chaînage – stoquer la valeur Insérer un élément dans une liste doublement chaînée en position Dlist *dlist_insert(Dlist *p_list, int valeur, int position) { if (p_list != NULL) { struct node *p_temp = p_list->p_debut; int i = 1; while (p_temp != NULL && i <= position) { if (position == i) { if (p_temp->p_next == NULL) { p_list = dlist_append(p_list, valeur); } else if (p_temp->p_prev == NULL) { p_list = dlist_prepend(p_list, valeur); } Insérer un élément dans une liste doublement chaînée en position else { struct node *p_new = malloc(sizeof *p_new); struct node *tempprecedent; struct node *tempsuivant; if (p_new != NULL) { p_new->valeur = valeur; tempsuivant =p_temp->p_next; tempsuivant ->p_prev = p_new; tempprecedent =p_temp->p_prev; tempprecedent ->p_next = p_new; p_new->p_prev = tempprecedent; p_new->p_next = tempsuivant; p_list->length++; } } } else { p_temp = p_temp->p_next; (je sauvegarde l’élément suivant dans une variable temporaire a1) (je sauvegarde l’élément précédent dans une variable temporaire a2); Affichage d’une liste chainée double • void affichage(Dlist *p_list){ • • if (p_list != NULL) /* On vérifie si notre liste a été allouée */ { • node *temp=p_list->p_debut; • • • • • • while(temp!=NULL){ printf("%d,",temp->valeur); temp=temp->p_next; } printf("\n"); } • } Les piles et les files définitions • Une pile permet de réaliser ce que l'on nomme une LIFO (Last In First Out) – Il est possible de comparer cela à une pile d'assiettes. • Lorsqu'on ajoute une assiette en haut de la pile, • on retire toujours en premier celle qui se trouve en haut de la pile, • c'est-à-dire celle qui a été ajoutée en dernier, sinon tout le reste s'écroule. • Une file, quant à elle, permet de réaliser une FIFO (First In First Out) – ce qui veut dire que les premiers éléments ajoutés à la file seront aussi les premiers à être récupérés. Structure de la pile • Tout d'abord, nous allons commencer par définir notre structure qui constituera notre pile. • Pour notre exemple, nous allons créer une pile d'entiers (int). • notre pile sera basée sur une liste chaînée (simple). • Chaque élément de la pile pointera vers l'élément précédent. • La liste pointera toujours vers le sommet de la pile. Voici donc la structure qui constituera notre pile Structure de la pile typedef struct pile { int donnee; /* La donnée que notre pile stockera. */ struct pile *precedent; /* Pointeur vers l'élément précédent de la pile. */ }pile; Rappel pointeurs, indicateur des piles void test(int a, int b){ a=a+2; b=b+2; } ---------------------------------int a=1; int b=1; test(a,b); printf("test a=%d,b=%d",a,b); ---------------------Resultat a reste 1 et b reste 1; Modifions la fonction précédante void test(int *a, int *b){ *a=*a+2; *b=*b+2; } --------------int a=1; int b=1; test(&a,&b); printf("test a=%d,b=%d",a,b); ---------------------Résultat : a=3 et b=3 Comparons alors : • int *a=1; • int *b=1; • test(a,b); ----------------------------------• int **a=10; • int **b=20; • test(*a,*b); • Retournons aux piles Structure de la pile Structure de la pile Fonction push • void pile_push(pile *(*p_pile), int donnee) • Ne retourne pas de valeur : différence avec ce que nous avons vu avec la manipulation des listes chaînées. • Donnée : La valeur à ajouter dans la pile Fonctionnement 1. On crée un nouvel élément de type Pile. 2. On vérifie que l'élément a bien été créé. 3. On assigne à la donnée de cet élément la donnée que l'on veut ajouter. 4. On fait pointer cet élément sur le sommet de la pile. 5. On fait pointer le sommet de la pile sur cet élément. 1. On crée un nouvel élément de type Pile • Pile *p_nouveau = malloc(sizeof (*p_nouveau)); 2. On vérifie que l'élément a bien été créé • if (p_nouveau != NULL) • { • } 3. On assigne à la donnée de cet élément la donnée que l'on veut ajouter • p_nouveau->donnee = donnee; 4. On fait pointer cet élément sur le sommet de la pile • p_nouveau->precedent = p_pile; 5. On fait pointer le sommet de la pile sur cet élément • p_pile = p_nouveau; En assemblant toutes les etapes void pile_push(pile **p_pile, int donnee) { pile *p_nouveau = malloc(sizeof (pile)); if (p_nouveau != NULL) { p_nouveau->donnee = donnee; p_nouveau->precedent = *p_pile; *p_pile = p_nouveau; } } Retrait d’un element • int pile_pop(Pile *p_pile); Retrait d’un element 1. Vérifier si la pile n'est pas vide. 2. Si elle ne l'est pas, stocker dans un élément temporaire l'avantdernier élément de la pile. 3. Stocker dans une variable locale la valeur étant stockée dans le dernier élément de la pile. 4. Supprimer l’élément sommet, de la pile. 5. Faire pointer la pile vers notre élément temporaire. 6. Retourner la valeur dépilée. 1. Vérifier si la pile n'est pas vide • if (p_pile != NULL) • { • } 2-Si elle ne l'est pas, stocker dans un élément temporaire l'avant-dernier élément de la pile • Pile *temporaire = (*p_pile)->precedent; 3-Stocker dans une variable locale la valeur étant stockée dans le dernier élément de la pile • ret = (*p_pile)->donnee; 4. Supprimer le dernier élément • free(*p_pile), *p_pile = NULL; 5. Faire pointer la pile vers notre élément temporaire *p_pile = temporaire; 6. Retourner la valeur dépilée • return ret; Depiler int pile_pop(pile **p_pile){ int ret = -1; if (p_pile != NULL) { pile *temporaire = (*p_pile)->precedent; ret = (*p_pile)->donnee; free(*p_pile); *p_pile = temporaire; } return ret; } • Exercice : • créer une fonction pile_peek retournant la valeur du dernier élément, mais sans le dépiler comme le ferait la fonction pile_pop solution • int pile_peek(Pile *p_pile) • { • int ret = -1; /* Variable de retour. */ • if (p_pile != NULL) /* Si la pile n'est pas vide. */ • { • ret = p_pile->donnee; /* On stocke dans la variable ret la valeur du dernier élément. */ • } • return ret; • } • Exercice 2 : Ecrire un programme qui empile 3 valeurs de variables saisies par le clavier. En utilisant la fonction push • Calculer la moyenne de ces 3 elements en utilisant la fonction pop. Les files • typedef struct file • { • int donnee; • struct file *suivant; • }; Ajout d’un element • 1. On crée un nouvel élément de type File. • 2. On vérifie que le nouvel élément a bien été créé. • 3. On fait pointer cet élément vers NULL. • 4. On assigne à la donnée de cet élément la donnée que l'on veut ajouter. • 5. Si la file est vide, alors on fait pointer la file vers l'élément que l'on vient de créer. • 6. Sinon, on crée un élément temporaire de type File pointant vers notre file. • 7. On parcourt entièrement la file. • 8. On fait pointer l'élément temporaire vers le nouvel élément créé. void file_enqueue(File **p_file, int donnee) { File *p_nouveau = malloc(sizeof *p_nouveau); if (p_nouveau != NULL) { p_nouveau->suivant = NULL; p_nouveau->donnee = donnee; if (*p_file == NULL) { *p_file = p_nouveau; } else { File *p_tmp = *p_file; while (p_tmp->suivant != NULL) { p_tmp = p_tmp->suivant; } p_tmp->suivant = p_nouveau; } } } int file_dequeue(File **p_file) { int ret = -1; /* On teste si la file n'est pas vide. */ if (*p_file != NULL) { /* Création d'un élément temporaire pointant vers le deuxième élément de la file. */ File *p_tmp = (*p_file)->suivant; /* Valeur à retourner */ ret = (*p_file)->donnee; /* Effacement du premier élément. */ free(*p_file), *p_file = NULL; /* On fait pointer la file vers le deuxième élément. */ *p_file = p_tmp; } return ret; } Vidage de la file void file_clear(File **p_file) • { • /* Tant que la file n'est pas vide. */ • while (*p_file != NULL) • { • /* On enlève l'élément courant. */ • file_dequeue(p_file); • } • } • Exercice : • faire une fonction file_peek retournant la valeur du premier élément de la file sans l'enlever de la liste. int file_qeek(File *p_file) { int ret = -1; if (p_file != NULL) { ret = p_file->donnee; } return ret; } Arbre binaire Définition d’un arbre binaire • Définition du type ARBRE, pointeur sur un noeud typedef struct Arbre{ int val ; struct Arbre * gauche; struct Arbre * droite; } Arbre; Arbre binaire de recherche ABR • Ce type d’arbre binaire permet une recherche ayant une complexité de l’ordre de Log(n) • Définition : considérons un élément de l’arbre: – Tous les éléments du sous arbre gauche sont inférieurs. – Tous les éléments du sous arbre droit sont supérieurs – Ce principe s’applique récursivement à chaque élément. • Chaque sous arbre est aussi un ABR Inserer un element dans un arbre binaire de recherche Arbre *inserer(int x, Arbre *a) {if(a==NULL){ a=malloc(sizeof(Arbre)); a->val=x; a->droite=NULL; a->gauche=NULL; return a; } if (x< a->val) { a->gauche=inserer(x,a->gauche); } else if ( x>a->val){ a->droite=inserer(x, a->droite); }} Parcours en profondeur • Principe : parcourir récursivement les fils dans l’ordre – fils gauche – puis fils droite. Parcours en profondeur void Parcours(ARBRE *a) { if (a != NULL) { Parcours (a->gauche); Parcours (a->droite); } } Parcours préfixé • Parcours préfixé : affichage de la valeur d’un noeud avant les valeurs figurant dans ses sous-arbres • Printf : 12 • Gauche – Printf 1 – Gauche • Printf 91 • Gauche NULL • Droite NULL – Droite • Printf 67 • Gauche null • Droite null • Droite – Printf 7 – Gauche NULL – Droite • Printf 82 • Gauche – Printf 61 – Gauche null – Droite null • Droite null Parcours préfixé • • • • • • • • • void ParcoursPrefixe(Arbre *a) { if (a != NULL) { printf("%d-",a->val); ParcoursPrefixe (a->gauche); ParcoursPrefixe(a->droite); } } Parcours infixé • Parcours infixé : affichage de la valeur d’un noeud après les valeurs figurant dans son sous-arbre gauche et avant les valeurs figurant dans son sous-arbre droit Parcours infixé • • • • • • • • • void ParcoursInfixe(Arbre *a) { if (a != NULL) { ParcoursInfixe(a->gauche); printf("%d-",a->val); ParcoursInfixe(a->droite); } } Parcours infixé • Gauche de l’element 12 – Gauche de l’element 1 • • • Gauche de l’element 91 (NULL) Printf 91 Droite NULL – Printf 1 – Droite de l’element 1 • • • • • Gauche NULL Printf 67 Droite NULL Printf 12 Droite de l’element 12 – Gauche de 7 = NULL – Printf 7 – Droite de l’element 7 • Gauche de l’element 82 – – – • • Gauche NULL Printf 61 Droite NULL Printf 82 Droite NULL Parcours postfixé • Parcours postfixé : affichage de la valeur d’un noeud après les valeurs figurant dans ses sous-arbres Parcours postfixé • Gauche de 12->1 – Gauche de 1->91 • • • Gauche de 91 : NULL Droite NULL Printf : 91 – Droite de 1->67 • • • Gauche NULL Droite NULL Printf 67 – Printf 1 • Droite de 12->7 – Gauche de 7 : NULL – Droite de 7: 82 • Gauche de 82: 61 – – – • • Droite NULL Printf 82 – Printf 7 • Printf 12 Gauche de 61 : NULL Droite de 61 : NULL Printf : 61 Parcours postfixé • • • • • • • • • void Parcourspostefixe(Arbre *a) { if (a != NULL) { Parcourspostefixe(a->gauche); Parcourspostefixe(a->droite); printf("%d-",a->val); } } Exercices arbre binaire • Exercice 1 : A-Ecrire une fonction qui retourne le nombre de noeuds total d’un arbre binaire B-Trouver la somme des elements des noeuds. C-Trouver la moyenne des elements d’un arbre binaire de recherche D-Trouver la hauteur d’un arbre binaire. Profondeur de l’arbre #include <Math.h> Int hauteur (ARBRE *a) { if (a != NULL) { Return 1+ Math.max (hauteur(a->gauche),hauteur(a->droite)); } else return 0; } Nombre de noeuds • • • • • int taille(Arbre a) { if(a==Null ) {return 0; } else { return 1 + taille(a->gauche) +taille(a->droite); } • 1+taille(a->gauche)+taille(a->droite); • 1+(1+taille(a->gauche)+taille(a>droite))+taille(a->droite) • • • • • • • • • void Parcours(ARBRE *a) { if (a != NULL) { Printf(“%d”,a->val); Parcours (a->droite); Parcours (a->gauche); } } • • • • int somme(Arbre a) { if(a==Null ) {return 0; } else { return a->val + somme(a->gauche) + somme (a->droite); • } moyenne • Float moyenne ( arbre *a){ //If (finle(a)!=0){ If (arbre!=NULL){ • Return somme(a)/finle(a); • }else • { • return 0; • } moyenne Int somme(ARBRE a){ If(arbre==NULL){ return 0; }else { Return a->val+somme(a->gauche)+somme(a>droite); } • Float moyenne( Arbre a) • {return somme(a)/finle(a);