1. FICHIERS Stocker les données dans des fichiers pour pouvoir les utiliser plus tard. C distingue 2 catégories de fichiers : fichiers standards et fichiers systèmes. On distingue : fichiers de textes et fichiers binaires. Fichiers binaires : fichiers de nombres, ils ne sont pas listables. Fichiers textes ou ASCII sont listables, le dernier octet de ces fichiers est EOF. La plupart des fonctions ou primitives permettant la manipulation des fichiers sont rangés dans la bibliothèque standard string.h. 1 1.1. Définition et propriétés Un fichier est un ensemble structuré de données stocké sur un support externe. Un fichier séquentiel est représenté séquentiellement par la suite ordonnée de ses éléments, notée : <x1, x2, x3,…,xn>. Propriétés de fichiers séquentiels : - les fichiers se trouvent en état d’écriture ou bien en état de lecture. - On accède uniquement à un seul enregistrement se trouvant en face de la tête de L/ E. - Après chaque accès, la tête de L/ E est déplacée derrière la donnée lue en dernier lieu. - Un fichier peut être vide. Un sous-fichier est la restriction d’un fichier f à un intervalle P (suite d’éléments consécutifs). P est un ensemble fini totalement ordonné. Ex : f = <5, 7, 59, 67, -1, 0, 18> alors <5, 7, 59> et <67, -1, 0, 18> sont des sousfichiers de f. Par contre, <5, 59, 18> n’est pas un sous-fichier. Tout fichier peut être schématisé par un ruban défilant devant une tête de L/ E. Elément accessible ou élément courant est l’ élément en face de la tête de L/ E. Un fichier étant une suite finie, on introduit une marque sur le ruban notée -| (fin de fichier). 2 tête de lecture/ écriture sens de défilement x1 x2 x3 x4 …. xn -| -----f- -----------------------f+---------------- L’élément courant (x3) permet, par définition, de décomposer un fichier en deux sous-fichiers : f = f- || f+ Où f- est le sous-fichier déjà lu et f+ est le sous-fichier qui reste à lire. L’élément courant est le premier élément de f+. Dans cet exemple : f- = <x1, x2>, f+ = <x3,…,xn>, l’élément courant est x4. Notation f— est définie par : f- = f--|| val. f— est le fichier f- avant exécution de l’action : lire(f,val)3 1.2. Actions primitives d’accès Elles donnent un modèle du comportement dynamique d’un fichier. 1.2.1. Déclaration Etapes pour traiter un fichier : - déclarer un pointeur de type FILE*, - affecter l’adresse retournée par fopen à ce pointeur, - employer le pointeur à la place du nom du fichier dans toutes les instructions de lecture ou d’écriture, -libérer le pointeur à la fin du traitement par fclose. 1.2.2. Primitives d’accès Avant de créer ou de lire un fichier, nous devons l’ouvrir. Après avoir terminé la manipulation du fichier, nous devons le fermer. 4 a) Ouvrir un fichier séquentiel FILE * fopen(char *nom, char *mode) ; On passe 2 chaînes de caractères : nom : celui figurant sur le disque, par exemple toto.dat. mode : r lecture seule ; w écriture seule, destruction de l’ancienne si elle existe. a+ lecture et écriture d’un fichier existant (mise à jour), pas de création d’une nouvelle version, le pointeur est positionné à la fin du fichier. b) Créer un fichier séquentiel int fprintf(FILE *f, char * format, liste d’expression) ; Retourne EOF en cas d’erreur. exemple : fprintf(f, %s\n, il fait beau) ; elle crée un fichier. Avant de lire un fichier on doit l’ouvrir en lecture. 5 c) Lire un fichier séquentiel int fgetc(FILE *f) ; lit un caractère mais retourne un entier ; retourne EOF si erreur, le pointeur avance d’une case. Exemple : char c ; c = (char)fgetc(f) ; int fscanf(FILE *f, char *format, liste d’adresses ) ; Avant de quitter un fichier on doit le fermer. d) Fermer un fichier séquentiel fclose(f) ; f est un pointeur du type FILE* Exemple : fclose(f) ; 6 e) Détecter la fin du fichier Les fonctions d’ouverture, de lecture et d’écriture de fichier donnent une valeur à FEOF qui détecte la fin d’un fichier. La fonction feof(f) retourne la valeur true, si la tête de lecture du fichier est arrivé à la fin du fichier ; sinon elle délivre la valeur false. <f> est un pointeur du type FILE* qui est relié au nom du fichier à lire. Exemple : une boucle typique pour lire les enregistrements d’un fichier séquentiel référencé par un pointeur f peut avoir la forme suivante : FILE* f; char nom[25], prenom[15]; f=fopen(“toto.dat”, “r”); … while( !feof(f)) { fscanf(f, %s %s\n , nom, prenom) ; traiter(nom, prenom); … 7 } CH_3_1.C : Le C lit et affiche le fichier C:\AUTOEXEC. BAT en le parcourant caractère par caractère. #include<stdio.h> #include<stdlib.h> main() { FILE *f ; // char car ; … f = fopen(C:\\AUTOEXEC.BAT, r) ; if(!f) { printf(Impossible d’ouvrir le fichier\n) ; exit(-1) ; } while( !feof(f))putchar(fgetc(f)) ; /* ou { fscanf(f,”%c”, &car) ; fclose(f) ; printf("%c", car) ; } } */ 8 Tableau 1 : Résumé sur fichier caractère et texte Action C ouverture en écriture f = fopen(nom, w) ouverture en lecture f = fopen(nom,r) fermeture fclose(f) fct finfichier feof(f) écriture fprintf(f,…, expression) fputc lecture fscanf(f,…, adresse) c=getc(f) 9 e) Détruire un fichier int remove(char *nom) ; retourne 0 si la fermeture s’est bien passée. Exemple : remove(c:\\toto.dat) ; f) Renommer un fichier int rename(char *oldName, char *newName) ; retourne 0 si la fermeture s’est bien passée. g) positionner le pointeur au début du fichier void rewind(FILE * f) ; 10 1.3. Algorithmes traitant un seul fichier 1.3.1. Somme des éléments d’un fichier Ecrire une fonction sommeElement qui calcule la somme des éléments d’un fichier. int sommeElement (FILE* f) { int som, val ; f=fopen("elem.dat", "r") ; fscanf(f, "%d", &som); while(!feof(f)) { fscanf(f, "%d", &val); som += val; } return som; } 11 Remarque : cas d’un fichier vide, on doit alors écrire un autre plus général à condition de préciser la convention adoptée dans ce cas. Convention : la somme des éléments d’un fichier vide est égale à zéro. On exécute, à l’initialisation, les actions suivantes : f=fopen("elem.dat", "r") ; s=0; La suite des algorithmes est inchangée. 1.3.2. Recherche de la valeur maximale contenue dans un fichier Soit un fichier f non vide, tel que l’ensemble des valeurs soit muni d’une relation d’ordre total. On cherche une valeur maxf et telle que yf, on ait maxy. Une telle valeur est dite maximale. L’en-tête de la fonction est le suivant : int maximum(FILE * f) 12 int maximum(FILE * f) { int max, val; f=fopen("max.dat", "r"); fscanf(f, "%d", &max); while(!feof(f)) { fscanf(f, "%d", &val); if(max<val)max=val; } return max; } Pour les algorithmes précédents, l’itération est toujours de la forme : f=fopen("max.dat", "r"); … while(!feof(f)) { fscanf(f, "%d", &val); traiter(val); } Où traiter est une fonction quelconque agissant ou non sur le contenu de val. A chaque exécution de l’action fscanf(f, "%d", &val); le nombre d’éléments de f+ diminue de 1. 13 1.3.3. Accès à un élément d’un fichier Ce problème est habituellement formulé de 2 manières : Soit on connaît le rang k de l’élément et on recherche alors le kième élément du fichier, Soit on connaît la valeur de l’élément que l’on cherche et on l’on recherche le premier élément qui correspond à cette valeur c’est-à-dire la première occurrence. On parle alors de recherche ou d’accès : Par position dans le premier cas, Par valeur ou associatif dans le second. 1.3.3.1. Accès par position (accès au kième élément) L’en-tête de la fonction est int accesk(FILE* f, int k, booleen *trouve) On peut décomposer le fichier en deux f1k-1 et fkn, d’où : f = f1k-1 || fkn Pour que le kième existe, le sous-fichier fkn ne doit pas être vide (fkn <>). Dans ce cas, et si on a effectué la lecture des k-1 premiers éléments, f+ commence par le kième élément du fichier f. 14 L’algorithme est alors composé de 2 parties : Parcours des k-1 premiers éléments de f (f-= f1k-1) Ensuite, suivant que le fichier f+ est vide ou non, on aura les résultats suivants : ** f+ est vide, trouve prendra la valeur faux (0) et valk sera indéfinie. ** f+ n’est pas vide, trouve prendra la valeur vrai et valk contiendra la valeur du kième élément en effectuant l’action fscanf( f, "%d", &valk); a) construction de f- = f1k-1 f=fopen("fic.dat", "r"); i=1; while((i<k) && (!feof(f))) { fscanf(f, "%d", &val); i+=1; } On a obtenu la 1ière partie de l’algorithme. 15 b) Suite de l’algorithme A la sortie whileon a ((i < k), (feof(f)) d’après la loi de De Morgan (i k) v feof(f). i > k signifie que k 0. En effet, i est initialisé avec la valeur 1. D’autre part i augmente de 1 à l’intérieur de while. Si k>0, la négation de i<k ne peut être que i=k. On ne peut avoir i>k sans être passé au préalable par la valeur de i égale à celle de k. Donc i>k ne peut jamais se produire. Nous allons étudier tous les cas possibles de l’assertion (i==k)||feof(f). 16 • i == k, feof(f) On est à la fin de fichier, et on a trouvé que le rang de l’élément était égal à k. Le fichier a donc k-1 éléments, le sous-fichier f+ est vide et le kième élément n’existe pas : f = f1k-1, trouve prend la valeur faux et valk est indéfinie. •i == k, !feof(f) signifie qu’on a trouvé le kième élément sans atteindre la fin du fichier. On a donc : (f- = f1i-1, i = k) f- = f1k-1 Il s’ensuit que f+ = f1n avec k n ; fscanf(f, "%d", &valk) permet à valk de prendre la valeur du kième élément et trouve doit prendre la valeur true. •i < k, feof(f) On a parcouru tous les éléments du fichier sans trouver le kième élément. f- = f1n, f+ est vide, trouve prend la valeur false et valk est indéfinie. •i < k, !feof(f) Ce cas ne peut jamais se produire car l’expression (i=k)||((feof(f)) est fausse et n’est pas donc une assertion. On peut résumer ces 4 cas dans un tableau appelé tableau de sortie. 17 Tableau 2 : tableau de sortie de tantque i == k vrai vrai faux (i<k) faux (i<k) feof(f) vrai faux vrai faux résultat trouve = false trouve=true; fscanf(f, "%d", &valk) trouve = false impossible (à cause de while) En examinant les 3 premières lignes du tableau, on constate que la valeur de trouve est identique è celle de !feof(f). Il faut donc continuer l’algorithme en utilisant un test sur la valeur du prédicat. Par contre, utiliser un test sur la valeur de i==k, entraîne une erreur dans le cas de la 1ière ligne. On aurait donc écrit un algorithme qui aurait fonctionné dans la plupart des cas, sauf si par hasard on avait rencontré le cas d’un fichier contenant k-1 éléments. D’où l’intérêt de la construction et de l’analyse du tableau de sortie qui permet de compléter l’algorithme. 18 On poursuit l’algorithme en écrivant : if(!feof(f)) { trouve=true; fscanf(f, "%d", &valk); return valk; } else trouve = false; ou encore trouve = !feof(f); if(trouve) { fscanf(f, "%d", &valk) ; return valk; } Une autre solution consiste à initialiser trouve à false au début de l’algorithme, il suffit alors de terminer l’algorithme avec : trouve=false; if(!feof(f)) { trouve=true; fscanf(f, "%d", &valk); return valk; } C’est cette dernière solution que nous utilisons fréquemment, car elle permet d’alléger l’algorithme en évitant l’écriture de else. 19 Algorithme d’accès au kième élément. int accesk(FILE *f, int k, booléen *trouve) { int nomb, valk ; int i; f=fopen("fic.dat", "r"); *trouve = false; i=1; while((i<k) && (!feof(f)) { fscanf(f, "%d", &nomb); i+=1; } if(!feof(f)) { *trouve = true; fscanf(f, "%d", &valk); return valk; } } 20 1.3.3.2. Accès associatif Soit un fichier f et une valeur val de même type que les éléments de f. On veut savoir si la valeur contenue dans val est présente dans le fichier f (= f1n). valf signifie que i [1..n], xi = val. booléen acces(FILE *f, int val) { int nomb ; booléen egal; f=fopen("fic.dat", "r"); egal=false; while(!feof(f) && !egal) { fscanf(f, "%d", &nomb); if(nomb==val) egal=true; } return egal; } 21 Tableau 3 : Tableau de sortie fdf(f) vrai vrai faux faux egal vrai faux vrai faux Résultat : val f trouve = true (val = Df) trouve = false trouve = true impossible (while) On constate que trouve a la même valeur que egal. 1.3.4. Fichier ordonné (fichier trié) Si tous les éléments consécutifs d’un fichier vérifient la relation d’ordre (xi xi+1), on dit que le fichier est trié par ordre croissant. On appellera fichier trié (ou ordonné) un fichier ordonné par ordre croissant. On admettra que le fichier vide et que le fichier ne contenant qu’un seul élément sont triés. 22 En d’autres termes, la définition d’un fichier trié : un fichier vide est trié, un fichier comportant un seul élément est trié, soit f = <x1, x2, …,xn>, avec n>1, le fichier f est trié si i [1..n-1], la relation xi xi+1 est vraie. On peut donner une définition récursive d’un fichier trié : un fichier vide est trié, un fichier contenant un seul élément est trié, (f1i trié, xi xi+1) f1i+1 trié pour i [1..n-1]. 1.3.4.1. Algorithme vérifiant qu’un fichier est trié. Ecrire un algorithme qui vérifie si un fichier est trié ou non. L’en-tête est le suivant : booléen trie(FILE *f). 23 Algorithme vérifiant qu’un fichier est trié. booléen trie (FILE* f) { int precedent, nomb; f=fopen("fic.dat", "r"); if(feof(f)) return true; else { fscanf(f, "%d", &precedent); nomb=precedent; while(!feof(f) && (precedent nomb)) { precedent=nomb; fscanf(f, "%d", &nomb) ; } return precedentnomb; } } 24 1.3.4.2. Algorithme d’accès à un élément dans un fichier trié En général, l’accès associatif dans un fichier ordonné est plus rapide que dans le cas d’un fichier non ordonné. On cherche à écrire une fonction dont l’en-tête est : booléen accest(FILE *f, int val) On peut toujours décomposer un fichier f trié et non vide en 2 sousfichiers f’ et f’’ tel que : f = f’ || f’’, f’< val f’’. Remarque Si f’ est vide, la relation s’écrit : val fSi f’’ est vide, la relation s’écrit f’ < val. On contrôle l’itération par une variable booléenne infer définie par : (infer, f- < val) || (!infer, Df- val). 25 Algorithme booléen accest(FILE *f, int val) { int nomb; boléen infer; f=fopen("fic.dat", "r"); infer=true; while(!feof(f) && infer) { fscanf(f, "%d", &nomb); if(nomb>=val)infer=false; } if(nomb==val)return true; else return false; } 26 2. Récursivité et structures de données avancées S’initialiser à la programmation récursive. Construire des algorithmes classiques et fondamentaux sur des structures de données plus avancées telllistes chaînées, les piles, les files d’attente, les tables et les arbres. 2.1. Listes linéaires chaînées es que les 2.1.1. Définition Une cellule : un couple composé d’une information et d’une adresse. Une liste linéaire chaînée ou liste chaînée est un ensemble de cellules chaînées entre elles. C’est l’adresse de la première de ces cellules qui détermine la liste. Cette adresse se trouve dans une variable que nous appellerons souvent liste. Par convention, on notera liste+ la suite des éléments contenus dans la liste dont l’adresse de la première cellule se trouve dans la variable liste. Exemple : la liste linéaire (a, b, c, d) est schématisé liste a b c d liste+ = (a, b, c, d). On notera que : - si liste == NULL, liste+ est vide, - il y a autant de cellules que d’éléments, - le champ suivant d’une cellule contient l’ adresse de la cellule suivante, - le champ suivant de la dernière cellule contient la valeur NULL. 27 - il suffit de connaître l’adresse de la première cellule rangée dans la variable liste pour avoir accès à tous les éléments en utilisant : liste->info, liste->suivant, liste->suivant->info, … liste->info et liste->suivant->info ne sont définie que si liste est différente de NULL. Notation : a<liste+, indique que a est inférieur à tous les éléments de liste+. Sous-liste : une sous-liste p+ de liste+ est une liste telle que les éléments de p+ soient une suite d’éléments consécutifs de liste+. Exemple : liste+ = (a, b, c, d, e). p+ = (b, c) est une sous-liste de liste+. p+ = (b, d) n’est une sous-liste de liste+. On notera que : liste+ = p- || p+, On remarque que si liste+ = (a, b, c, d), liste- est vide et que si liste=NULL on a liste+ est vide. 2.1.2. Algorithme de création d’une liste chaînée Créer une liste chaînée qui est la représentation de (a, b). Déclaration struct cellule { t info; struct cellule * suivant; }; Où t est un type simple, vecteur, structure. Puis on peut déclarer une variable liste : struct cellule* liste; struct cellule* p; ou bien typedef struct cellule *pointeur; pointeur liste; pointeur p; 28 Créer la liste (a, b) à partir de son dernier élément. Etape 1 : création d’une liste vide d’adresse liste. liste = NULL; Etape 2 : création de la liste d’adresse liste et contenant l’information b. - création d’une cellule d’adresse p contenant l’information b p=(pointeur)malloc(sizeof(struct cellule)); p->info=‘b’; - chaînage avec la liste précédente p->suivant=liste; - création de la liste d’adresse liste représentation de (b) liste=p; Etape 3 : création de la liste d’adresse liste contenant l’information (a, b). - création d’une cellule d’adresse p contenant l’information a p=(pointeur)malloc(sizeof(struct cellule)); p->info=‘a’; - chaînage avec la liste précédente p->suivant=liste; - création de la liste d’adresse liste représentation de (a, b) liste=p; 29 Algorithme de création d’une liste chaînée à partir d’un fichier pointeur creerliste(FILE* f) { pointeur l, p; int val; l=NULL; f=fopen("fic.dat", "r"); while(!feof(f)) { p=(pointeur)malloc(sizeof(struct cellule)); fscanf(f, "%d", &val); p->info=val; p->suivant=l; l=p; } return l; } Passage d’un vecteur à une liste chaînée pointeur vecliste(int v[], int n) { int i; pointeur l, p; l=NULL; i=n; while(i n) { p=(pointeur)malloc(sizeof(struct cellule)); p->info=v[i]; p->suivant=l; l=p; i--; } return l; } 30 Pour obtenir les éléments du fichier dans le même ordre, on doit créer la liste à partir de son premier élément. Pour ce faire, on doit d’abord prévoir le cas du premier élément qui permet la création de la première cellule de la liste et dont l’adresse doit être préservée car elle permettra l’accès à toutes les cellules créées. Créer une liste à l’endroit à partir d’un fichier. pointeur creerliste(FILE* f) { pointeur l, p, der; int val; f=fopen("fic.dat", "r"); if(feof(f))l=NULL; else { l = (pointeur)malloc(sizeof(struct cellule)); fscanf(f, "%d", &val); l->info=val; der=l; while(!feof(f)) { p=(pointeur)malloc(sizeof(struct cellule)); fscanf(f, "%d", &val); p->info=val; der->suivant=p; der=p; } der->suivant=NULL; return l; } } 31 2.1.3. Parcours d’une liste a) schéma récursif Pour écrire des algorithmes récursifs, on utilise la définition suivante d’une liste chaînée : • soit une liste est vide (liste == NULL), • soit une liste est composée d’une cellule (la 1ière) chaînée à une sous-liste (obtenue après suppression de la 1ière cellule). Exemple : (a,b,c,d) est composée d’une cellule contenant a et d’une sous-liste contenant (b, c, d). Cette sous-liste a pour adresse liste->suivant+. Premier parcours - on traite la 1ière cellule, - on effectue le parcours de la sous-liste liste->suivant+. Algorithme de parcours d’une liste à l’endroit void parcours1(pointeur l) { if(l != NULL) { traiter(l->info); parcours1(l->suivant); } } Deuxième parcours - on effectue le parcours de la sous-liste liste->suivant +. - on traite la 1ière cellule. Algorithme de parcours de la liste à l’envers 32 void parcours2(pointeur l) { if(l != NULL) { parcours2(l->suivant); traiter(l->info); } } Où traiter est une fonction quelconque. b) Schéma itératif. Le 2ième parcours n’est pas simple à obtenir en itératif. Il est nécessaire de disposer d’une pile. Algorithme du schéma itératif du 1ier parcours. void parcours1(pointeur l) { while(l != NULL) { traiter(l->info); l=l->suivant; } } Schéma itératif de parcours1, protection de l’adresse de la 1ière cellule void parcours1 (pointeur* l) { pointeur p; p=(*l); //protection de l’adresse while(p != NULL) { traiter(p->info); p=p->suivant; } 33 } 2.1.4. Algorithmes d’écriture des éléments d’une liste Ecrire sous forme récursive deux algorithmes d’écriture des éléments d’une liste. a) première version On utilise le 1ier parcours parcours1. On écrit les éléments de la liste à partir du 1ier élément. void ecritliste1(pointeur l) { if(l != NULL) { printf("%d", l->info); ecritliste1(l->suivant); } } b) deuxième version On utilise le 2ième parcours parcours2 qui permet d’obtenir l’écriture des éléments à partir du dernier élément. void ecritliste2(pointeur l) { if(l != NULL) { ecritliste2(l->suivant); printf("%d", l->info); } 34 } 2.1.5. Exemples d’algorithmes sur les listes chaînées 2.1.5.1. Algorithme de calcul de la longueur d’une liste a) schéma itératif int longue1(pointeur l) { int nomb=0; while(l != NULL) { nomb += 1; l = l->suivant; } return nomb; } b) schéma récursif Raisonnement : •liste+ est vide sa longueur est alors égale à zéro. return 0 ; terminé •liste+ n’est pas vide elle est composée d’une cellule (1ier élément) chaînée à la sous-liste liste->suivant+. La longueur de la liste est donc égale à 1 plus la longueur de la liste liste-> suivant+. return (1 + longue2(liste->suivant)); 35 int longue2(pointeur l) { if(l == NULL) return 0; else return (1+longue2(l->suivant)); } 2.1.5.2. Algorithme de calcul du nombre d’occurrences d’un élément dans une liste a) schéma itératif int nbocc1 (pointeur l, int val) { int nomb=0; while(l != NULL) { if(l->info == val)nomb += 1; l = l ->suivant; } return nomb; } 36 b) Schéma récursif Raisonnement : • liste+ est vide. Le nombre d’occurrences est égale à zéro : return 0 ; * • liste+ n’est pas vide. Elle est donc composée d’un élément et d’une sous-liste liste->suivant+. °° liste->info == val. Le nombre d’occurrences est égal à 1 plus le nombre d’occurrences de val dans la sous-liste liste->suivant+ : return(1+nbocc2(liste->suivant, val)); °° liste->info val. Le 1ier élément ne contient pas la valeur val, le nombre d’occurrences de val dans la liste est donc égal au nombre d’occurrences de val dans la sous-liste liste->suivant+ : return (nbocc2(liste->suivant, val)); int nbocc2 (pointeur l, int val) { if(l==NULL) return 0; else { if(l->info == val) return (1+ nbocc2(l->suivant, val)); else return(nbocc2(l->suivant, val)); } } 37 4.1.5.3. Algorithmes d’accès dans une liste On distingue 2 sortes d’accès : accès par position ou accès associatif. a) Algorithme d’accès par position - schéma itératif On utilise le même algorithme que celui donné sur les fichiers séquentiels. pointeur accesk(pointeur l, int k, booléen* trouve) { int i=1; pointeur pointk; while((i<k) && (l != NULL)) { i++; l = l->suivant; } *trouve=((i==k) && (l != NULL)); pointk=l; return pointk; } 38 - schéma récursif Le raisonnement est fondé sur la constatation suivante : chercher le kième (k>1) élément dans liste revient à chercher le k-1ième élément dans liste->suivant+. liste (1) (2) (k-1) (k) (n) liste->suivant (1) (k-2) (k-1) (n-1) Raisonnement • liste+ est vide. Algorithme est terminé, le kième élément n‘existe pas et pointk prend la valeur NULL : return liste; * • liste+ n’est pas vide. °° k == 1. Algorithme est terminé et pointk prend la valeur de l’adresse contenue dans la variable liste : return liste; °° k 1. On rappelle la fonction pour chercher le k-1ième élément dans la sous-liste liste->suivant+ : return (pointk(liste->suivant, k-1)); On peut mettre en facteur return liste; 39 Accès par position, schéma récursif. pointeur pointk(pointeur l, int k) { if((l==NULL) || (k==1))return l; else return (pointk(l->suivant, k-1)); } b) Accès associatif dans une liste - schéma itératif void accesv(pointeur l, int val, pointeur* point, booléen* acces) { booléen trouve = false; while((l != NULL) && (!trouve)) { if(l->info==val)trouve = true; else l = l->suivant; } *acces = trouve; *point = l ; } -schéma récursif En utilisant une convention ci-après, on peut écrire cet algorithme sous forme d’une fonction récursive dont l’en-tête est pointeur point(pointeur liste, int val). 40 Convention - point devra contenir l’adresse de la cellule contenant la valeur de la 1ière occurrence de la valeur val si elle existe dans liste+. -point devra contenir la valeur NULL si la valeur val n’existe pas dans liste+. Raisonnement • liste+ est vide. L’algorithme est terminé, point délivre la valeur NULL (val liste+). return NULL; • liste+ n’est pas vide. °° liste->info == val. L’algorithme est terminé et point délivre la valeur contenue dans la variable liste. return liste; °° liste->info val. On recherche la présence de val dans la sous-liste liste->suivant+. return (point(liste->suivant, val)); Accès par valeur, schéma récursif pointeur point(pointeur l, int val) { if(l == NULL) return l; else { if(l->info == val) return l; else return (point(l->suivant, val)); } } On notera qu’il est impossible de mettre en facteur l’action retour l ; à cause des risques de l’incohérence dus aux conditions l==NULL et l->info qui ne sont pas indépendantes. 41 c) Accès associatif dans une liste triée Définition d’une liste triée • une liste vide est triée, • une liste d’un élément est triée, • une liste de plus d’un élément est triée si tous les éléments consécutifs vérifient la relation d’ordre : liste NULL, liste->suivant NULL, liste->info liste->suivant->info. -schéma récursif. On écrit une fonction d’en-tête pointeur point (pointeur l, t val). • liste+ est vide. La valeur val n’est pas présente dans liste+, return NULL; * • liste+ n’est pas vide. °° liste->info<val, on doit chercher la 1ière occurrence de val dans liste->suivant+. return point(liste->suivant, val); °° liste->info>val, la valeur val n’est pas dans la liste : return NULL; * °° liste-> info == val, la cellule d’adresse liste contient la 1ière occurrence de val. return liste ; * pointeur point(pointeur l, int val) { if(l == NULL) return NULL; else { if(l->info<val) return point (l->suivant, val); else { if(l->info>val) return NULL; else return l; } } } 42 4.1.6. Algorithmes de mise à jour dans une liste Ces algorithmes ont une grande importance en raison de leur facilité de mise en œuvre. En effet, la mise en œuvre d’une liste n’entraîne que la modification d’un ou deux pointeurs sans recopie ni décalage des éléments non concernés par la mise à jour. 4.1.6.1. Algorithmes d’insertion dans une liste a) Insertion d’un élément en tête de liste C’est un cas très particulier car l’insertion en tête modifie l’adresse de la liste. [Schéma] Créer la cellule d’adresse p. Le champ info (information) reçoit la valeur de l’élément. Terminer par la réalisation des 2 liaisons (a) et (b) dans cet ordre. Les éléments suivants sont décalés automatiquement d’une position. void insertete (pointeur *l, int elem) { pointeur p ; p = (pointeur)malloc(sizeof(struct cellule)); //création d’une cellule d’adresse p. p->info = elem; //remplissage du champ info p->suivant = *l; //liaison a *l = p; //liaison b } 43 b) Algorithme d’insertion d’un élément en fin de liste - schéma itératif Si la liste est vide, on est ramené à une insertion en tête. Dans le cas général, la liste n’est pas vide. [schéma] Après avoir créé une cellule d’adresse p contenant l’information elem, on doit effectuer les liaisons (a) et (b). Pour pouvoir effectuer la liaison (b), il faut connaître l’adresse der de la dernière cellule. Disposant de la fonction dernier dont l’en-tête est pointeur dernier(pointeur liste), nous pouvons écrire l’algorithme. void inserfin(pointeur *l, int elem) { pointeur der, p; if((*l) == NULL)insertete(*l, elem); else { der = dernier (*l); p = (pointeur)malloc(sizeof(struct cellule)) ; // 1 p->info = elem; // 2 p->suivant = NULL; // fin de liste 3 der->suivant = p; // chaînage en fin de liste 4 } 44 } Les actions 1, 2, 3 et 4 correspondent à insertete ((&der)->suivant, elem). En effet, insérer en fin de liste est équivalent à insérer en tête de la liste qui suit (c’est-à-dire la liste vide). insertion en fin de liste, schéma itératif version 2. void inserfin (pointeur *l, t elem) //Spécification (l+ = la+) (l+ = la+ ||elem) { pointeur der, p ; if ((*l) == NULL) insertete (*l, elem) ; else { der = dernier (*l) ; insertete ((&der)->suivant, elem) ; } } c) Ecriture de la fonction dernier - schéma itératif On effectue un parcours de tous les éléments de la liste en préservant à chaque fois l’adresse de la cellule précédente. pointeur dernier (pointeur l) { pointeur precedent ; while(l != NULL) { precedent = l ; l = l->suivant ; } return precedent ; 45 } - schéma récursif Le raisonnement est : • liste ne contient qu’une seule cellule (l-> suivant == NULL). La liste contient l’adresse de la dernière cellule. return l ; * • liste contient plus d’une cellule (l->suivant NULL). return dernier(l->suivant) ; Algorithme pointeur dernier (pointeur l) { if(l->suivant == NULL)return l; else return dernier (l->suivant); } - schéma récursif de inserfin On peut également donner une version récursive de la fonction inserfin en utilisant la fonction insertete. 46 Le raisonnement : • liste+ = vide, On insère l’élément. return insertete (&liste, elem); * • liste+ vide, return inserfin((&liste)->suivant, elem) ; Algorithme void inserfin (pointeur *l, int elem) { if((*l) == NULL)insertete ((*l), elem); else inserfin((*l)->suivant, elem); } d) Algorithme d’insertion d’un élément à la kième place [Schéma] - schéma itératif L’insertion d’un élément à la kième place consiste à créer les liaisons (a) et (b) dans cet ordre. Les éléments suivants sont automatiquement décalés d’une position. Pour réaliser la liaison (b), il faut connaître l’adresse de la cellule précédente (k-1ème). L’insertion n’est possible que si k[1..n+1] où n est le nombre d’éléments de la liste. Il faudra prévoir l’insertion en tête (k==1) car elle modifie l’adresse de la liste. On utilisera la fonction pointk pour déterminer l’adresse de la k-1ème cellule. 47 Insertion d’un élément à la kième place, schéma itératif. void insertk (pointeur *l, int k, int elem, booléen *possible) { pointeur p, precedent; *possible = false; if(k == 1) { insertete ((*l), elem) ; *possible = true; } else { precedent = pointk(*l, k-1); if(precedent != NULL) { p = (pointeur)malloc(sizeof(struct cellule)); //1 p->info = elem; //2 p->suivant = precedent ->suivant; //3 precedent->suivant = p; //4 *possible = true; } } } On peut également constater que l’exécution des actions 1, 2, 3, 4 de l’algorithme correspond à l’exécution de l’action insertete pour la liste precedent->suivant+ en écrivant à la place de ces actions, l’action insertete((&precedent)->suivant, elem); 48 - schéma récursif • k == 1, On effectue une insertion en tête de liste+ insertete(liste, elem); * •k1 °° liste+ est vide, l’insertion est impossible * °° liste+ n’est vide insertk(liste->suivant, k-1, elem, possible); void insertk (pointeur *l, int k, int elem, booléen*possible) { if(k ==1) { *possible = true; insertete ((*l), elem); } else { if((*l) == NULL) *possible = false; else insertk((*l)->suivant, k-1, elem, possible) ; } } e) Algorithme d’insertion de la valeur elem après la première occurrence de la valeur val. Il suffit de connaître l’adresse de la cellule qui contient la 1ière occurrence de la valeur val pour pouvoir réaliser les liaisons (a), (b) dans cet ordre (schéma). 49 Insertion de elem après la première occurrence de val, schéma non récursif. booléen insert(pointeur *l, int val, int elem) { pointeur ptval; booléen possible; possible = false; ptval = point((*l), val); if(ptval != NULL) { possible = true; insertete((&ptval)->suivant, elem); } return possible; } - schéma récursif : on souhaite écrire la fonction insert sous-forme récursive. Le raisonnement : • liste+ est vide, l’insertion est impossible car val liste+. * • liste+ n’est pas vide °° liste->info==val, on effectue l’insertion en tête liste->suivant. insertete(liste->suivant, elem) * °° liste->infoval insert(liste->suivant, val, elem) 50 Insertion de elem après la 1ière occurrence de val, schéma récursif. booléen insert (pointeur *l, int val, int elem) { if((*l)==NULL) return false; else { if((*l)->info == val) { insertete ((*l)->suivant, elem) ; return true; } else insert ((*l)->suivant, val, elem); } } f) Algorithme d’insertion dans une liste triée On considère que la liste est la concaténation de 2 sous-listes la et lb telle : liste+ =la+||lb+ et la+<elemlb+. Nous avons mis l’égalité à droite afin de minimiser le temps de parcours. Cas particuliers : liste+ est vide, il suffit d’insérer en tête de liste+. la+ est vide, elem est donc inférieur ou égal à la 1ière valeur de la liste. On effectue une insertion en tête de liste+. lb+ est vide, elem est supérieur à tous les éléments de la liste. On insère en fin de liste, donc en tête de la liste dont le point d’entrée se trouve dans le champ suivant de la dernière cellule. L’algorithme consiste à parcourir toute la liste la+ et à insérer elem en tête de la liste lb+, dont le point d’entrée se trouve dans le champ suivant de la cellule d’adresse dernier (la+). - schéma récursif : l’en-tête de la fonction est void insertri(pointeur *l, t elem). 51 Raisonnement • liste+ est vide, on effectue l’insertion en tête de liste+ : insertete (liste, elem) ; * • liste+ n’est pas vide °° liste->info < elem, liste->info appartient à la+. insertri (liste->suivant, elem) ; °° liste->info elem, on est positionné sur le 1er élément de lb+ : insertete(liste, elem) ; * Algorithme void insertri (pointeur *l, int elem) { if((*l) == NULL)insertete((*l), elem); else { if(elem(*l)->info)insertete((*l), elem); else insertri ((*l)->suivant, elem); } } - schéma itératif : il faut effectuer un parcours séquentiel afin de déterminer les listes la+ et lb+. On peut utiliser une variable auxiliaire qui contient l’adresse de la cellule précédente preced. [schéma] Initialisation On a 2 cas particuliers : liste+ est vide, il faut effectuer une insertion en tête de liste+ : insertete(liste, elem); liste+ n’est pas vide, elem liste->info, on effectue une insertion en tête de liste+. insertete (liste, elem); Ces 2 cas particuliers étant traités, on réalise l’initialisation preced = liste; p = liste->suivant; 52 Algorithme d’insertion d’un élément dans une liste triée, schéma itératif void insertri (pointeur *l, int elem) { pointeur p, preced ; booléen super; if((*l ) == NULL)insertete((*l), elem); else { if(elem (*l)->info)insertete((*l), elem); else { preced = *l ; p = (*l)->suivant ; super = true; while((p != NULL) && super) if(elem > p->info) { preced = p ; p = p->suivant ; } else super = false; insertete ((&preced)->suivant, elem) ; } } } 4.1.6.2. Algorithme de suppression d’un élément dans une liste On a le cas particulier de suppression du 1ier élément qui a pour conséquence de modifier l’adresse de la liste. Dans le cas général, il suffit de modifier le contenu d’un pointeur. [schéma] 53 a) Algorithme de suppression du premier élément On suppose que la liste n’est pas vide. [schéma] Il faut préserver l’adresse de la tête de liste avant d’effectuer la modification de cette adresse. void supptete(pointeur* l) { pointeur p ; p = *l ; *l = (*l)->suivant; free(p); } b) Algorithme de suppression par position : supprimer le kième élément. - schéma itératif Il faut déterminer l’adresse precedent de la cellule qui précède celle que l’on veut supprimer. C’est-à-dire l’adresse de la k-1ième cellule qui sera obtenue par la fonction pointk. Ensuite si le kième cellule existe, on modifie la valeur d’adresse precedent. [schéma] Algorithme : au préalable, on aurait pris soin de traiter le cas particulier du premier élément. 54 void supprimek(pointeur *l, int k, booléen* possible) { pointeur ptk , precedent; if((*l) != NULL) && (k==1)) { *possible = true; supptete(*l); } else { *possible = false; precedent = pointk(*l, k-1); if(precedent != NULL) { ptk = precedent->suivant; if(ptk != NULL) { //ptk = adresse de la kième cellule et precedent = adresse de la cellule précédente. *possible = true; precedent->suivant = ptk->suivant; free(ptk); } } } } -schéma récursif. Le raisonnement est : •liste+ est vide, la suppression est impossible. car val liste+. On exécute *possible = false; •liste+ n’est pas vide °° k==1, on effectue la suppression en tête liste. *possible = true; supptete(liste); * 55 °° k1, supprimek(liste->suivant, k-1, *possible); * Suppression du kième élément, schéma récursif. void supprimek(pointeur *l, int k, booléen *possible) { if((*l) == NULL)*possible = false; else { if(k==1) { *possible = true; supptete (l); } else supprimek((*l)->suivant, k-1, possible) ; } } Algorithme de suppression associative Supprimer la 1ière occurrence de val de la liste. Il faut définir une variable booléenne possible qui nous permettra de savoir si cette suppression a été réalisée ou non. - schéma récursif. On souhaite écrire une fonction dont l’en-tête est : void suppval (pointeur *liste, int val, int *possible) Raisonnement • liste+ est vide, la suppression est impossible, car val liste+ : *possible = false; * • liste+ n’est pas vide °° liste->info==val, on effectue la suppression en tête : *possible=true; supptete(liste); * 56 °° liste->infoval, on fait l’appel récursif : suppval(liste->suivant, val, possible); Suppression associative, schéma récursif. void suppval (pointeur *l, int val, booléen *possible) { if((*l)==NULL) *possible = false; else if((*l)->info==val) { supptete (*l); *possible = true; } else suppval ((*l)->suivant, val, possible); } 57