Cours Structure de Données
Thèmes abordés
Cours Structure de Données
Thèmes abordés
Polycopié du cours
structures de données en C.
M. OUZZIF
1
Chapitre 1 : Eléments de base
1. Introduction
Les structures que nous avons traitées jusqu’à présent sont des structures linéaires contiguës de
type vecteurs ou fichiers séquentiels. Dans ce chapitre nous allons aborder une représentation
dynamique de ces structures linéaires en utilisant la notion de pointeur permettant des
chainages non contigus.
2. Notion de pointeur
Un pointeur est une variable dont le contenu est une adresse. Supposons que p est une variable
de type pointeur qui contient l’adresse de l’information a. p sera représenté par la figure 1.
Var p : pointeur ;
2
p désigne la variable qui contient l’adresse. Cette variable peut être utilisée en partie droite ou
gauche d’une affectation contenant des adresses.
p^ : désigne l’objet dont l’adresse est rangée dans p. p^ peut apparaître en partie droite ou
gauche d’une affectation concernant des objets de même type que celui pointé par p.
Exemple :
p1 p2
p1^ p2^
a b
p1 p2
p2^, p1^
a b
Remarque : a n’est plus accessible car son adresse qui se trouvait dans p1 a été perdu.
p1^ p2^ permet d’obtenir la situation représentée par la figure 4 dans laquelle a n’existe
plus et la donnée b existe en deux exemplaires dans les zones mémoires adressées par les
pointeurs p1 et p2.
3
p1 p2
p1^ p2^
b b
- Valeur NIL
On dispose d’une valeur conventionnelle nommée NIL de type pointeur qui ne correspond à
aucune adresse possible d’objet.
3. Notion de cellule
En vue de construire dans la suite de ce chapitre des structures linéaires à composantes chaînées
dynamiquement, nous introduisons la notion de cellule. Cette peut être représentée par un
enregistrement comprenant une donnée « d » de type quelconque « t » et un pointeur « suivant »
sur une structure de type cet enregistrement.
4
- Pointeur est le type défini par :
o type pointeur : ^cellule ;
p^
d Suivant
L’adresse de la cellule est rangée dans la variable p. On parle généralement de cellule pointée
par p. La cellule contient deux champs. Un champ d de type t et un champ suivant contenant
une adresse. Si p est différent de NIL, p^.d permet d’accéder à la donnée de la cellule p^.suivant
permet d’accéder à la zonne pointée par p^.suivant qui, au cas où il est différent de NIL,
représente la cellule suivante.
Cette primitive permet à chaque appel d’obtenir une nouvelle cellule dont l’adresse est
retournée dans la variable p.
Remarque : Var signifie que le paramètre est passé par variable (ou référence).
5
Procédure liberer (Val p : poiteur)
Spécification { } ==> {rend la cellule d’adresse p au resvoir}
4. Pointeurs en C.
On appelle Lvalue (left value) tout objet pouvant être placé à gauche d'un opérateur
d'affectation. Une Lvalue est caractérisée par :
son adresse, c'est-à-dire l'adresse-mémoire à partir de laquelle l'objet est stocké ;
sa valeur, c'est-à-dire ce qui est stocké à cette adresse.
int i, j;
i = 3;
j = i;
Deux variables différentes ont des adresses différentes. L'affectation i = j; n'opère que sur les
valeurs des variables. Les variables i et j étant de type int, elles sont stockées sur 4 octets. Ainsi
la valeur de i est stockée sur les octets d'adresse 4831836000 à 4831836003.
L'adresse d'un objet étant un numéro d'octet en mémoire, il s'agit d'un entier quelque soit le type
de l'objet considéré. Le format interne de cet entier (16 bits, 32 bits ou 64 bits) dépend des
architectures. Sur un DEC alpha, par exemple, une adresse a toujours le format d'un entier long
(64 bits).
6
L'opérateur & permet d'accéder à l'adresse d'une variable. Toutefois &i n'est pas une Lvalue
mais une constante : on ne peut pas faire figurer &i à gauche d'un opérateur d'affectation. Pour
pouvoir manipuler des adresses, on doit donc recourir un nouveau type d'objets, les pointeurs.
on définit un pointeur p qui pointe vers un entier i :
int i = 3;
int *p;
p = &i;
main ()
{
int i = 3;
int *p;
p = &i;
printf("*p = %d \n",*p);
}
imprime *p = 3.
Dans ce programme, les objets i et *p sont identiques : ils ont mêmes adresse et valeur. Nous
sommes dans la configuration :
7
Cela signifie en particulier que toute modification de *p modifie i. Ainsi, si l'on ajoute
l'instruction *p = 0; à la fin du programme précédent, la valeur de i devient nulle.
On peut donc dans un programme manipuler à la fois les objets p et *p. Ces deux manipulations
sont très différentes. Comparons par exemple les deux programmes suivants :
main()
{
int i = 3, j = 6;
int *p1, *p2;
p1 = &i;
p2 = &j;
*p1 = *p2;
}
et
main()
{
int i = 3, j = 6;
int *p1, *p2;
p1 = &i;
p2 = &j;
p1 = p2;
}
Avant la dernière affectation de chacun de ces programmes, on est dans une configuration du
type :
8
Par contre, l'affectation p1 = p2 du second programme, conduit à la situation :
5. Notion de récursivité.
La récursivité est un concept général qui peut être illustré dans (quasiment) tous les langages
de programmation, et qui peut être utile dans de nombreuses situations. Elle est très utile dans
les algorithmes de manipulation des structures de données.
La définition la plus simple d'une fonction récursive est la suivante : c'est une fonction qui
s'appelle elle-même. Si dans le corps (le contenu) de la fonction, vous l'utilisez elle-même, alors
elle est récursive.
L'exemple habituel est la fonction factorielle. Cette fonction, provenant des mathématiques (et
très utilisée dans certains domaines mathématiques) prend en entrée un entier positif, et renvoie
le produit des entiers inférieurs jusqu'à 1, lui compris :
Pour pouvoir trouver une solution récursive, il faut trouver la condition d’arrêt des appels
appelée aussi condition de sortie.
9
Nous pouvons observer ici que le dernier return est en fait l'appel récursif et nous
soustrayons 1 à chaque appel jusqu'à ce que n == 1 qui est, comme décrit plus haut, notre
condition de sortie.
Non, cela ne s'arrête pas là et c'est ici que nous allons voir le fonctionnement des fonctions
récursives. Lorsque la fonction rencontre la condition de sortie, elle remonte dans tous les
appels précédents pour calculer n avec la valeur précédemment trouvée !
Les appels des fonctions récursives sont en fait empilées (pile qui est une structure de donnée
régie selon le mode LIFO: Last In First Out, Dernier Entré Premier Sorti). Chaque appel se
trouve donc l'un à la suite de l'autre dans la pile du programme. Une fonction de ce type possède
donc deux parcours: la phase de descente et la phase de remontée.
Voyons ceci grâce à un petit schéma:
Descente Remontée
n==6 n*120=720
n==5 n*24=120
n==4 n*6=24
n==3 n*2=6
n==2 n*1=2
n==1 n==1
Les fonctions récursives étant un moyen assez puissant pour résoudre certains problèmes de
façon élégante, elles n'en restent pas moins dangereuses et ce pour plusieurs raisons : le
dépassement de capacité et le débordement de pile (Stack Overflow).
- Dépassement de capacité
Une des causes assez fréquente quand vous travaillez sur de très grands nombres, est le
dépassement de capacité. C'est un phénomène qui se produit lorsque vous essayez de stocker
un nombre plus grand que ce que peut contenir le type de votre variable.
Il est d'usage de choisir un type approprié, même si vous êtes certains que le type que vous avez
choisi ne sera jamais dépassé, utilisez tant que possible une variable pouvant contenir de plus
grandes données. Ceci s'applique à tous types de données.
10
Evitez le type int si vous travaillez avec une fonction récursive, comme les exemples précédents
pour le calcul de factoriels. Ce type est très petit et dans une fonction récursive il peut très vite
arriver de le dépasser et c'est donc le plantage assuré. Préférez-lui un type comme long pour
assurer un minimum la viabilité de votre application.
- Débordement de pile
Ceci est sans doute une des causes les plus souvent rencontrés dans le plantage de programmes
avec des fonctions récursives. Nous savons que les appels récursifs de fonctions sont placés
dans la pile du programme, pile qui est d'une taille assez limité car elle est fixée une fois pour
toutes lors de la compilation du programme.
Dans la pile sont non seulement stockés les valeurs des variables de retour mais aussi les
adresses des fonctions entre autres choses, les données sont nombreuses et un débordement de
la pile peut très vite arriver ce qui provoque sans conteste une sortie anormale du programme.
Une autre méthode existe cependant ! Si vous êtes presque sûr de dépasser ce genre de limites,
préférez alors une approche itérative plutôt qu'une approche récursive du problème. Une
approche récursive demande beaucoup de moyens en ressources car énormément de données
doivent être stockées dans la pile d'exécution alors qu'en revanche, une approche itérative telle
une boucle for est bien moins coûteuse en terme de ressources et est bien plus sûre, sauf dans
le cas d'un dépassement de capacité bien sûr.
11
Chapitre 2 : Listes linéaires chaînées
1. Définition
Une liste linéaire chaînée est constitué d’une suite de cellules chaînées entre elles. L’adresse
de la première cellule détermine toute la liste. Une liste est une structure souple : elle peut
grandir ou se rétrécir à tout moment et de manière dynamique. Ses éléments peuvent être
insérés ou supprimés à n’importe quel endroit. L’intérêt de l’utilisation de cette structure de
données réside dans le fait qu’un nouvel élément n’occupe pas le mot mémoire suivant et par
conséquent, sa création n’est pas liée à la contiguïté des zones mémoires (comme les tableaux).
Liste
2. Représentation
Nous représentons la liste linéaire chaînée comme étant un pointeur sur une cellule. On aura
ainsi la description suivante :
Type cellule = enregistrement
d:t;
suivant : pointeur ;
Fin cellule ;
12
Représentation en C :
typedef struct cel{ Créer une cellule
int data; cellule createCellule(cellule next, int x){
struct cel *next; cellule k = (cellule)malloc(sizeof(cellule));
}cel; k->data = x;
typedef cel* cellule; k->next = next;
return k;
}
liste NIL
Nouveau(p) ;
p^.d b ;
13
Puis nous chaînons avec la liste précédente :
p^.suivantliste
p^
Liste p ;
p^
b
liste
Nous créons en premier lieu une nouvelle cellule d’adresse p contenant l’information a :
Nouveau (p) ;
P^.info a ;
14
liste p
b a
p^.suivant liste ;
p liste
a b
Liste p ;
liste p
a b
15
On peut généraliser cet algorithme en supposant que les éléments successifs se trouvent dans
un fichier f. Dans ce cas, on crée la liste à partir de son dernier élément. On obtient ainsi une
liste contenant les éléments rangés dans l’ordre inverse.
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,
comme nous l'avons vu, l'accès à toutes les cellules créées.
Ensuite, pour chaque nouvel élément de f, on doit créer une nouvelle cellule qu'il faudra insérer
en fin de liste et non plus en tête. L'algorithme est alors le suivant:
17
der p;
(adresse de la dernière cellule)
finfaire ;
deri.suivant nil;
(fin de liste)
finsi;
finproc;
Comme on peut le constater, cet algorithme est moins simple que le précédent.
Pour l'écriture des algorithmes, sous forme récursive, on utilise la définition suivante d'une liste
chaînée:
• Exemple :
liste+ = (a, b, c, d, e) est composée d'une cellule contenant a et d'une sous-liste contenant (b, c,
d, e). Cette sous-liste a pour adresse listei.suivant. On l'appelle sous-liste (ou liste)
liste^.suivant+.
Premier parcours
Ce parcours traite les cellules de listé de gauche à droite. On appellera ce parcours parcoursgd.
Deuxième parcours
Ce parcours traite les cellules de liste+ de droite à gauche. On appellera ce parcours parcoursdg.
Exemple
Soit liste+ = (a, b, c, d). Pour les besoins de l'explication, nous avons appelé l'adresse de la
•• traiter (listei):
parcoursgd (Iistel):
• • traiter (liste1);
•• parcoursgd (IisteZ):
• • • • parcoursgd (liste4)
• • • • • parcoursgd (nil)
19
le parcours est terminé.
On constate que l'on traite les éléments dans l'ordre c'est-à-dire de gauche à droite. Dans le cas
de l'exécution de l'instruction parcoursdg (listei), on obtient:
• parcoursdg(Iistei)
• • parcoursdg (listei+1);
• • traiter (Iistei):
On obtient alors:
• parcoursdg (listel ) ;
• • parcoursdg (liste2) ;
• • • • parcours dg (liste4) ;
• • • • • parcoursdg (nil) ;
• • traiter (liste 1) ;
• que, pour chaque appel correspondant à une liste non vide, on doit exécuter les deux
instructions prévues dans la condition,
• que les éléments sont alors traités dans l'ordre inverse, c'est-à-dire de droite à gauche.
Ces deux parcours sont très utilisés et sont à rapprocher de la création de la liste à l'endroit ou
à l'envers que nous avons effectuée précédemment.
En C :
Le deuxième parcours n'est pas simple à obtenir sous forme itérative. Dans le cas général, il est
nécessaire de disposer d'une pile, comme nous le verrons dans les chapitres suivants. Nous nous
limiterons donc, pour le moment, au premier parcours qui correspond à ce qu'on appelle une
récursivité terminale; c'est-à-dire qu'il n'y a pas d'instruction à exécuter après l'appel récursif.
Le schéma itératif est alors le suivant:
Le paramètre liste étant un paramètre donnée (d), l'adresse de la première cellule est protégée
par l'instruction : listel:= liste; Dans le cas paramètre serait une donnée-résultat (dr), il serait
également nécessaire d'utiliser une variable auxiliaire de type pointeur pour effectuer le
parcours afin de ne pas perdre l'accès à la liste. L'algorithme serait alors le même.
En C :
On propose d'écrire des algorithmes de parcours d'une liste chaînée en commençant d'abord par
l'écriture, sous forme récursive, des éléments d'une liste.
21
a- Ecriture des éléments d'une liste
L'écriture de gauche à droite d'une liste chaînée correspondra au premier parcours alors que
l'écriture de droite à gauche correspondra au deuxième parcours.
On utilise le premier parcours parcoursgd. On écrit donc les éléments de la liste à partir du
premier élément.
On utilise le deuxième parcours parcoursdg qui permet d'obtenir l'écriture des éléments à partir
du dernier élément.
22
Quiz :
Schéma itératif
Initialisation : Il suffit que la variable liste1 contienne l'adresse de la première cellule de la liste
et d'initialiser la variable n à zéro en exécutant les instructions:
23
liste1liste;
n0 ;
Cet algorithme est une transposition de l'algorithme de calcul du nombre des éléments d'un
fichier.
Schéma récursif
24
d- Insertion dans une liste
Pour insérer un élément en tête de liste, Il faut d’abord créer une cellule p d’adresse p par
exécution de l’instruction nouveau (p). Ensuite, le champ information reçoit la valeur elem à
insérer. Pour terminer par la réalisation de deux liaisons (a) et (b) dans cet ordre figure 14.
List
e
(b)
(a) b
p
Elément à insérer
b
Figure 14. Insertion en tête de liste
Après avoir créé une cellule d’adresse p contenant l’information à insérer elem, nous devons
effectuer les liaisons a et b dans cet ordre. Pour pouvoir effectuer la liaison (b), il faut connître
l’adresse der de la dernière cellule. Nous supposons que nous disposon d’une fonction dernier
qui nous retourne ce pointeur.
25
liste der
(b) (a)
Elément à insérer p
Debproc
Si liste=NIL alors inserertete(liste,elem) ;
Sinon
derdernier(liste) ;
nouveau(p) ;
p^.infoelem ;
p^.suivantNIL ;
der^.suivant=p ;
finproc;
26
Exercice :
Ecrire la version récursive de la fonction dernier.
liste
(k-1) (k)
(b)
(a)
p Elément à insérer
liste
p (b)
(a)
Elément à insérer
Nous allons donner dans ce qui suit l’algorithme relatif à cette description.
27
Procédure insertk (Var liste: pointeur, Val k : entier ;val elem : t ; Var possible :
booleen) ;
p, precedent : pointeur
debproc
si k=1 alors inserertete(liste,elem) ;
possible vrai ;
sinon
precedent=pointk(liste, k-1) ;
si precedent =NIL alors possiblefaux ;
sinon
nouveau(p) ;
p^.infoelem ;
p^.suivant precedent^.suivant ;
precedent^.suivant p ;
possible vrai ;
finsi
finsi;
finproc
Comme les insertions, on a le cas particulier de la suppression du premier é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 comme le montre la figure 18.
liste
Elément à
supprimer
28
Suppression du premier élément
On suppose que la liste n’est pas vide. On suppose également que l’on n’a plus besoin de
l’élément et qu’on rend la cellule au réservoir de cellules. Il faut préserver par contre l’adresse
de la tête de la liste avant d’effectuer la modification de cette adresse.
liste
Elément à
supprimer
Nous voulons dans cette section supprimer le kième élément d’une liste. Il faut comme pour
l’insertion, déterminer l’adresse « precedent » de la cellule qui précède celle que nous voulons
supprimer : i.e. la kieme-1 cellule qui sera obtenue par un appel de la fonction pointk (à réaliser
comme exercice). Ensuite, si la kième cellule existe, nous modifions la valeur de l’adresse
contenue dans le champ suivant de la cellule d’adresse précédent comme indiqué dans la figure
20. Au préalable, nous aurions pris soin de traiter le cas particulier de la suppression du premier
élément.
29
liste
(k
)
Elément à insérer
precedent ptk
Procédure supprimerk (Var liste: pointeur, Val k : entier ;val elem : t ; Var possible :
booleen) ;
p, der : pointeur ;
Debproc
Si (liste#NIL) et (k=1) alors
supprimertete(liste) ;
possible vrai ;
sinon
possible faux ;
precedent=pointk(liste, k-1) ;
si precedent # NIL alors
ptk precedent^.suivant;
si ptk # NIL alors
possible=vrai ;
p^.suivantptk^.suivant ;
laisser (ptk) ;
finsi
finsi
finproc;
30
Exercice :
Ecrire la version récursive de la fonction supprimerk.
Plusieurs procédures et fonctions permettant de manipuler les listes sont fournies dans la partie
programmes C des structures données fournie comme complément en fin de ce cours. Il est
conseillé aux étudiants de les travailler avant de consulter la solution.
31
Chapitre 3 : Piles
1. Introduction
La structure de pile est une structure de liste particulière. Contrairement aux fichiers et vecteurs,
elle ne sert généralement pas à garder de façon plus ou moins définitive des informations. On
s'intéresse plutôt à la suite des états de la pile et on utilise le fait que le dernier élément ajouté
se trouve au sommet de la pile, afin de pouvoir être atteint le premier.
On peut donner une image du fonctionnement de cette structure avec une pile d'assiettes : on
peut ajouter et enlever des assiettes au sommet de la pile ; toute insertion ou retrait d'une assiette
en milieu de pile La stratégie de gestion d'une pile est "dernier arrivé, premier servi". En anglais
on dira Last ln First Out, plus connu sous le nom de LlFO. La structure de pile est utilisée pour
sauvegarder temporairement des informations en respectant leur ordre d'entrée, et les réutiliser
en ordre inverse.
Sommet
Sommet
. . .
. . .
. . ………………………. .
. . .
. . .
. . .
Base Base
32
Le dernier élément de chaque liste est appelé base de la pile; le premier élément de chaque liste
est appelé sommet de la pile. Les mises à jour de chaque liste, d'après la définition, se font à
partir du sommet. Bien que théoriquement de taille infinie, une liste est toujours, dans la
pratique, formée d'un nombre fini d'éléments qui correspond à la taille maximale de la pile.
Les adjonctions s'arrêtent lorsque cette taille maximale est atteinte ; on parle de débordement
de la pile (ou sur-dépassement). Les suppressions s'arrêtent lorsque la pile devient vide (sous-
dépassement). On définit habituellement les deux primitives suivantes:
Remarques :
• En général, la variable qui permet l'accès au sommet de la pile, ainsi que la pile elle-même,
sont des variables globales. On ne les passera donc pas en paramètres.
• Les valeurs des éléments d'une pile sont d'un type quelconque, que nous appellerons t.
• Exemples
Soient la pile schématisée en figure 22 (1) de et la variable val, qui contient z ; si l'on exécute
l'instruction empiler(val), le nouvel état de la pile sera figure 22 (2) :
Empiler (val) z
c c
b val z b
a a
(1) (2)
si l'on exécute l'instruction dépiler(val), le nouvel état de la pile est (3), et val contient alors c :
On peut définir d'autres primitives qui facilitent l'utilisation d'une pile mais qui ne modifient
pas la pile elle-même:
une fonction sommetpile qui délivre la valeur de l'élément de sommet de la pile.
33
Depiler (val)
c
b b
a a
(1) (2)
Une pile est une liste linéaire, elle peut donc être représentée de manière contiguë ou chaînée.
Les algorithmes proposés par la suite ne tiennent pas compte de la représentation. Notons bien
que ces primitives doivent être utilisables quelle que soit la représentation interne choisie pour
la pile. Seul le corps des primitives, inaccessible à l'utilisateur, dépend de cette représentation.
Soit une pile utilisant une représentation contiguë: un vecteur pile dont on donne a priori la
taille maximale dimpile. Dans ce cas, l'accès au premier élément est déterminé par un indice
sommet dont les valeurs extrêmes permettant de tester les dépassements sont 0 et dimpile. On
34
peut remarquer que la liste est représentée "à l'envers" dans le vecteur :la tête de liste correspond
à l'indice le plus élevé, ou sommet, la base à l'indice 1.
Les dépassements sont testés dans les primitives dépiler et empiler (dans la pratique, ces deux
tests ne sont pas toujours effectués; selon la nature du problème utilisant la pile, d'autres tests
peuvent être envisagés).
Les variables pile, dimpile et sommet étant globales, les primitives sont les suivantes:
- Primitive empiler
debproc
si pilepleine alors
écritexte ('erreur: la pile est pleine') ;
arrêt;
sinon
sommet sommet + 1;
pile [sommet] v;
finsi;
finproc;
- Primitive dépiler
35
Remarques : On supposera que arrêt est une instruction qui permet d'arrêter le déroulement de
l'algorithme qui a appelé ces primitives.
fonction sommetpile : t;
debfonc
retour pile [sommet] ;
finfonc;
procédure initpilevide ;
debproc
sommet:=0;
finproc;
36
4. Représentation chaînée d'une pile
Une pile peut être représentée par une liste linéaire chaînée telle que les insertions ou les
suppressions sont toujours effectuées en tête de liste.
pile
Notons qu'en raison de la gestion dynamique de la mémoire, la primitive pilepleine n'a plus de
raison d'être, on supposera que l'instruction d'empilement est toujours possible.
- Primitive dépiler
37
- Primitive vérification si la pile est vide
procédure initpilevide ;
debproc
pile NIL ;
finproc ;
38
Chapitre 4 : Files
1. Introduction
Les files correspondent au comportement d'une file d'attente devant un guichetoù la gestion est
la suivante : premier arrivé, premier servi. En anglaison dira: First ln First Out ou plus
simplement FIFO. Ces files d'attentesont d'un usage très répandu dans la programmation
système, où ellesservent à gérer, par exemple, l'allocation de ressources.
Définition
2. Représentation contiguë
Onpeut songer à représenter une file d'attente dans un vecteur simplement en ajoutant les
nouveaux éléments à droite (queue de la file), et en déplaçant le pointeurde tête de la file vers
la droite chaque fois que l'on supprime un élément.
• Exemple
Tête Queue
Après insertion de E, F et G et A B C D E F G
suppression de A et B
Tête Queue
39
Ensupposant que le vecteur FILE [1..n], ainsi que les entiers tête et queue, sont des variables
globales, les primitives de gestion de la file seraient les suivantes:
- Primitive d’initialisation
procédure initfile ;
debproc
tête1;
queue0;
finproc;
40
- Primitive Défiler
On voit que tête et queue augmentent toujours, et donc que le vecteur devra être très grand,
même s'il ne contient que peu d'éléments à chaque instant. Pour éviter cet inconvénient, on peut
envisager deux solutions.
Tête Queue
Après insertion de E, F et G et C D E F G
suppression de A et B
Tête Queue
41
Les primitives modifiées sont les suivantes:
- Primitive d’initialisation
procédure initfile ;
debproc
queue0;
finproc;
- Primitive Défiler.
42
La taille du vecteur doit correspondre au nombre maximum d'éléments qui peuvent être
simultanément présents dans la file d'attente.
C'est la meilleure solution, elle minimise la place nécessaire dans levecteur comme dans la
solution précédente, tout en évitant les opérationsde décalage. Exemple : On suppose ici n=6 :
Tête Queue
Après insertion de E, F et G et G _ C D E F
suppression de A et B
Tête Queue
Lorsque tête devient égal à (queue + 1) mod n, la file est soit vide, soit pleine. On est donc
amené à distinguer les deux possibilités en utilisant deuxindicateurs booléens filevide et
filepleine. Les primitives deviennent alors :
- Primitive Enfiler (ajout en queue de la file)
43
- Primitive d’initialisation de la file
procédure initfile ;
debproc
tête := 1 ;
queue:= 0;
filevide := vrai;
filepleine := faux;
finproc;
- Primitive Défiler.
44
Tête Queue
Les algorithmes d'insertion et de suppression sont les suivants (tête et queue sont des variables
globales) :
45
Chapitre 5 : Arbre
1. Définition
Un arbre est une structure dynamique d’éléments organisés de façon hiérarchique (père, fils)et
définie en fonction des nœuds qui la composent. Un nœud est l’élément élémentaire de l’arbre
contenant des données et des pointeurs.
2. Exemple d’utilisation
Soit la figure suivante :
A
C
J K
I E
F N
B M
D
G L H
La relation d’inclusion entre les ensembles de la figure 29 peut être représentée à l’aide de
l’arbre de la figure 29.
B C D
E F G H
I J K L
Un type structuré dans tout langage de programmation peut être réprésenté sous forme d’arbre.
46
Type tdate = structure Type tlieu = structure
fin fin
Inscription : tetablissement ;
identite : tidentite ;
fin
Etudiant
Identite
Inscription
47
3. Notations et terminologie
On a remarqué que les schémas récursifs simplifiaient beaucoup l'écriture d'algorithmes sur les listes
chaînées. On utilisait, pour ce faire, la définition récursive suivante d'une liste: une liste est
• soit vide,
Il en est de même pour les arborescences, ou arbres, où on peut donner la définition suivante : un arbre
est
• soit vide,
- Terminologie
Racine
Niveau 1 A Degré 2
B K
L fils de K
E père de G
C D E L
F G H M N
I J
Feuilles
Degré : 0
48
4. Arbre n-aire et arbre binaire
Lorsqu’un arbre admet, pour chaque nœud, au plus n fils, l’arbre est dit n-aire. Si n est égale à
2, l’arbre est dit binaire. Nous nous interessons à ce deuxième type d’arbre. Dans cet arbre, nous
disposons pour chaque nœud d’au plus deux descendants nommés fils gauche et fils droit (ainsi
que de sous arbre gauche et sous arbre droit). Les algorithmes concernant les arbres binaires
sont souvent écrits sous forme récursif.
B E
C D F
Racine
B E
C D D
49
5. Arbre n-aire et arbre binaire
Le problème de parcours d’un arbre est analogue à celui du parcours d’une liste chaînée. Ces
algorithmes sont d’une grande importance car ils servent de base à l’écriture de très nombreux
algorithmes concernant les arbres. Il y a plusieurs manières pour parcourir un arbre binaire
suivant l’ordre dans lequel on énumère un nœud et ses sous arbres gauche et droit. Nous nous
interessons à trois types de parcours : préfixé, infixé et postfixé.
Exemple adopté :
B C
F H D
P M T I R
a. Parcours préfixé
Le traitement de la racine.
50
b. Parcours infixe (projectif ou symétrique)
Le traitement de la racine.
P, F, M, B, H, T, A, I, D, R, C, E.
Le traitement de la racine.
P, F, M, B, H, T, A, I, D, R, C, E.
51
Procédure postefixe (Val racine : pointeur)
debproc
si (racine # NIL) alors
postefixe (racine^.gauche) ;
postefixe (racine^.droit) ;
traiter (racine) ;
finproc
Nous souhaitons écrire une fonction permettant de prendre un arbre en entrée et de retourner le
nombre de ses nœuds.
Fonction taille (val racine : pointeur) : entier
Specification {} ===> {résultat = nombre de nœuds de racine+}
Si racine+ est vide alors le nombre de nœuds est 0.
Si racine+ est non vide alors cet arbre est formé d’un élément, d’un sous arbre gauche et d’un
sous arbre droit.
La taille de l’arbre est égale à 1 + la taille du sous arbre droit + la taille du sous arbre gauche.
Nous réalisons la fonction qui détermine si un nœud est feuille en premier lieu.
Fonction taille (val racine : pointeur) : booleen
Specification {nœud #NIL} ===> {résultat : nœud est une feuille}
52
Fonction feuille (Val noeud : pointeur) : booleen
debproc
retourner ((noeud^.droit=NIL) et (noeud^.gauche=NIL))
finsi
finproc
53
Effectuer la recherche dans le sous arbre droit.
Si le resultat est faux, Effectuer la recherche dans le sous arbre gauche.
D’autres procédures et fonctions qui seront discutés lors du cours pour illustrer leur
raisonnement sont données dans la partie programmes C des structures données fournie comme
complément en fin de ce cours.
54
Programmes C des algorithmes manipulant
les structures de données.
1. Programmes sur les listes chaînées
}
Insérer au début d’une liste
Calculer La taille d’une liste
void insertDebut(cellule *tete, int x){
int taille(cellule tete){
if(*tete == NULL)
if(tete == NULL)
*tete = createCellule(NULL, x);
return 0;
else
return ( 1 + taille(tete->next) );
*tete = createCellule(*tete, x);
}
}
void insertFinRecursive(cellule *tete, int x){ void insertRangIterative(cellule *tete, int x, int rang){
if(*tete == NULL || (*tete)->next == NULL){ if(rang <= 0 && rang > taille(*tete)+1){
printf("%d : Rang invalider.\n",rang);
insertDebut(&(*tete)->next, x);
return;
return;
}
}
if(rang == 1){
insertFinRecursive(&(*tete)->next, x); insertDebut(tete, x);
} return;
}
Insérer à un rang de la liste (récursif) cellule tmp = *tete;
return; }
tmp->next = createCellule(tmp->next, x);
}
}
if(rang == 1){
insertDebut(tete ,x);
Insérer en respectant l’ordre (itératif)
return;
void insertOrdreIterative(cellule *tete, int x){
}
if(*tete == NULL || x < (*tete)->data){
if(rang == 2){ insertDebut(tete ,x);
56
Insérer en respectant l’ordre (itératif) Supprimer le début de la liste
return; free(tmp);
} }
} return;
free(tmp); }
} }
…….
else{
while(tmp != NULL && rang != 2){ //parcourir jusqu'a rang-1 (la cellule precedente)
tmp = tmp->next;
rang--;
free(tmp2);
return;
if(rang == 1){
supprDebut(tete);
return;
supprRangRecursive(&(*tete)->next, rang-1);
58
Supprimer la première occurrence d’une valeur dans la liste (itératif)
getch();
if(*tete == NULL){
return;
if((*tete)->data == x){
supprDebut(tete);
return;
supprPoccurenceRecursive(&(*tete)->next, x);
59
Supprimer toutes les occurrences d’une valeur dans la liste (itératif)
int cpt = 0;
supprDebut(tete);
cpt++;
tmp = (*tete)->next;
prev = *tete;
while(tmp != NULL){
if(tmp->data == x){
prev->next = tmp->next;
free(tmp);
tmp = prev->next;
cpt++;
else{
prev = tmp;
tmp = tmp->next;
getch();
60
Supprimer toutes les occurrences d’une valeur dans la liste (récursif)
if(*tete == NULL){
return;
if((*tete)->data == x){
supprDebut(tete);
supprToutesOccurencesRecursive(tete, x);
else
supprToutesOccurencesRecursive(&(*tete)->next, x);
61
while(tmp != NULL && k != 0){
if(tmp->data == x){
prev->next = tmp->next;
free(tmp);
tmp = prev->next;
k--;
else{
prev = tmp;
tmp = tmp->next;
getch();
return;
supprDebut(tete);
supprKOccurencesRecursive(tete, x, k-1);
else
supprKOccurencesRecursive(&(*tete)->next, x, k);
62
Supprimer la k ieme occurrence d’une valeur dans la liste (itératif)
getch();
return;
return;
if(tmp->data == x){
keme--;
if(keme == 0)
break;
prev = tmp;
tmp = tmp->next;
if(tmp == NULL){
getch();
return;
supprDebut(&prev->next);
63
}
Supprimer la k ieme occurrence d’une valeur dans la liste (récursif)
return;
supprDebut(tete);
else if((*tete)->data == x)
supprKemeOccurencesRecursive(&(*tete)->next, x ,keme-1);
else
supprKemeOccurencesRecursive(&(*tete)->next, x ,keme);
64
if(tmp == NULL){
return;
supprDebut(&prev->next);
if(*tete == NULL){
return;
if((*tete)->data == val){
supprK(&(*tete)->next, k , val);
k--;
if(k==0)
supprDebut(tete);
else
supprK(&(*tete)->next, k, (*tete)->data);
else
supprK(&(*tete)->next, k, (*tete)->data);
65
2. Programmes sur les piles
- Représentation statique
return 0; }
} else{
int k = p->tab[p->sommet];
Enpiler un élément dans la Pile(PUSH)
p->sommet--;
void enpiler(pile *p , int x){
printf("\n%d est d%cpiler\n",k,130);
if(pleinS(*p)){
}
printf("La pile est plein !");
}
Vider la Pile est vide
getch();
void RAZ(pile *p){
}
p->sommet = -1;
p->sommet++;
}
p->tab[p->sommet] = x;
int sommet(pile *p){
printf("\n%d est empiler !",x);
return p->tab[p->sommet];
}
}
}
66
- Représentation dynamique
}cellule; return;
return 0 ; }
} Vider la Pile
void enpiler(pile *p, int x){ //On peut faire tout simplement p == NULL
67
3. Programmes sur les files
- Représentation statique
}file; }
} }
else
Afficher le contenu de la tête de la file f->queue = (f->queue + 1)%MAX;
68
Défiler un élément de la file
int k;
if(vide(*f)){
return;
k = f->tab[f->tete];
f->tete = -1;
f->queue = -1;
else{
k = f->tab[f->tete];
- Représentation dynamique
filePtr tete; }
filePtr queue;
}file;}file; 69
Structure d’une file statique Enfiler un élément à la file
70
Défiler un élément de la file (suite)
……….
else{
k = f->tete->data;
filePtr tmp = f->tete;
f->tete = f->tete->next;
free(tmp);
}
printf("\n%d est d%cfil%c.\n",k,130,130);
}
4. Les arbres
return a;
}k;
71
}
Procédures des parcours dans un arbre binaire
If(A==NULL)return 0 ;
Else{
Return(1+max(hauteur(A->g),hauteur(A-d) ) ) ;
72
Calcul de la hauteur d’un arbre
If(A == NULL)return 0 ;
If(A == NULL)return 0 ;
If(A==NULL)return 0 ;
If(A->data == x)return 1 ;
Return(recherche(A->d) || recherche(A->g)) ;
If(A == NULL)return 0;
If(A->data == x)return 1 ;
Else
Recherche_abr(A->g) ;
}
73
Recherche de la plus grande valeur d’un arbre
If(A->d == NULL){
*max = A->data ;
Return A->g ;
Return A ;
74
…………
else{
int x;
//il y a les deux
A->g = gdr(A->g, &x);
A->data = x;
return A;
}
}
return A;
}
}//fin
return A;
}
}//fin
Bibliographie
- Initiation à l’algorithmique et aux structures de données, volume1, Jacques Courtin et Irène
Kowarski, Dunod, 1994.
75