Vous êtes sur la page 1sur 57

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 sous-
fichiers 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 maxf et telle que yf, on ait maxy. 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 whileon 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 feof(f) résultat
vrai vrai trouve = false
vrai faux trouve=true; fscanf(f, "%d", &valk)
faux (i<k) vrai trouve = false
faux (i<k) faux 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).
valf 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) egalRésultat : val  f


vrai vrai trouve = true (val = Df)
vrai fauxtrouve = false
fauxvrai trouve = true
fauxfauximpossible (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 precedentnomb;
}
}

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 sous-


fichiers f’ et f’’ tel que : f = f’ || f’’, f’< val  f’’.
Remarque
Si f’ est vide, la relation s’écrit : val  f-
Si 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 1 iè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 1 iè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 1 iè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 1 iè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); *
•k1
°° 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 1 iè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->infoval
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+<elemlb+. 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); *
°° k1, supprimek(liste->suivant, k-1, *possible); 55
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); *
°° liste->infoval, on fait l’appel récursif : suppval(liste->suivant, val, possible);
56
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

Vous aimerez peut-être aussi