La présentation est en train de télécharger. S'il vous plaît, attendez

La présentation est en train de télécharger. S'il vous plaît, attendez

Les structures de données

Présentations similaires


Présentation au sujet: "Les structures de données"— Transcription de la présentation:

1 Les structures de données
Rajae El Ouazzani

2 Section 1: Introduction
Les traitements informatiques ont pour objectif la manipulation des données. D’où la nécessité d’organiser et structurer les données de manière appropriée aux traitements envisagés. Ce problème d’organisation des données est bien antérieur à l’informatique. Par exemple, on numérote les pages d’un livre, on construit des index des termes importants. Dans un dictionnaire par exemple, les mots sont rangés dans l’ordre alphabétique, etc ... Ainsi, l’informatique n’arrête pas de pousser à l’extrême cette démarche organisatrice.

3 Les types des structures de données
Dans le cadre de la structuration des données, nous distinguerons soigneusement une structure de données abstraite et une structure de données concrète. La différence entre les 2 types de structures est de même nature que la différence entre un problème et un algorithme. Une structure de données abstraite décrit l’ensemble de services algorithmiques associés à la structure permettant la création, la consultation et la modification des données. Alors qu’une structure de donnée concrète décrit et regroupe les algorithmes promis par la structure abstraite correspondante.

4 Les types des structures de données (Suite)
Donnons quelques exemples de structures de données abstraites. La plus simple est la collection : elle regroupe simplement une collection d’objets. Une structure plus élaborée est celle du dictionnaire, qui regroupe un ensemble d’objets munis de clés et il permet de retrouver facilement un objet à partir de sa clé. Le tableau et la liste chaînée sont deux exemples de structures de données concrètes.

5 Section 2: Pointeur et tableau (Gestion dynamique de la mémoire)

6 1-La gestion des variables en mémoire
Pour aborder la notion de pointeur, il convient de revenir sur la notion de variable. Nous allons voir de plus près ce qui se passe au niveau de la machine quand on déclare une variable ou lorsqu’on l'assigne. Les variables sont stockées dans une mémoire statique (qu’on peut représenter par un tableau). Chaque ligne de ce tableau est une "case" mémoire c'est à dire une zone où l'on peut stocker une donnée atomique (par exemple un entier). Dans la machine, toutes ces cases sont numérotées sur n bits. Ce numéro de la case est attribué de façon unique à chaque case de la mémoire, on parle donc de l'adresse mémoire de la case.

7 Exemple : Le programme suivant est l'algorithme inversion de deux variables étudié en algorithmique impérative) : Algorithme inversion_calcul Variables a : entier b : entier ...

8 Voici à quoi ressemble notre mémoire statique si notre machine code les entiers sur un octet (une case mémoire). Voici le schéma de la mémoire avec deux cases a et b réservées :

9 Exemple (Suite) À chaque assignation d'une variable, on relie une case mémoire (adresse) à une variable. L'identifiant de la variable est en fait une abstraction et derrière l'identifiant d'une variable se cache l'adresse mémoire de son contenu.

10 2- La mémoire dynamique En plus d'allouer à chaque programme une mémoire statique, le système d'exploitation alloue au programme P une mémoire dynamique. Dans notre exemple avec l'inversion de deux variables, notre programme n'utilise que la mémoire statique. Nous aurions pu omettre de représenter sur nos schémas cette mémoire. La mémoire dynamique a les mêmes caractéristiques que la mémoire statique telle que décrite précédemment. Les cases sont numérotées (nous supposerons là encore sur 8 bits : de 00 à FF) Nous allons apprendre comment utiliser cette mémoire dynamique avec les pointeurs.

11 3- Les pointeurs typés Le pointeur est un nouveau type tout comme les entiers, les caractères, chaînes de caractères, les booléens etc. Il convient cependant de distinguer les pointeurs typés des pointeurs non-typés (également appelés génériques). Un pointeur typé indique le type de la donnée qu'il pointe. Il y a donc des pointeurs vers des entiers, des pointeurs vers des caractères, etc. Il convient d'affecter à des variables de types pointeur vers T uniquement des expressions de types pointeur vers T tout comme on doit assigner à une variable de type entier une expression entière.

12 Utilisation des pointeurs typés
Pour stocker des données dans notre mémoire dynamique, il faut réserver l'espace nécessaire. p est une variable de type pointeur (vers T, un type quelconque : entier...) : Lexique p : ^T (* un pointeur vers des données de type T *) Au lancement du programme, ce dernier va réserver dans la mémoire statique l'espace permettant de stocker ce pointeur.

13 Voici la représentation de la mémoire à cet instant:

14 Les pointeurs typés ( Suite)
new(p) Voici l'effet produit par new sur la mémoire. Deux choses se sont produites : De la mémoire a été réservée pour stocker une donnée de type T (la case pointée est grisée). La variable p contient l'adresse mémoire de cet espace réservé (représenté par une flèche).

15 Les pointeurs typés (Suite)
p contient alors l'adresse de l'espace que nous pouvons utiliser, on peut donc stocker le contenu de p en mémoire dynamique. Pour utiliser l'espace pointé par p, nous utiliserons l'expression p^ p^:=??? L'espace pointé contient alors la valeur de l'expression qu'on a assigné.

16

17 Les pointeurs typés (Suite)
On peut libérer l'espace : c'est à dire indiquer au système d'exploitation que cette partie de la mémoire dynamique n'est plus utilisée et qu'il peut désormais en faire ce qu'il veut. Pour cela on utilise la procédure « réciproque » de new : delete(p)

18

19 On remarquera qu'après un delete la donnée stockée ainsi que le pointeur restent en mémoire.
La seule chose qui change est que l'espace en mémoire dynamique n'est plus réservé. La donnée pointée peut donc être écrasée. La valeur de p^ restera en mémoire tant que le système d'exploitation n'aura pas décidé d'attribuer cet espace. Remarquez que demander au processeur de supprimer les contenus qui ont été stockés serait une perte de temps puisque si cette partie de la mémoire vient d’être utilisée, les données seront écrasées de toute façon.

20 Exemple avec des entiers
Remarque : encore une fois ce programme n'a pas l’utilité. On peut en faire un équivalent sans utiliser de pointeurs. Le but est d'expliquer les mécanismes décrits précédemment.

21 Algorithme Exemple Lexique a : entier b : entier p1 : ^entier p2 : ^entier Début a:=1 new(p1) (* on réserve de l'espace pour stocker un entier *) p1^:=a (* stocke 1 dans la zone mémoire pointée par p1 *) ecrire(p1^) (* affiche 1 *) a:=2 p2:=p1 (* p2 va pointer la zone mémoire pointée par p1 *) ecrire(p2^) (* affiche 1 *) p2^:=3 (* on place 3 dans la zone mémoire pointée par p2 *) ecrire(p1^) (* affiche 3 *) delete(p1) (* on libère la zone pointée par p1 et donc la zone pointée par p2 *)

22 (* a ce stade : plus aucune zone mémoire n'est réservée dans la mémoire dynamique *)
new(p1) new(p2) p1^:=4 p2^:=p1^+1 ecrire(p2^) (* affiche 5 *) (* il est d'usage de libérer au maximum la mémoire dynamique : *) delete(p1) delete(p2) Fin

23 4- Les pointeurs génériques
Un pointeur générique est un type de pointeur particulier puisqu'il ne donne pas d'information sur le type de donnée qu'il pointe. C'est simplement une adresse vers une zone mémoire.

24 Utilisation Les pointeurs typés sont également des adresses mémoires, donc on peut assigner les uns aux autres. Lexique p : pointeur p_T : ^T Ces opérations sont possibles : p:=p_T p_T:=p Cependant, on ne peut pas utiliser les procédures new() et delete() avec un pointeur générique en paramètre. En effet, le type de la donnée pointée étant inconnu, le système d'exploitation ne peut savoir combien d'espace réservé.

25 Exemple avec des entiers
Algorithme exemple Lexique p : pointeur p_entier : ^entier début new(p_entier) p_entier^:=1 p:=p_entier p_entier:=p ecrire(p_entier^) (* affiche 1 *) fin

26 Section 3: Listes chainées
Les listes chaînées sont des structures de données à accès indirect. L’idée d’une liste chainée est la définition d’une structure ayant un ou plusieurs pointeurs. Ces pointeurs sont reliés pour faire une chaîne : le premier élément pointe le second, le second pointe de troisième, etc.

27 L’avantage des listes est le dynamisme d’une part
L’avantage des listes est le dynamisme d’une part. En effet, on réserve la mémoire pour chaque maillon de la chaîne, donc si la taille de la liste varie beaucoup on prend moins de ressource inutilement. Le deuxième avantage est le fait que l’on peut facilement ajouter des éléments au milieu de la liste. Dans un tableau, il faut d’abord déplacer les éléments du tableau pour avoir une case vide. De même, on peut facilement enlever des éléments d’une liste.

28 Représentation d’une liste chainées
Pour des raisons de lisibilité et parce que cela ne change rien, on représente les listes chaînées en les mettant dans l’ordre logique et on n’identifie pas les adresses mémoire.

29 Définition de la structure n
Définition d’une structure n typedef struct n{ int info; struct n* suivant; /*pointeur ‘suivant’ de type ‘structure n’ */ }NŒUD ;

30 1- Initialiser une liste chaînée
NŒUD* initialiser(){ return NULL ; /* ‘initialiser()’ retourne NULL */ } void main(){ NŒUD* ptrNoeud ; /*pointeur ‘ptrNoeud’ de type ‘NŒUD’*/ ptrNoeud = initialiser();/*initialiser le pointeur ptrNoeud à NULL */

31 2- Initialiser une liste chaînée avec un nœud factice au début
NŒUD* initialiser(){ NŒUD* ptrNoeud; ptrNoeud=(NŒUD*)malloc(sizeof(NŒUD)); if(ptrNoeud!= NULL){/*vérifier si la réservation mémoire est bien faite */ ptrNoeud->suivant=NULL; /* Mettre ‘NULL’ dans le pointeur ‘suivant’ */ } return ptrNoeud ; void main(){ NŒUD* debut; debut = initialiser(); /* ‘debut’ est le début de la liste */

32 Fonctions malloc et free
<pointeur> = <type> malloc(<taille>); - <type> est un type pointeur définissant la variable pointé par <pointeur>. - <taille> est la taille, en octets, de la zone mémoire à allouer dynamiquement. <taille> est du type unsigned int, donc on ne peut pas réserver plus de octets à la fois. - La fonction malloc retourne l’adresse du premier octet de la zone mémoire allouée. En cas d’échec, elle retourne NULL. 2- free - Si on n’a plus besoin d’un bloc de mémoire réservé dynamiquement par malloc, alors on peut le libérer à l’aide de la fonction free. free(<pointeur>); - Libère le bloc de mémoire désignée par le pointeur <pointeur>.

33 3- Initialiser une liste chaînée avec un nœud factice au début et à la fin
NŒUD* initialiser(){ NŒUD* z, * debut ; z = (NŒUD*)malloc(sizeof(NŒUD)); /* ‘z’ est le nœud de la fin*/ if( z != NULL ){ /* Vérifier si l’allocation dynamique du nœud z est bien faite */ z->suivant = z ; /* le pointeur ‘suivant’ de z pointera sur z */ debut = (NŒUD*)malloc(sizeof(NŒUD)); if( debut != NULL ){ debut->suivant = z ;/* Mettre z dans le pointeur ‘suivant’ de début */ } return debut ; void main(){ NŒUD* debut ; debut = initialiser() ;

34 Insérer dans une liste chaînée

35 1- Insérer dans une liste chaînée avec un argument de type pointeur
NŒUD* insererEnTete(NŒUD* debut, int i){ NŒUD* nouveau ; nouveau = (NŒUD*)malloc(sizeof(NŒUD)) ; if(nouveau!= NULL){ nouveau->suivant = debut ;/* Mettre dans le ‘suivant de nouveau’ ‘NULL’*/ nouveau->info = i ; debut = nouveau ; } return nouveau ; void main(){ NŒUD* debut ; debut = initialiser() ; debut = insererEnTete(debut,20);/* Mettre la valeur 20 dans le nœud inséré */

36 2- Insérer dans une liste chaînée avec un argument de type pointeur de pointeur
int insererEnTete(NŒUD** debut, int i){ NŒUD* nouveau ; nouveau = (NŒUD*)malloc(sizeof(NŒUD)); if( nouveau != NULL ){ nouveau->suivant = *debut; nouveau->info = i ; *debut = nouveau ; return 1 } else { return 0 } void main(){ NŒUD* debut ; int res ; debut = initialiser() ; res = insererEnTete(&debut, 20) ;

37 3- Insérer dans une liste chaînée avec un nœud factice au début
int insererEnTete(NŒUD* debut, int i){ NŒUD* nouveau; nouveau = (NŒUD*)malloc(sizeof(NŒUD)); if(nouveau != NULL){ nouveau->suivant = debut->suivant; /* Affecter le suivant de debut (NULL) au suivant de nouveau*/ nouveau->info = i; debut->suivant = nouveau; /* Mettre dans le suivant de ‘debut’ le nœud ‘nouveau’*/ return 1; } else { return 0; } void main(){ NŒUD* debut; int res; debut = initialiser(); res = insererEnTete( debut, 20);

38 4- Insérer dans une liste chaînée avec un nœud factice au début et à la fin
La fonction précédente est utilisée puisqu’elle ne dépend que du premier nœud : int insererEnTete( NŒUD* debut, int i){ NŒUD* nouveau ; nouveau = (NŒUD*)malloc(sizeof(NŒUD)); if(nouveau != NULL){ nouveau->suivant = debut->suivant; /*Mettre le suivant de ‘debut’ (z) dans le suivant ‘nouveau’*/ nouveau->info = i; debut->suivant = nouveau; /*le suivant de ‘debut’ recevra nouveau */ return 1; } else { return 0; }

39 Rechercher dans une liste chaînée

40 Rechercher un élément dans une liste avec un nœud factice au début
Nœud* recherchePrecedent( Nœud* debut, int i){ while(debut!=NULL && debut->suivant->info!=i){ /*tant que info du suivant ≠ i*/ debut = debut ->suivant ; /*continuer la recherche*/ } return debut ;

41 Rechercher un élément dans un liste ayant un nœud factice à la fin
Nœud* recherchePrecedent(Nœud* debut, int i){ while(debut->suivant->info!= i){ /*tant que info du suivant ≠ i*/ debut = debut ->suivant; /*continuer la recherche*/ } return debut; Ce n’est plus nécessaire de tester le fin de la liste.

42 Supprimer dans une liste chaînée

43 Supprimer dans une liste chaînée
Supprimer un élément (30 par exemple) : Pour supprimer un élément ⇒ il faut s’arrêter sur le précédent :

44 Rechercher l’élément précédant celui à supprimer sans nœud factice
Nœud* recherchePrecedent(Nœud* debut, int i){ if(debut !=NULL){ /* liste non vide */ if(debut->info == i){ /* si le premier est celui recherché */ return debut; } while(debut->suivant!=NULL && debut->suivant->info!= i){ debut = debut ->suivant;

45 Rechercher l’élément précédant celui à supprimer avec un nœud factice au début
Nœud* recherchePrecedent(Nœud* debut, int i){ while(debut->suivant!=NULL && debut->suivant->info!=i){ debut = debut->suivant ; } return debut ; Ce n’est plus nécessaire de tester le début de la liste.

46 Rechercher l’élément précédant celui à supprimer avec un nœud factice au début et à la fin
Nœud* recherchePrecedent(Nœud* debut, int i){ while(debut->suivant->info != i){ debut = debut->suivant; } return debut; Ce n’est plus nécessaire de tester ni le début, ni la fin de la liste.

47 Supprimer dans une liste chaînée
void supprimerSuivant(NŒUD* n){ Nœud* tmp; tmp = n->suivant; /* mémorise le suivant … */ n->suivant = n->suivant->suivant; /* pour le changer ici … */ free(tmp); /* et le détruire là */ }

48 Rechercher et supprimer dans une liste chaînée ayant un nœud factice au début et à la fin
void supprimer(Nœud* debut, int i){ Nœud* ptrPreced ; ptrPreced = recherchePrécédent(debut, i) ; if(ptrPreced!=NULL && ptrPreced->suivant!=ptrPreced->suivant->suivant){ supprimerSuivant(ptrPreced) ; }

49 Section 4: Les différentes modélisation des listes chainées
Une liste est une structure qui permet de stocker de manière ordonnée des éléments. Une liste est composée de maillons. Un maillon est une structure qui contient un élément à stocker et un pointeur (au sens large) sur le prochain maillon de la liste. structure MAILLON: ELEMENT elt; POINTEUR suiv; fin structure;

50 Modélisation par tableau
Une première façon de modéliser une liste chaînée consiste à allouer à l'avance un certain nombre de maillons. Autrement dit, un tableau de maillons, de taille fixe et allouée à la création de la liste. Ainsi, la liste est formée de cases de tableau. Avec cette modélisation, un pointeur sur un prochain maillon dans la liste sera tout simplement un indice dans le tableau. type POINTEUR: ENTIER; Par convention, un pointeur sur rien aura la valeur VIDE = -1.

51 Modélisation par tableau (Suite)
La figure suivante représente une liste chaînée par un tableau. La liste contient les chaînes de caractères suivantes classées dans cet ordre: "Paris", "Marseille", "Bordeaux", "Lyon", "Toulouse".

52 Modélisation par tableau (Suite)
Voici la structure de données correspondante à une liste chaînée, représentée par un tableau. structure LISTE_TAB: MAILLON tab[NMAX]; ENTIER n; POINTEUR tete; POINTEUR fin; fin structure; ‘NMAX’ est une constante représentant la taille du tableau allouée. ‘n’ est le nombre d'éléments dans la liste. ‘tete’ et ‘fin’ pointent resp. sur la tête (premier élément) et la queue (dernier élément) de la liste.

53 Modélisation par pointeurs
Une seconde façon de modéliser une liste chaînée consiste à allouer dynamiquement les maillons chaque fois que cela est nécessaire. Dans ce cas, un pointeur sur un maillon sera bien ce que l'on appelle couramment un pointeur. type POINTEUR: MAILLON *; Un pointeur sur rien aura donc la valeur VIDE = NIL.

54 Modélisation par pointeurs (Suite)
La figure suivante représente une liste chaînée par des pointeurs. Elle reprend la même liste de l'exemple précédent.

55 Modélisation par pointeurs (Suite)
Voici la structure de données correspondante à une liste chaînée, représentée par pointeurs. structure LISTE_PTR: POINTEUR tete; POINTEUR fin; fin structure; Comme pour la représentation par tableau, tete et fin pointent resp. sur la tête et la queue de la liste.

56 Conclusion La représentation par tableau d'une liste chaînée n'a finalement que peu d'intérêt par rapport à un tableau. Mais, au niveau de l'insertion, un tableau d’une liste chainée est intéressante par rapport au tableau: En effet, pour insérer un élément à n'importe quelle position dans une liste chaînée par tableau, il suffit d'ajouter un élément à la fin du tableau et de faire les liens de chaînage. Alors que dans un tableau, l'insertion d'un élément implique le décalage vers la droite d'un certain nombre d'éléments. Cependant au niveau de la suppression, la liste chaînée par tableau a le même défaut qu'un tableau: il faut décaler un certain nombre d'éléments vers la gauche.

57 Conclusion (Suite) Dans le cas d'une liste chaînée par pointeurs, le défaut constaté au niveau de la suppression d'un élément disparaît. En résumé, une liste chaînée par pointeurs permet une insertion et une suppression rapide des éléments. Cependant, contrairement au tableau, une liste chaînée interdit un accès direct aux éléments (exception faite pour la tête et la queue).

58 Section 6 : Piles et files d’attente
Pile = collection (ensemble) d'éléments gérée en LIFO (Last In First Out) - stack File = collection (ensemble) d'éléments gérée en FIFO (First In First Out) – queue

59 Exemple de pile

60 Exemple de file

61 Les piles Pour expliquer l'algorithme, nous allons utiliser une liste simplement chaînée.

62 Définition La pile est une structure de données, qui permet de stocker les données dans l'ordre LIFO (Last In First Out) - en français Dernier Entré Premier Sorti). La récupération des données sera faite dans l'ordre inverse de leur insertion. Pour l'implémentation, nous utilisons une liste simplement chaînée, présentée sur la verticale. L'insertion se fait toujours au début de la liste, le 1er élément de la liste sera le dernier élément saisi, donc sa position est en haut de la pile. Ce qui nous intéresse c'est que le dernier élément entré, sera le 1er élément récupéré.

63 La construction du prototype d'un élément de la pile
Pour définir un élément de la pile, le type struct sera utilisé. L'élément de la pile contiendra un champ donnee et un pointeur suivant. Le pointeur suivant doit être du même type que l'élément, sinon il ne pourra pas pointer vers l'élément. Le pointeur suivant permettra l'accès au prochain élément. typedef struct ElementListe { char *donnee; struct ElementListe *suivant; }Element;

64 le premier élément. le nombre d'éléments.
Pour permettre des opérations sur la pile, nous allons sauvegarder certains éléments : le premier élément. le nombre d'éléments. Le 1er élément, qui se trouve en haut de la pile, nous permettra de réaliser l'opération de récupération des données situées en haut de la pile. Pour réaliser cela, une autre structure sera utilisée. Voici sa composition: typedef struct ListeRepere{ Element *debut; int taille; }Pile; Le pointeur debut contiendra l'adresse du premier élément de la liste. La variable taille contient le nombre d'éléments.

65 Observation : Nous n'utilisons pas cette fois un pointeur fin (voir les listes simplement chaînées), puisque nous n'en avons pas besoin, vu que nous ne travaillons qu'au début de la liste. Quelque soit la position dans la liste, le pointeur debut pointe toujours vers le 1er élément, qui sera en haut de la pile. Le champ taille contiendra le nombre d'éléments de la pile, quelque soit l'opération effectuée sur la pile.

66 Opérations sur les piles

67 Initialisation Prototype de la fonction : void initialisation(Pile *tas); Cette opération doit être faite avant toute autre opération sur la pile. Elle initialise le pointeur debut avec le pointeur NULL et la taille avec la valeur 0. La fonction void initialisation(Pile * tas){ tas->debut = NULL; tas->taille = 0; }

68 Insertion d'un élément dans la pile
Voici l'algorithme d'insertion et de sauvegarde des éléments : déclaration de l'élément à insérer. allocation de la mémoire pour le nouvel élément. remplir le contenu du champ de données. mettre à jour le pointeur debut vers le 1er élément (le haut de la pile). mettre à jour la taille de la pile.

69 Prototype de la fonction :
int empiler (Pile *tas, char *donnee); Cette image montre le début de l'insertion, donc la liste a la taille 1 après l'insertion. La caractéristique de la pile n'est pas très bien mise en évidence avec un seul élément, puisque c'est le seul à récupérer.

70 Cette image nous permet d'observer le comportement de la pile.
A retenir: l'insertion se fait toujours en haut de la pile (au début de la liste).

71 La fonction /* empiler (ajouter) un élément dans la pile */
int empiler(Pile * tas, char *donnee){ Element *nouveau_element; if((nouveau_element=(Element*)malloc(sizeof(Element)))==NULL) return -1; if((nouveau_element->donnee=(char*)malloc(50*sizeof(char)))==NULL) strcpy(nouveau_element->donnee, donnee); nouveau_element->suivant = tas->debut; tas->debut = nouveau_element; tas->taille++; }

72 Ôter un élément de la pile
Pour supprimer (ôter ou dépiler) un élément de la pile, il faut tout simplement supprimer l'élément vers lequel pointe le pointeur debut. Cette opération ne permet pas de récupérer la donnée en haut de la pile, mais seulement de la supprimer. Prototype de la fonction : int depiler(Pile *tas); La fonction renvoie -1 en cas d'échec sinon elle renvoie 0.

73 Les étapes : le pointeur supp_elem contiendra l'adresse du 1er élément. le pointeur debut pointera vers le 2ème élément (après la suppression du 1er élément, le 2ème sera en haut de la pile). la taille de la pile sera décrémentée d'un élément.

74 La fonction int depiler (Pile * tas){ Element *supp_element;
if (tas->taille == 0) return -1; supp_element = tas->debut; tas->debut = tas->debut->suivant; free (supp_element->donnee); free (supp_element); tas->taille--; return 0; }

75 Affichage de la pile Pour afficher la pile entière, il faut se positionner au début de la pile (le pointeur debut le permettra). Ensuite en utilisant le pointeur suivant de chaque élément, la pile est parcourue du 1er vers le dernier élément. La condition d'arrêt est donnée par la taille de la pile. /* affichage de la pile */ void affiche (Pile * tas){ Element *courant; int i; courant = tas->debut; for(i=0;i<tas->taille;++i){ printf("\t\t%s\n", courant->donnee); courant = courant->suivant; }

76 Récupération de la donnée en haut de la pile
Pour récupérer la donnée en haut de la pile sans la supprimer, j'ai utilisé une macro. La macro lit les données en haut de la pile en utilisant le pointeur debut. #define pile_donnee(tas) tas->debut->donnee

77 Les files - Premier Entré Premier Sorti

78 Définition La file est une structure de données qui permet de stocker les données dans l'ordre FIFO (First In First Out) - en français Premier Entré Premier Sorti). La récupération des données sera faite dans l'ordre d'insertion. Pour l'implémentation, j'ai choisi une liste simplement chaînée. L'insertion dans la file se fera dans l'ordre normal, le 1er élément de la file sera le premier élément saisi, donc sa position est au début de la file.

79 La construction du prototype d'un élément de la file
Pour définir un élément de la file, le type struct sera utilisé. L'élément de la file contiendra un champ donnee et un pointeur suivant. Le pointeur suivant doit être du même type que l'élément, sinon il ne pourra pas pointer vers l'élément. Le pointeur suivant permettra l'accès vers le prochain élément. typedef struct ElementListe { char *donnee; struct ElementListe *suivant; }Element;

80 Pour avoir le contrôle de la file, il est préférable de sauvegarder certains éléments : le premier élément, le dernier élément et le nombre d'éléments. Pour réaliser cela, une autre structure sera utilisée. Voici sa composition: typedef struct ListeRepere{ Element *debut; Element *fin; int taille; } File;

81 Opérations sur les files

82 Initialisation Prototype de la fonction : void initialisation(File * suite); Cette opération doit être faite avant toute autre opération sur la file. Elle initialise le pointeur debut et le pointeur fin avec le pointeur NULL et la taille avec la valeur 0. La fonction: void initialisation(File * suite){ suite->debut = NULL; suite->fin = NULL; suite->taille = 0; }

83 Insertion d'un élément dans la file
Voici l'algorithme d'insertion et de sauvegarde des éléments: déclaration d'élément(s) à insérer. allocation de la mémoire pour le nouvel élément. remplir le contenu du champ de données. mettre à jour le pointeur debut vers le 1er élément (le début de le file). mettre à jour le pointeur fin (ça nous servira pour l'insertion vers la fin de la file). mettre à jour la taille de la file. Prototype de la fonction: int enfiler (File* suite, Element* courant, char *donnee);

84 La 1ère image affiche le début de l'insertion, donc la liste a la taille 1 après l'insertion.

85 Dans la file, l'élément à récupérer est le 1er entré
Dans la file, l'élément à récupérer est le 1er entré. Pour cela, l'insertion se fera toujours à la fin de la file. Il s'agit de l'ordre normal de l'insertion (1er , 2ème , 3ème, etc. ).

86 int enfiler (File * suite, Element * courant, char *donnee){
Element *nouveau_element; if ((nouveau_element = (Element *) malloc (sizeof (Element))) == NULL) return -1; if((nouveau_element->donnee = (char *) malloc (50 * sizeof (char)))== NULL) strcpy (nouveau_element->donnee, donnee);   if(courant == NULL){ if(suite->taille == 0) suite->fin = nouveau_element; nouveau_element->suivant = suite->debut; suite->debut = nouveau_element; }else { if(courant->suivant == NULL) nouveau_element->suivant = courant->suivant; courant->suivant = nouveau_element; } suite->taille++; return 0;

87 Oter un élément de la file
Pour supprimer (ôter) l'élément de la file, il faut tout simplement supprimer l'élément vers lequel pointe le pointeur debut. Cette opération ne permet pas de récupérer la donnée au début de la file (la première donnée), mais seulement de la supprimer. Prototype de la fonction : int defiler (File * suite); La fonction renvoie -1 en cas d'échec sinon elle renvoie 0.

88 Etapes : le pointeur supp_elem contiendra l'adresse du 1er élément. le pointeur debut pointera vers le 2ème élément (après la suppression du 1er élément, le 2ème sera au début de la file). la taille de la file sera décrémentée d'un élément.

89 La fonction int defiler (File * suite){ Element *supp_element;
if (suite->taille == 0) return -1; supp_element = suite->debut; suite->debut = suite->debut->suivant; free (supp_element->donnee); free (supp_element); suite->taille--; return 0; }

90 Affichage de la file Pour afficher la file entière, il faut se positionner au début de la file (le pointeur debut le permettra). Ensuite en utilisant le pointeur suivant de chaque élément, la file est parcourue du 1er vers le dernier élément. La condition d'arrêt est donnée par la taille de la file. La fonction void affiche(File *suite){ Element *courant; int i; courant = suite->debut; for(i=0;i<suite->taille;++i){ printf(" %s ", courant->donnee); courant = courant->suivant; }

91 Récupération de la donnée au début de la file
Pour récupérer la donnée au début de la file sans la supprimer, j'ai utilisé une macro. La macro lit les données au début de la file en utilisant le pointeur debut. #define file_donnee(suite) suite->debut->donnee

92 Section 7: La récursivité
Une procédure ou une fonction est dite récursive si elle fait appel à elle même, directement ou indirectement.

93 Appel récursif --> Boucle
Pour éviter les boucles infinies, les appels récursifs doivent être conditionnels (à l'intérieur d'un SI ou un SINON ou un Tant Que, etc). Exemple de la fonction factorielle: n!= n*(n-1)*(n-2)*…*1 pour n>0 et 0!=1 n!= n*(n-1)! pour n>0 et n!=1 pour n=0 Fact(n:entier):entier SI n=0 Fact:= /* cas particulier */ SINON Fact:=n*Fact(n-1) /* cas général */ FSI

94 Exemple 1 Déroulement pour n=4 : 1- 4! = 4 * 3! 2- 3! = 3 * 2!
1- 4! = 4 * 3! 2- 3! = 3 * 2! 3- 2! = 2 * 1! 4- 1! = 1 * 0! 5- 0! = 1 (cas particulier) l'exécution i attend la terminaison de l'exécution i-1 pour continuer son traitement.

95 Exemple de la fonction Fibonacci (attention
Exemple de la fonction Fibonacci (attention! solution très inefficace mais simple) U0 = 1, U1 = 1, Un = Un-1 + Un-2 pour n > 1 Fib( n:entier ):entier SI n < 2 Fib := 1 SINON Fib := Fib( n-1 ) + Fib( n-2 ) FSI

96 Déroulement :

97 Exemple de la recherche dichotomique
Recherche binaire dans un tableau ordonnée. Un élément x se trouve soit dans la 1ère moitié ou bien dans la 2ème moitié, pour le savoir on le compare avec la valeur du milieu.

98 Algorithme de recherche
Rech(x:Tqlq; bi, bs:entier):entier /* l'indice de x dans T. ou bien -1 */ /* bi : borne inf. bs : borne sup. */ SI (bi > bs) Rech := /* cas particulier */ SINON milieu := (bi+bs) div 2; SI (x = T[milieu]) Rech := milieu /* cas particulier */ SI (x < T[milieu]) Rech( x , bi , milieu-1 ) /* 1ere moitié */ Rech( x , milieu+1 , bs ) /* 2e moitié */ FSI

99 Remarques: Dans un module récursif (procédure ou fonction), les paramètres doivent être clairement spécifiés. Dans le corps du module, il doit y avoir: un ou plusieurs cas particuliers: ce sont les cas simples qui ne nécessitent pas d'appels récursifs. un ou plusieurs cas généraux: ce sont les cas complexes qui sont résolus par des appels récursifs. L'appel récursif d'un cas général doit toujours mener vers un des cas particuliers.

100 Schéma général d'une décomposition récursive

101 Exemple du tri d'un tableau

102 Algorithme de tri par fusion
Tri_Fusion( T:Tab; n:entier; var S:Tab ) SI (n = 1 ) S[1] := T[1] SINON /* récupérer les 2 moitiés T1 et T2 */ POUR i=1,n SI (i <= n/2) T1[i]:=T[i] SINON T2[ i-n/2 ] := T[ i ] FP; n1 := n/2; n2 := n - n1; /* Trier T1 et T2 en utilisant des appels récursifs */ Tri_Fusion( T1, n1, S1 ); Tri_Fusion( T2, n2 , S2); /* Fusionner les deux sous-solutions S1 et S2 */ Fusion( S1, n1, S2, n2, S ); FSI

103 Chapitre 2 : Complexité

104 Section 1: Notion d’algorithme et d’espace mémoire
L’objectif initial de l’informatique est de fournir des algorithmes permettant de résoudre un problème donné. Algorithme : ensemble de règles opératoires dont l’application permet de résoudre un problème au moyen d’un nombre fini d’opérations. → problème : temps d’exécution des traitements à minimiser. Espace mémoire : taille des données utilisées dans le traitement et la représentation du problème. → problème : espace mémoire occupé à minimiser. Les notions de traitements et d’espace mémoire sont liés. Ces deux critères doivent servir de guide au choix d'une représentation de donnée.

105 2- Approche intuitive de la complexité
Exemple de problème: le voyageur de commerce. Ce voyageur doit trouver le chemin le plus court pour visiter tous ses clients, localisés chacun dans une ville différente et en passant une et une seule fois par chaque ville. On peut imaginer un algorithme naïf : 1. on envisage tous les chemins possibles, 2. on calcule la longueur pour chacun d'eux, 3. on conserve celui donnant la plus petite longueur.

106 Approche intuitive de la complexité (Suite)
Cette méthode « brutale » conduit à calculer un nombre de chemin «exponentiel» par rapport au nombre de villes. Il existe des algorithmes plus « fins » permettant de réduire le nombre de chemins à calculer et l’espace mémoire utilisé. Mais on ne connaît pas d'algorithme exact qui ait une complexité (temps de résolution) acceptable pour ce problème. Pour les problèmes qui sont aussi «difficile», on cherche des méthodes approchées, appelées heuristiques.

107 3- Complexité en temps et mémoire
Si l’on souhaite comparer les performances des algorithmes, on peut considérer une mesure basée sur leur temps d’exécution. Cette mesure est appelée la complexité en temps de l’algorithme. On utilise la notion dite « de Landau » qui traite l’ordre de grandeur du nombre d’opérations effectuées par un algorithme donné. → On utilise la notation « O » qui donne une majoration de l’ordre de grandeur du nombre d’opérations.

108 3- Complexité en temps et mémoire (Suite)
Pour déterminer cette majoration, il faut : Connaître la taille n de la donnée en entrée du problème (ex. nombre de données à traiter, le degré d’un polynôme, taille d’un fichier, le codage d’un entier, le nombre de sommets d’un graphe, etc. ) Déterminer les opérations fondamentales qui interviennent dans ces algorithmes. Ainsi, les temps d'exécution seront directement proportionnels au nombre de ces opérations.

109 Exemple en C: calculer la puissance n-ième d’un entier (n étant positif).
int puissance (int a, int n) { int i, x ; x=1 ; for (i = 0; i < n; i=i+1) // boucle de n itérations x = x * a ; // opération fondamentale de multiplication return x ; } La complexité de cet algorithme est en temps linéaire O(n). Remarque 1: les performances de la machine n’interviennent pas directement dans l’ordre de grandeur de la complexité. Remarque 2: la théorie de la complexité a pour but de donner un contenu formel à la notion intuitive de difficulté de résolution d’un problème.

110 Type de complexité algorithmique
On considère désormais un algorithme dont le temps maximal d’exécution pour une donnée de taille n en entrée est noté T(n). Chercher la complexité au pire –dans la situation la plus défavorable– c’est exactement exprimer T(n) en général en notation O. Par exemple : T(n)=O(1), temps constant : temps d’exécution indépendant de la taille des données à traiter. T(n)= O(log(n)), temps logarithmique : on rencontre généralement une telle complexité lorsque l’algorithme casse un gros problème en plusieurs petits, de sorte que la résolution d’un seul de ces problèmes conduit à la solution du problème initial. T(n)=O(n), temps linéaire : cette complexité est généralement obtenue lorsqu’un travail en temps constant est effectué sur chaque donnée en entrée.

111 Type de complexité algorithmique (Suite)
T(n) = O(n.log(n)) : l’algorithme scinde le problème en plusieurs sous-problèmes plus petits qui sont résolus de manière indépendante. La résolution de l’ensemble de ces problèmes plus petits apporte la solution du problème initial. T(n) = O(n²), temps quadratique : apparaît notamment lorsque l’algorithme envisage toutes les paires de données parmi les n entrées (ex. deux boucles imbriquées) Remarque : O(n3) temps cubique T(n) = O(2n), temps exponentiel : souvent le résultat de recherche brutale d’une solution.

112 Complexité en place mémoire
L'analyse de la complexité en place mémoire revient à évaluer, en fonction de la taille de la donnée, la place mémoire nécessaire pour l'exécution de l'algorithme, en dehors de l'espace occupé par les instructions du programme et des données. Conclusion : Il s'agit donc d’obtenir le meilleur compromis espace-temps.

113 Chapitre 3. Les Arbres

114 1- Définition de l’arborescence
Une arborescence est une collection de nœuds reliés entre eux par des arcs. La collection peut être vide, c a d l’arborescence ne possède ni nœuds ni arcs. Si elle n’est pas vide, elle contient un nœud particulier r appelé racine et une séquence de (sous-) arborescences A1;A2;…;Ak, de racines respectives r1; r2;…; rk. La racine r est reliée à chacun des ri; i = 1;…;k, par un arc orienté de r vers ri.

115 2- Vocabulaire et propriétés
Cette arborescence est composée d’une racine, le nœud a, et de trois sous-arborescences de racines b, c et d. Les arcs sont implicitement orientés du haut vers le bas. Les fils du nœud g sont les nœuds j, k l, m et n. Le père du nœud j est le nœud g. Les feuilles de cette arborescence sont les nœuds h, i, f, c, j, o, p, q, l, m, n. Les ancêtres de k sont k, g, d et a. Les descendants de b sont b, e, f, h et i. Les hauteurs respectives des nœuds b, c, d sont 2, 0, 3. La hauteur de l’arborescence est 4.

116 La racine de l’arborescence est le seul nœud sans père.
Les ri sont les fils de r et r est le père de tous les ri. La racine de l’arborescence est le seul nœud sans père. Les nœuds qui n’ont pas de fils sont appelés feuilles de l’arborescence. Les nœuds qui ont au moins un fils sont appelés nœuds internes. Tous les arcs sont orientés de la racine vers les feuilles. Une séquence de nœuds partant d’un nœud a en suivant l’orientation des arcs jusqu’au nœud p s’appelle un chemin de a à p. Tous les nœuds du chemin de a à p sont des ancêtres de p et des descendants de a. On peut donc dire que les nœuds d’une sous-arborescence de racine x sont les descendants de x (y compris x).

117 La hauteur de toute feuille est donc 0.
La longueur d’un chemin a1;a2;…;ak est le nombre d’arcs sur le chemin, c a d k-1. Il existe exactement un chemin de la racine à chaque nœud de l’arborescence . La hauteur d’un nœud est la longueur du plus long chemin partant de ce nœud et aboutissant à une feuille. La hauteur de toute feuille est donc 0. La hauteur de l’arborescence est la hauteur de sa racine. L’arité d’un nœud est le nombre de ses fils. Une arborescence binaire, ou simplement arbre binaire, est une arborescence dans lequel tout nœud possède au plus deux fils.

118 Un arbre binaire complet est un arbre binaire dont tous les nœuds internes ont deux fils, et dont tous les chemins de la racine aux feuilles sont de longueur égales. FIG: A gauche, un arbre binaire complet donc parfaitement équilibré. À droite, un arbre binaire possédant le même nombre de nœuds, mais complètement déséquilibré.

119 Le nœud La déclaration des types constituant le nœud est la suivante:
typedef struct s_noeud *p_noeud_t; typedef struct s_noeud { int valeur; p_noeud_t fils_gauche; p_noeud_t fils_droit; p_noeud_t pere; /* Optionnellement */ } noeud_t; Le pointeur vers le nœud père est optionnel. Il sert simplement à simplifier l’écriture et la complexité de certains algorithmes.

120 L’arbre La déclaration des types constituant l’arbre est la suivante :
typedef p_noeud_t arbre_t; typedef arbre_t *p_arbre_t;

121 3- Usages des arborescences
Ils sont multiples et ils capturent l’idée d’hiérarchie. Exemples : découpage d’un livre en parties, chapitres, sections, paragraphes…, hiérarchies de fichiers, expressions arithmétiques, etc.

122 4- Parcours d’arborescences
2 types de parcours : Parcours en profondeur et parcours en largeur. 4.1. Parcours en profondeur Le parcours en profondeur d’abord consiste à explorer un arbre de haut en bas puis de gauche à droite. On distingue trois types de parcours en profondeur : le parcours préfixe traite le nœud courant puis le sous-arbre gauche et enfin le sous-arbre droit ; le parcours infixe traite le sous-arbre gauche, puis le nœud courant et enfin le sous-arbre droit ; le parcours postfixe traite le sous-arbre gauche, le sous-arbre droit et enfin le nœud courant.

123 Exemple : affichage de l’arbre
Les 3 versions du parcours en profondeur récursif sont présentées ci-après. Pour chaque version, la fonction disp affiche le contenu de l’arbre sous la forme d’une ligne de nombres entiers séparés par des espaces. Parcours préfixe : Le traitement effectué sur les nœuds par la fonction disp est l’affichage du contenu du nœud. Dans ce premier parcours préfixe, le traitement du nœud courant a lieu avant les deux appels récursifs. void disp(arbre_t arbre){ p_noeud_t p_noeud = arbre; if (p_noeud){ printf("%d ", p_noeud->valeur); disp(p_noeud->fils_gauche); disp(p_noeud->fils_droit); }

124 Parcours infixe Parcours postfixe void disp(arbre_t arbre){
p_noeud_t p_noeud = arbre; if (p_noeud){ disp(p_noeud->fils_gauche); printf("%d ", p_noeud->valeur); disp(p_noeud->fils_droit); } Parcours postfixe

125 Exemple 1. Parcours préfixe : r,a,c,h,d, i , j ,l,b,e,k, f .
2. Parcours postfixe : h,c, i ,l, j ,d,a,k,e, f ,b, r . 3. Parcours infixe : c,h,a, i ,d,l, j , r,k,e,b, f .

126 Exercice Écrire les sommets de l’arbre ci-dessous pour chacun des ordres postfixe, préfixe, infixe : Pour le parcours infixe, on ajoute la convention suivante : on ajoute une parenthèse ouvrante à chaque fois qu’on entre dans un sous-arbre et on ajoute une parenthèse fermante lorsqu’on quitte ce sous-arbre.

127 4.2. Parcours en largeur Le parcours en largeur d’abord consiste à explorer un arbre de gauche à droite puis de haut en bas.


Télécharger ppt "Les structures de données"

Présentations similaires


Annonces Google