SDD Et Algo
SDD Et Algo
SDD Et Algo
STRUCTURES DE DONNÉES ET
ALGORITHME
Introduction Générale 7
1 Rappels et compléments de C 8
1.1 Les pointeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.1.1 Créer un pointeur . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.1.2 Envoyer un pointeur à une fonction . . . . . . . . . . . . . . . . . . 11
1.1.3 Une autre façon d’envoyer un pointeur à une fonction . . . . . . . . 12
1.2 Qui a dit : ”Un problème bien ennuyeux” ? . . . . . . . . . . . . . . . . . . 13
1.3 Les tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.3.1 Les tableaux dans la mémoire . . . . . . . . . . . . . . . . . . . . . 14
1.3.2 Définir un tableau . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.3.3 Les tableaux à taille dynamique . . . . . . . . . . . . . . . . . . . . 16
1.3.4 Parcourir un tableau . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3.5 Initialiser un tableau . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.3.6 Une autre façon d’initialiser . . . . . . . . . . . . . . . . . . . . . . 17
1.3.7 Passage de tableau à une fonction . . . . . . . . . . . . . . . . . . . 18
1.4 Les chaînes de caractères . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.4.1 Le type char . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4.2 Les chaînes de caractères sont les tableaux de char . . . . . . . . . 20
1.4.3 Création et initialisation de la chaîne . . . . . . . . . . . . . . . . . 21
1.4.4 Récupération d’une chaîne via un scanf . . . . . . . . . . . . . . . . 22
1.4.5 Fonctions de manipulation des chaînes . . . . . . . . . . . . . . . . 23
1.5 Les structures et les énumérations . . . . . . . . . . . . . . . . . . . . . . . 24
1.5.1 Définir une structure . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.5.2 Utilisation d’une structure . . . . . . . . . . . . . . . . . . . . . . . 26
1.5.3 Pointeur de structure . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.5.4 Les énumérations . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.6 L’allocation dynamique de mémoire . . . . . . . . . . . . . . . . . . . . . . 33
1.6.1 La taille des variables . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.6.2 Une nouvelle façon de voir la mémoire . . . . . . . . . . . . . . . . 34
1.6.3 Allocation de mémoire dynamique . . . . . . . . . . . . . . . . . . 36
1
Chargé du cours : M. Baïmi Badjoua ©UPM
2 Notions d’algorithme 42
2.1 Qu’est-ce qu’un algorithme ? . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.1.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.1.2 Omniprésence des algorithmes . . . . . . . . . . . . . . . . . . . . . 43
2.1.3 Rôle privilégié des algorithmes . . . . . . . . . . . . . . . . . . . . 43
2.1.4 Notion de structure de données . . . . . . . . . . . . . . . . . . . . 44
2.2 Découvrez l’intérêt des algorithmes . . . . . . . . . . . . . . . . . . . . . . 44
2.2.1 C’est logique ! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.2.2 Les algorithmes que nous connaissons . . . . . . . . . . . . . . . . . 45
2.2.3 Qu’est-ce qu’un programme ? . . . . . . . . . . . . . . . . . . . . . 46
2.3 Conventions pour écrire un algorithme . . . . . . . . . . . . . . . . . . . . 47
2.4 Types d’instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4 Les listes 54
4.1 Représentation d’une liste chaînée . . . . . . . . . . . . . . . . . . . . . . . 54
4.2 Construction d’une liste chaînée . . . . . . . . . . . . . . . . . . . . . . . . 55
4.2.1 Un élément de la liste . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.2.2 La structure de contrôle . . . . . . . . . . . . . . . . . . . . . . . . 56
4.2.3 Le dernier élément de la liste . . . . . . . . . . . . . . . . . . . . . 57
4.3 Les fonctions de gestion de la liste . . . . . . . . . . . . . . . . . . . . . . . 57
4.3.1 Initialiser la liste . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
4.3.2 Ajouter un élément . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
4.3.3 Supprimer un élément . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.3.4 Afficher la liste chaînée . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.4 Travaux Pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
Conclusion Générale 78
Références Bibliographiques 79
4
Chargé du cours : M. Baïmi Badjoua ©UPM
6
INTRODUCTION GÉNÉRALE
Avec les éléments de programmation que nous connaissons, nous sommes capables de
programmer tout ce qui est programmable. Cependant l’expérience des programmeurs a
conduit à introduire d’autres notions, théoriquement non indispensables mais facilitant
nettement la pratique de la programmation. Ceci est le cas des sous-programmes ; c’est
le cas également des structures de données qui sont avec les algorithmes, l’objet principal
de ce cours.
Jusqu’à maintenant on attribuait une variable simple par donnée à un instant précis. Il
arrive que les données soient nombreuses et aient un certain lien entre elles, ce concept
de lien étant considéré de façon intuitive et suffisamment vague pour l’instant. Ce phé-
nomène conduit à considérer ce que l’on appelle une structure de données en informatique.
Les structures de données dont le besoin s’est fait le plus ressentir au cours de l’histoire
(assez courte, certes) de la programmation apparaissent explicitement dans les langages
informatiques. Nous allons les étudier l’une après l’autre en montrant leur intérêt. Nous
réfléchirons plus tard à la notion générale de structure de données, en considérant en
particulier celles qui ne sont pas implémentées dans les langages de programmation.
7
CHAPITRE 1
RAPPELS ET COMPLÉMENTS DE C
Introduction
Dans ce chapitre nous allons exposés les concepts clés, necéssaires à la comprehension des
structures des données. Attention, il ne s’agit pas ici de faire un cours sur le langage C,
c’est juste une mise à jour. Soyez donc très attentifs.
1 i n t ∗ monPointeur ;
Comme je vous l’ai appris, il est important d’initialiser dès le début ses variables, en leur
donnant la valeur 0 par exemple. C’est encore plus important de le faire avec les pointeurs !
Pour initialiser un pointeur, c’est-à-dire lui donner une valeur par défaut, on n’utilise
généralement pas le nombre 0 mais le mot-clé NULL (veillez à l’écrire en majuscules) :
1 i n t ∗ monPointeur = NULL ;
Là, vous avez un pointeur initialisé à NULL. Comme ça, vous saurez dans la suite de
votre programme que votre pointeur ne contient aucune adresse.
8
Chargé du cours : M. Baïmi Badjoua ©UPM
Que se passe-t-il ? Ce code va réserver une case en mémoire comme si vous aviez créé une
variable normale. Cependant, et c’est ce qui change, la valeur du pointeur est faite pour
contenir une adresse. L’adresse… d’une autre variable.
Pourquoi pas l’adresse de la variable age ? Vous savez maintenant comment indiquer
l’adresse d’une variable au lieu de sa valeur (en utilisant le symbole &), alors allons-y !
Ça nous donne :
1 i n t age = 10 ;
2 i n t ∗ p o i n t e u r S u r A g e = &age ;
La première ligne signifie : « Créer une variable de type int dont la valeur vaut 10 ». La
seconde ligne signifie : « Créer une variable de type pointeur ici int* dont la valeur vaut
l’adresse de la variable age ».
La seconde ligne fait donc deux choses à la fois. Si vous le souhaitez, pour ne pas tout
mélanger, sachez qu’on peut la découper en deux temps :
1 i n t age = 10 ;
2 i n t ∗ p o i n t e u r S u r A g e ; // 1 ) s i g n i f i e ” Je c r é e un p o i n t e u r ”
3 p o i n t e u r S u r A g e = &age ; // 2 ) s i g n i f i e ” p o i n t e u r S u r A g e c o n t i e n t l ’ a d r e s s e
de l a v a r i a b l e age ”
Vous avez remarqué qu’il n’y a pas de type « pointeur » comme il y a un type int et un
type double. On n’écrit donc pas :
1 pointeur pointeurSurAge ;
Au lieu de ça, on utilise le symbole *, mais on continue à écrire int. Qu’est-ce que ça signi-
fie ? En fait, on doit indiquer quel est le type de la variable dont le pointeur va contenir
l’adresse. Comme notre pointeur pointeurSurAge va contenir l’adresse de la variable age
(qui est de type int), alors mon pointeur doit être de type int* ! Si ma variable age avait
été de type double, alors j’aurais dû écrire double *monPointeur.
Dans ce schéma, la variable age a été placée à l’adresse 177450 (vous voyez d’ailleurs que
sa valeur est 10), et le pointeur pointeurSurAge a été placé à l’adresse 3 (c’est tout à fait
le fruit du hasard).
Lorsque mon pointeur est créé, le système d’exploitation réserve une case en mémoire
comme il l’a fait pour age. La différence ici, c’est que la valeur de pointeurSurAge est un
peu particulière. Regardez bien le schéma : c’est l’adresse de la variable age !
Ceci, chers lecteurs, est le secret absolu de tout programme écrit en langage C. On y est,
nous venons de rentrer dans le monde merveilleux des pointeurs !
Ça ne transforme pas encore votre ordinateur en machine à café, certes. Seulement main-
tenant, on a un pointeurSurAge qui contient l’adresse de la variable age. Essayons de voir
ce que contient le pointeur à l’aide d’un printf :
1 i n t age = 10 ;
2 i n t ∗ p o i n t e u r S u r A g e = &age ;
3 p r i n t f ( ”%d” , p o i n t e u r S u r A g e ) ;
1 i n t age = 10 ;
2 i n t ∗ p o i n t e u r S u r A g e = &age ;
3 p r i n t f ( ”%d” , ∗ p o i n t e u r S u r A g e ) ;
À retenir absolument
Voici ce qu’il faut avoir compris et ce qu’il faut retenir pour la suite de ce chapitre :
— sur une variable, comme la variable age :
— age signifie : « Je veux la valeur de la variable age »,
— &age signifie : « Je veux l’adresse à laquelle se trouve la variable age » ;
— sur un pointeur, comme pointeurSurAge :
— pointeurSurAge signifie : « Je veux la valeur de pointeurSurAge » (cette valeur
étant une adresse),
— *pointeurSurAge signifie : « Je veux la valeur de la variable qui se trouve à
l’adresse contenue dans pointeurSurAge ».
Contentez-vous de bien retenir ces quatre points. Faites des tests et vérifiez que ça marche.
1 v o i d t r i p l e P o i n t e u r ( i n t ∗ pointeurSurNombre ) ;
2
3 i n t main ( i n t argc , c h a r ∗ argv [ ] )
4 {
5 i n t nombre = 5 ;
6 t r i p l e P o i n t e u r (&nombre ) ; // On e n v o i e l ’ a d r e s s e de nombre à l a
fonction
7 p r i n t f (”%d ” , nombre ) ; // On a f f i c h e l a v a r i a b l e nombre . La f o n c t i o n a
d i r e c t e m e n t m o d i f i é l a v a l e u r de l a v a r i a b l e c a r e l l e c o n n a i s s a i t
son a d r e s s e
8 return 0 ;
9 }
10
11 v o i d t r i p l e P o i n t e u r ( i n t ∗ pointeurSurNombre )
12 {
13 ∗ pointeurSurNombre ∗= 3 ; // On m u l t i p l i e par 3 l a v a l e u r de nombre
14 }
1. une variable nombre est créée dans le main. On lui affecte la valeur 5. Ça, vous
connaissez ;
2. on appelle la fonction triplePointeur. On lui envoie en paramètre l’adresse de notre
variable nombre ;
Il faut absolument que vous sachiez qu’il y a une autre façon d’écrire le code précédent,
en ajoutant un pointeur dans la fonction main :
1 v o i d t r i p l e P o i n t e u r ( i n t ∗ pointeurSurNombre ) ;
2
3 i n t main ( i n t argc , c h a r ∗ argv [ ] )
4 {
5 i n t nombre = 5 ;
6 i n t ∗ p o i n t e u r = &nombre ; // p o i n t e u r prend l ’ a d r e s s e de nombre
7 t r i p l e P o i n t e u r ( p o i n t e u r ) ; // On e n v o i e p o i n t e u r ( l ’ a d r e s s e de nombre )
à la fonction
8 p r i n t f ( ”%d” , ∗ p o i n t e u r ) ; // On a f f i c h e l a v a l e u r de nombre avec ∗
pointeur
9 return 0 ;
10 }
11
12 v o i d t r i p l e P o i n t e u r ( i n t ∗ pointeurSurNombre )
13 {
14 ∗ pointeurSurNombre ∗= 3 ; // On m u l t i p l i e par 3 l a v a l e u r de nombre
15 }
Ce qui compte, c’est d’envoyer l’adresse de la variable nombre à la fonction. Or, pointeur
vaut l’adresse de la variable nombre, donc c’est bon de ce côté ! On le fait seulement d’une
manière différente en créant un pointeur dans la fonction main.
Dans le printf(et c’est juste pour l’exercice), j’affiche le contenu de la variable nombre en
tapant *pointeur. Notez qu’à la place, j’aurais pu écrire nombre : le résultat aurait été
identique car *pointeur et nombre désignent la même chose dans la mémoire.
1 v o i d decoupeMinutes ( i n t ∗ p o i n t e u r H e u r e s , i n t ∗ p o i n t e u r M i n u t e s ) ;
2
Rien ne devrait vous surprendre dans ce code source. Toutefois, comme on n’est jamais
trop prudent, je vais râbacher une fois de plus ce qui se passe dans ce code afin d’être
certain que tout le monde me suive bien. C’est un chapitre important, vous devez faire
beaucoup d’efforts pour comprendre : je peux donc bien en faire moi aussi pour vous !
Enfin, chaque case du tableau contient un nombre du même type. Si le tableau est de
type int, alors chaque case du tableau contiendra un int. On ne peut pas faire de tableau
contenant à la fois des int et des double par exemple.
— Toutes les cases d’un tableau sont du même type. Ainsi, un tableau de int contien-
dra uniquement des int, et pas autre chose.
1 int tableau [ 4 ] ;
Voilà, c’est tout. Il suffit donc de rajouter entre crochets le nombre de cases que vous
voulez mettre dans votre tableau. Il n’y a pas de limite (à part peut-être la taille de votre
mémoire, quand même).
Maintenant, comment accéder à chaque case du tableau ? C’est simple, il faut écrire :
1 t a b l e a u [ numeroDeLaCase ]
Attention : un tableau commence à l’indice numéro 0 ! Notre tableau de 4 int a donc les
indices 0, 1, 2 et 3. Il n’y a pas d’indice 4 dans un tableau de 4 cases ! C’est une source
d’erreurs très courantes, souvenez-vous-en.
Si je veux mettre dans mon tableau les mêmes valeurs que celles indiquées sur la figure
précedente, je devrai donc écrire :
1 int tableau [ 4 ] ;
2 t a b l e a u [ 0 ] = 10 ;
3 t a b l e a u [ 1 ] = 23 ;
4 t a b l e a u [ 2 ] = 505 ;
5 tableau [ 3 ] = 8 ;
Quel est le rapport entre pointeur et tableau ? En fait, si vous écrivez juste tableau, vous
obtenez un pointeur. C’est un pointeur sur la première case du tableau. Faites le test :
1 int tableau [ 4 ] ;
2 p r i n t f ( ”%d” , t a b l e a u ) ;
En revanche, si vous indiquez l’indice de la case du tableau entre crochets, vous obtenez
la valeur :
1 int tableau [ 4 ] ;
2 p r i n t f ( ”%d” , t a b l e a u [ 0 ] ) ;
De même pour les autres indices. Notez que comme tableau est un pointeur, on peut
utiliser le symbole * pour connaître la première valeur :
1 int tableau [ 4 ] ;
2 p r i n t f ( ”%d” , ∗ t a b l e a u ) ;
Il est aussi possible d’obtenir la valeur de la seconde case avec *(tableau + 1)(adresse de
tableau + 1).
1 t a b l e a u [ 1 ] // Renvoie l a v a l e u r de l a s e c o n d e c a s e ( l a premi è r e c a s e é
tant 0)
2 ∗ ( t a b l e a u + 1 ) // I d e n t i q u e : r e n v o i e l a v a l e u r c o n t e n u e dans l a s e c o n d e
case
En clair, quand vous écrivez tableau[0], vous demandez la valeur qui se trouve à l’adresse
tableau + 0 case (c’est-à-dire 1600).
Si vous écrivez tableau[1], vous demandez la valeur se trouvant à l’adresse tableau + 1
case (c’est-à-dire 1601).
Et ainsi de suite pour les autres valeurs.
1 int t a i l l e = 5 ;
2 int tableau [ t a i l l e ] ;
Or cela n’est pas forcément reconnu par tous les compilateurs, certains planteront sur la
seconde ligne. Le langage C que je vous enseigne depuis le début (appelé le C89) n’autorise
pas ce genre de choses. Nous considèrerons donc que faire cela est interdit.
Nous allons nous mettre d’accord sur ceci : vous n’avez pas le droit d’utiliser une variable
entre crochets pour la définition de la taille du tableau, même si cette variable est une
constante ! Le tableau doit avoir une dimension fixe, c’est-à-dire que vous devez écrire noir
sur blanc le nombre correspondant à la taille du tableau :
1 int tableau [ 5 ] ;
Mais alors… il est interdit de créer un tableau dont la taille dépend d’une variable ?
Non, rassurez-vous : c’est possible, même en C89. Mais pour faire cela, nous utiliserons
une autre technique (plus sûre et qui marche partout) appelée l’allocation dynamique.
Nous verrons cela bien plus loin dans ce chapitre.
Elle consiste à écrire tableau[4] = valeur1, valeur2, valeur3, valeur4. En clair, vous placez
les valeurs une à une entre accolades, séparées par des virgules :
1 // P r o t o t y p e de l a f o n c t i o n d ’ a f f i c h a g e
2 void a f f i c h e ( i n t ∗ tableau , i n t t a i l l e T a b l e a u ) ;
3
4 i n t main ( i n t argc , c h a r ∗ argv [ ] )
5 {
6 i n t t a b l e a u [ 4 ] = { 1 0 , 1 5 , 3} ;
7 // On a f f i c h e l e contenu du t a b l e a u
8 a f f i c h e ( tableau , 4) ;
9 return 0 ;
10 }
11
12 void a f f i c h e ( i n t ∗ tableau , i n t t a i l l e T a b l e a u )
13 {
14 int i ;
15 f o r ( i = 0 ; i < t a i l l e T a b l e a u ; i ++)
16 {
17 p r i n t f (”%d\n ” , t a b l e a u [ i ] ) ;
18 }
19 }
— Les tableaux sont des ensembles de variables du même type stockées côte à côte
en mémoire.
— La taille d’un tableau doit être déterminée avant la compilation, elle ne peut pas
dépendre d’une variable.
— Chaque case d’un tableau de type int contient une variable de type int.
— Les cases sont numérotées via des indices commençant à 0 : tableau[0], tableau[1],
tableau[2], etc.
retenir sous forme de variable en mémoire. On pourrait ainsi stocker le nom de l’utilisateur.
Comme nous l’avons dit plus tôt, notre ordinateur ne peut retenir que des nombres. Les
lettres sont exclues. Comment diable les programmeurs font-ils pour manipuler du texte,
alors ? Eh bien ils sont malins, vous allez voir !
Afficher un caractère
La fonction printf, qui n’a décidemment pas fini de nous étonner, peut aussi afficher un
caractère. Pour cela, on doit utiliser le symbole %c (c comme caractère) :
On peut aussi demander à l’utilisateur d’entrer une lettre en utilisant le %c dans un scanf :
Voici à peu près tout ce qu’il faut savoir sur le type char :
— le type char permet de stocker des nombres allant de -128 à 127, unsigned char des
nombres de 0 à 255 ;
— il y a une table que votre ordinateur utilise pour convertir les lettres en nombres
et inversement, la table ASCII ;
— on peut donc utiliser le type char pour stocker UNE lettre ;
— ’A’ est remplacé à la compilation par la valeur correspondante (65 en l’occurrence).
On utilise donc les apostrophes pour obtenir la valeur d’une lettre.
Si on crée un tableau :
1 char chaine [ 5 ] ;
et qu’on met dans chaine[0] la lettre ’S’, dans chaine[1] la lettre ’a’… on peut ainsi former
une chaîne de caractères, c’est-à-dire du texte.
La figure suivante vous donne une idée de la façon dont la chaîne est stockée en mémoire
(attention : je vous préviens de suite, c’est un peu plus compliqué que ça en réalité, je
vous explique après pourquoi).
Comme on peut le voir, c’est un tableau qui prend 5 cases en mémoire pour représenter
le mot « Salut ». Pour la valeur, j’ai volontairement écrit sur le schéma les lettres entre
apostrophes pour indiquer que c’est un nombre qui est stocké, et non une lettre. En réa-
lité, dans la mémoire, ce sont bel et bien les nombres correspondant à ces lettres qui sont
stockés.
Toutefois, une chaîne de caractères ne contient pas que des lettres ! Le schéma de la figure
précedente est en fait incomplet. Une chaîne de caractère doit impérativement contenir un
caractère spécial à la fin de la chaîne, appelé « caractère de fin de chaîne ». Ce caractère
s’écrit ’\0’.
Comme vous le voyez, la chaîne prend 6 caractères et non pas 5, il va falloir s’y faire. La
chaîne se termine par ’\0’, le caractère de fin de chaîne qui permet d’indiquer à l’ordina-
teur que la chaîne se termine là.
Voyez le caractère ’\0’ comme un avantage. Grâce à lui, vous n’aurez pas à retenir la taille
de votre tableau car il indique que le tableau s’arrête à cet endroit. Vous pourrez passer
votre tableau de char à une fonction sans avoir à ajouter à côté une variable indiquant la
taille du tableau.
Cela n’est valable que pour les chaînes de caractères (c’est-à-dire le type char*, qu’on
peut aussi écrire char[]). Pour les autres types de tableaux, vous êtes toujours obligés de
retenir la taille du tableau quelque part.
4 chaine [2] = ’l ’ ;
5 chaine [3] = ’u ’ ;
6 chaine [4] = ’t ’ ;
7 chaine [5] = ’ \0 ’ ;
Voici le code complet qui crée une chaîne « Salut » en mémoire et qui l’affiche :
1 #i n c l u d e <s t d i o . h>
2 #i n c l u d e < s t d l i b . h>
3
Pour initialiser une chaîne, il existe heureusement une méthode plus simple :
1 #i n c l u d e <s t r i n g . h>
— ...
Résumé sur les chaînes de caractères :
— Un ordinateur ne sait pas manipuler du texte, il ne connaît que les nombres. Pour
régler le problème, on associe à chaque lettre de l’alphabet un nombre correspon-
dant dans une table appelée la table ASCII.
— Le type char est utilisé pour stocker une et une seule lettre. Il stocke en réalité un
nombre mais ce nombre est automatiquement traduit par l’ordinateur à l’affichage.
— Pour créer un mot ou une phrase, on doit construire une chaîne de caractères. Pour
cela, on utilise un tableau de char.
— Toute chaîne de caractère se termine par un caractère spécial appelé \0 qui signifie
« fin de chaîne ».
— Il existe de nombreuses fonctions toutes prêtes de manipulation des chaînes dans
la bibliothèque string. Il faut inclure string.h pour pouvoir les utiliser.
Créer de nouveaux types de variables devient indispensable quand on cherche à faire des
programmes plus complexes.
Il faut savoir que les bibliothèques définissent généralement leurs propres types. Vous ne
tarderez donc pas à manipuler un type de variable Fichier ou encore, un peu plus tard,
d’autres de types Fenetre, Audio, Clavier, etc.
1 s t r u c t NomDeVotreStructure
2 {
3 int variable1 ;
4 int variable2 ;
5 int autreVariable ;
6 d o u b l e nombreDecimal ;
7 };
Exemple de structure
Imaginons par exemple que vous vouliez créer une variable qui stocke les coordonnées d’un
point à l’écran. Vous aurez très certainement besoin d’une structure comme cela lorsque
vous ferez des jeux 2D dans la partie suivante, c’est donc l’occasion de s’avancer un peu.
Êtes-vous capables d’écrire une structure Coordonnees qui permette de stocker à la fois
la valeur de l’abscisse (x) et celle de l’ordonnée (y) d’un point ?
1 s t r u c t Coordonnees
2 {
3 i n t x ; // A b s c i s s e s
4 i n t y ; // Ordonn é e s
5 };
Allez, imaginons une structure Personne qui stockerait diverses informations sur une per-
sonne :
1 s t r u c t Personne
2 {
3 c h a r nom [ 1 0 0 ] ;
4 c h a r prenom [ 1 0 0 ] ;
5 char a d r e s s e [ 1 0 0 0 ] ;
6
7 i n t age ;
8 i n t g e n r e ; // Bool é en : 1 = g a r ç on , 0 = f i l l e
9 };
1 #i n c l u d e ” main . h” // I n c l u s i o n du . h q u i c o n t i e n t l e s p r o t o t y p e s e t
structures
2
3 i n t main ( i n t argc , c h a r ∗ argv [ ] )
4 {
5 s t r u c t Coordonnees p o i n t ; // Cr é a t i o n d ’ une v a r i a b l e ” p o i n t ” de type
Coordonnees
6 return 0 ;
7 }
Le typedef
Retournons dans le fichier.h qui contient la définition de notre structure de type Coor-
donnees. Nous allons ajouter une instruction appelée typedef qui sert à créer un alias de
structure, c’est-à-dire à dire qu’écrire telle chose équivaut à écrire telle autre chose.
Nous allons ajouter une ligne commençant par typedef juste avant la définition de la
structure :
1 t y p e d e f s t r u c t Coordonnees Coordonnees ;
2 s t r u c t Coordonnees
3 {
4 int x ;
5 int y ;
6 };
En clair, cette ligne (1) dit « Écrire le mot Coordonnees est désormais équivalent à écrire
struct Coordonnees ». En faisant cela, vous n’aurez plus besoin de mettre le mot struct
à chaque définition de variable de type Coordonnees. On peut donc retourner dans notre
main et écrire tout simplement :
2 {
3 Coordonnees p o i n t ; // L ’ o r d i n a t e u r comprend qu ’ i l s ’ a g i t de ” s t r u c t
Coordonnees ” g r â c e au t y p e d e f
4 return 0 ;
5 }
Si on prend la structure Personne que nous avons vue tout à l’heure et qu’on demande le
nom et le prénom, on devra faire comme ça :
ordinaires ;
— un tableau : on met chacune de ses valeurs à 0.
Pour les structures, l’initialisation va un peu ressembler à celle d’un tableau. En effet, on
peut faire à la déclaration de la variable :
1 Coordonnees p o i n t = { 0 , 0} ; // Dans l ’ o r d r e , p o i n t . x = 0 e t p o i n t . y = 0 .
Revenons à la structure Personne (qui contient des chaînes). Vous avez aussi le droit
d’initialiser une chaîne en écrivant juste ”” (rien entre les guillemets). Je ne vous ai pas
parlé de cette possibilité dans la section sur les chaînes, mais il n’est pas trop tard pour
l’apprendre. On peut donc initialiser dans l’ordre nom, prenom, adresse, age et garcon
comme ceci :
1 Personne u t i l i s a t e u r = { ” ” , ” ” , ” ” , 0 , 0} ;
1 Coordonnees ∗ p o i n t = NULL ;
On va faire ceci pour cet exemple : on va simplement créer une variable de type Coor-
donnees dans le main et envoyer son adresse à initialiserCoordonnees. Cette fonction aura
pour rôle de mettre tous les éléments de la structure à 0.
Ma variable monPoint est donc créée dans le main. On envoie son adresse à la fonction
initialiserCoordonnees qui récupère cette variable sous la forme d’un pointeur appelé point
(on aurait d’ailleurs pu l’appeler n’importe comment dans la fonction, cela n’aurait pas
eu d’incidence).
Bien : maintenant que nous sommes dans initialiserCoordonnees, nous allons initialiser
chacune des valeurs une à une. Il ne faut pas oublier de mettre une étoile devant le nom
du pointeur pour accéder à la variable. Si vous ne le faites pas, vous risquez de modifier
l’adresse, et ce n’est pas ce que nous voulons faire.
Oui mais voilà, problème… On ne peut pas vraiment faire :
1 v o i d i n i t i a l i s e r C o o r d o n n e e s ( Coordonnees ∗ p o i n t )
2 {
3 ∗ point . x = 0 ;
4 ∗ point . y = 0 ;
5 }
Ce serait trop facile… Pourquoi on ne peut pas faire ça ? Parce que le point de séparation
s’applique sur le mot point et non sur *point en entier. Or, nous ce qu’on veut, c’est
accéder à *point pour en modifier la valeur. Pour régler le problème, il faut placer des
parenthèses autour de *point. Comme cela, le point de séparation s’appliquera à *point
et non juste à point :
1 v o i d i n i t i a l i s e r C o o r d o n n e e s ( Coordonnees ∗ p o i n t )
2 {
3 (∗ point ) . x = 0 ;
4 (∗ point ) . y = 0 ;
5 }
1 (∗ point ) . x = 0 ;
2 // EST EQUIVALENT A
3 p o i n t −>x = 0 ;
Reprenons notre fonction initialiserCoordonnees. Nous pouvons donc l’écrire comme ceci :
1 v o i d i n i t i a l i s e r C o o r d o n n e e s ( Coordonnees ∗ p o i n t )
2 {
3 p o i n t −>x = 0 ;
4 p o i n t −>y = 0 ;
5 }
Retenez bien ce raccourci de la flèche, nous allons le réutiliser un certain nombre de fois. Et
surtout, ne confondez pas la flèche avec le « point ». La flèche est réservée aux pointeurs,
le « point » est réservé aux variables. Utilisez ce petit exemple pour vous en souvenir :
Une énumération ne contient pas de « sous-variables » comme c’était le cas pour les
structures. C’est une liste de « valeurs possibles » pour une variable. Une énumération ne
prend donc qu’une case en mémoire et cette case peut prendre une des valeurs que vous
définissez (et une seule à la fois).
Vous noterez qu’on utilise un typedef là aussi, comme on l’a fait jusqu’ici.
Pour créer une énumération, on utilise le mot-clé enum. Notre énumération s’appelle ici
Volume. C’est un type de variable personnalisé qui peut prendre une des trois valeurs
qu’on a indiquées : soit FAIBLE, soit MOYEN, soit FORT.
On va pouvoir créer une variable de type Volume, par exemple musique, qui stockera
le volume actuel de la musique. On peut par exemple initialiser la musique au volume
MOYEN :
Voilà qui est fait. Plus tard dans le programme, on pourra modifier la valeur du volume
et la mettre soit à FAIBLE, soit à FORT.
En effet, c’est assez similaire mais ce n’est pourtant pas exactement la même chose. Le
compilateur associe automatiquement un nombre à chacune des valeurs possibles de l’énu-
mération.
Dans le cas de notre énumération Volume, FAIBLE vaut 0, MOYEN vaut 1 et FORT vaut
2. L’association est automatique et commence à 0.
Contrairement au #define, c’est le compilateur qui associe MOYEN à 1 par exemple, et
non le préprocesseur. Au bout du compte, ça revient un peu au même. En fait, quand on
a initialisé la variable musique à MOYEN, on a donc mis la case en mémoire à la valeur
1.
En pratique, est-ce utile de savoir que MOYEN vaut 1, FORT vaut 2, etc. ?
Non. En général ça nous est égal. C’est le compilateur qui associe automatiquement un
nombre à chaque valeur. Grâce à ça, vous n’avez plus qu’à écrire :
1 i f ( musique == MOYEN)
2 {
3 // Jouer l a musique au volume moyen
4 }
Peu importe la valeur de MOYEN, vous laissez le compilateur se charger de gérer les
nombres.
L’intérêt de tout ça ? C’est que de ce fait votre code est très lisible. En effet, tout le
monde peut facilement lire le if précédent (on comprend bien que la condition signifie «
Si la musique est au volume moyen »).
Quel intérêt est-ce que ça peut bien avoir ? Eh bien supposons que sur votre ordinateur, le
volume soit géré entre 0 et 100 (0 = pas de son, 100 = 100 % du son). Il est alors pratique
d’associer une valeur précise à chaque élément :
Il est impératif de bien savoir manipuler les pointeurs pour pouvoir lire ce chapitre ! Si
vous avez encore des doutes sur les pointeurs, je vous recommande d’aller relire la section
correspondant avant de commencer.
1 i n t monNombre = 0 ;
Jusqu’ici, les choses étaient automatiques. Lorsqu’on déclarait une variable, le système
d’exploitation était automatiquement appelé par le programme. Que diriez-vous de faire
cela manuellement ? Non pas par pur plaisir de faire quelque chose de compliqué (même si
c’est tentant !), mais plutôt parce que nous allons parfois être obligés de procéder comme
cela.
1 sizeof ( int ) ;
À la compilation, cela sera remplacé par un nombre : le nombre d’octets que prendinten
mémoire. Chez moi, sizeof(int) vaut 4, ce qui signifie que int occupe 4 octets. Chez vous,
c’est probablement la même valeur, mais ce n’est pas une règle. Testez pour voir, en
affichant la valeur à l’aide d’unprintfpar exemple :
1 p r i n t f ( ” c h a r : %d o c t e t s \n” , s i z e o f ( c h a r ) ) ;
2 p r i n t f ( ” i n t : %d o c t e t s \n” , s i z e o f ( i n t ) ) ;
3 p r i n t f ( ” l o n g : %d o c t e t s \n” , s i z e o f ( l o n g ) ) ;
4 p r i n t f ( ” d o u b l e : %d o c t e t s \n” , s i z e o f ( d o u b l e ) ) ;
Peut-on afficher la taille d’un type personnalisé qu’on a créé (une structure) ?
1 t y p e d e f s t r u c t Coordonnees Coordonnees ;
2 s t r u c t Coordonnees
3 {
4 int x ;
5 int y ;
6 };
7
8 i n t main ( i n t argc , c h a r ∗ argv [ ] )
9 {
10 p r i n t f ( ” Coordonnees : %d o c t e t s \n” , s i z e o f ( Coordonnees ) ) ;
11 return 0 ;
12 }
1 i n t nombre = 18 ;
… et que sizeof(int) indique 4 octets sur notre ordinateur, alors la variable occupera 4
octets en mémoire !
Supposons que la variable nombre soit allouée à l’adresse 1600 en mémoire. On aurait
alors le schéma de la figure suivante.
Si on avait fait la même chose avec un char, on n’aurait alors occupé qu’un seul octet en
mémoire (figure suivante).
Imaginez maintenant un tableau de int ! Chaque « case » du tableau occupera 4 octets.
Si notre tableau fait 100 cases :
Il est important de bien comprendre ces petits calculs pour la suite de cette section.
On va avoir besoin d’inclure la bibliothèque <stdlib.h>. Si vous avez suivi mes conseils,
vous devriez avoir inclus cette bibliothèque dans tous vos programmes, de toute façon.
Cette bibliothèque contient deux fonctions dont nous allons avoir besoin :
Quand vous faites une allocation manuelle de mémoire, vous devez toujours suivre ces
trois étapes :
Le principe est exactement le même qu’avec les fichiers : on alloue, on vérifie si l’allocation
a marché, on utilise la mémoire, puis on la libère quand on a fini de l’utiliser.
Exemple :
1 i n t ∗ memoireAllouee = NULL ; // On c r é e un p o i n t e u r s u r i n t
2 memoireAllouee = m a l l o c ( s i z e o f ( i n t ) ) ; // La f o n c t i o n m a l l o c i n s c r i t dans
notre pointeur l ’ adresse qui a é t é r e s e r v é e .
À la fin de ce code, memoireAllouee est un pointeur contenant une adresse qui vous a été
réservée par l’OS, par exemple l’adresse 1600 pour reprendre mes schémas précédents.
On va utiliser une fonction standard qu’on n’avait pas encore vue jusqu’ici : exit(). Elle ar-
rête immédiatement le programme. Elle prend un paramètre : la valeur que le programme
doit retourner (cela correspond en fait au return du main()).
Si le pointeur est différent de NULL, le programme peut continuer, sinon il faut afficher un
message d’erreur ou même mettre fin au programme, parce qu’il ne pourra pas continuer
correctement s’il n’y a plus de place en mémoire.
La fonction free a juste besoin de l’adresse mémoire à libérer. On va donc lui envoyer
notre pointeur, c’est-à-dire memoireAllouee dans notre exemple. Voici le schéma complet
et final, ressemblant à s’y méprendre à ce qu’on a vu dans le chapitre sur les fichiers :
Revenons à notre code. On y a alloué dynamiquement une variable de type int. Au final,
ce qu’on a écrit revient exactement au même que d’utiliser la méthode « automatique »
qu’on connaît bien maintenant :
Je vous rassure : dans la suite du cours, nous aurons l’occasion d’utiliser le malloc pour
des choses bien plus intéressantes que le stockage de l’âge de ses amis !
— Une variable occupe plus ou moins d’espace en mémoire en fonction de son type.
— On peut connaître le nombre d’octets occupés par un type à l’aide de l’opérateur
sizeof().
— L’allocation dynamique consiste à réserver manuellement de l’espace en mémoire
pour une variable ou un tableau.
— L’allocation est effectuée avec malloc() et il ne faut surtout pas oublier de libérer
la mémoire avec free() dès qu’on n’en a plus besoin.
— L’allocation dynamique permet notamment de créer un tableau dont la taille est
déterminée par une variable au moment de l’exécution.
Conclusion
Nous avons dans ce chapitre, rappelé les notions de base et complémentaires du langage
C. Ces notions sont très importantes voire indispensables pour la comprehension de ce
cours. Relisez ce chapitre au temps de fois qu’il le faut pour asssimiler tous les concepts
exposés dans ce chapitre. Sans quoi vous aurez des sérieux problèmes pour comprendre la
suite des chapitres.
Introduction
Vous venez d’apprendre les bases d’un langage de programmation ? Vous vous êtes peut-
être rendu compte que parfois, en modifiant un peu votre programme, vous pouvez obtenir
le même résultat mais 2, 10 ou 1000 fois plus vite ?
De telles améliorations ne sont pas le fruit du hasard, ni même dues à une augmentation
de la mémoire vive ou à un changement de processeur : il y a plusieurs manières de pro-
grammer quelque chose et certaines sont incroyablement meilleures que d’autres.
Avec un peu de réflexion, et des outils théoriques de base, vous serez vous aussi en mesure
de faire de bons choix pour vos programmes. À la fin de ce cours, vous serez de meilleurs
développeurs, en mesure de comprendre, corriger et concevoir des programmes plus effi-
caces.
L’un des éléments important à connaitre pour développer des logiciels optimisés (qui
consomme moins de mémoire et qui prennent moins de temps à s’exécuter) est l’algorithme
qui est l’objet de ce chapitre.
Dans la vie de tous les jours, nous avons souvent besoin de résoudre des problèmes. Surtout
si on considère la notion de ”problème” au sens large.
42
Chargé du cours : M. Baïmi Badjoua ©UPM
J’ai décrit une solution au problème ”il faut faire cuire du riz”, sous forme de concepts
simples. Vous remarquerez qu’il y a pourtant beaucoup de choses implicites : j’ai précisé
que vous étiez au départ en possession du riz, mais il faut aussi une casserole, de l’eau,
etc. On peut se trouver dans des situations spécifiques où tous ces objets ne sont pas
disponibles, et il faudra alors utiliser un autre algorithme (ou commencer par construire
une casserole...).
Les instructions que j’ai utilisées sont ”précises”, mais on pourrait préciser moins de
choses, ou plus. Comment fait-on pour remplir une casserole d’eau, plus précisément ? Si
le cuisinier à qui la recette est destinée ne sait pas interpréter la ligne ”remplir une casse-
role d’eau”, il faudra l’expliquer en termes plus simples (en expliquant comment utiliser
le robinet, par exemple).
De même, quand vous programmez, le degré de précision que vous utilisez dépend de
nombreux paramètres : le langage que vous utilisez, les bibliothèques que vous avez à
disposition, etc.
continuellement.
Vous aurez peut-être pensé au célèbre moteur de recherche Google (qui a initialement
dominé le marché grâce aux capacités de son algorithme de recherche), mais ce genre
d’activités n’est pas restreint au (vaste) secteur d’Internet : quand vous jouez à un jeu de
stratégie en temps réel, et que vous ordonnez à une unité de se déplacer, l’ordinateur a en
sa possession plusieurs informations (la structure de la carte, le point de départ, le point
d’arrivée) et il doit produire une nouvelle information : l’itinéraire que doit suivre l’unité.
Il y a des liens forts entre les algorithmes (qui décrivent des méthodes) et les structures
de données (qui décrivent une organisation). Typiquement, certaines structures de don-
nées sont indispensables à la mise en place de certaines méthodes, et à l’inverse certains
algorithmes sont nécessaires aux structures de données : par exemple, si on veut rajouter
un mot dans un dictionnaire classé alphabétiquement, on ne peut pas juste l’écrire dans
l’espace libre sur la dernière page, il faut utiliser un algorithme pour l’ajouter au bon
endroit. L’étude des structures de données est donc inséparable de celle des algorithmes,
et vous n’y échapperez pas.
Un algorithme est une manière de résoudre un problème. C’est tout. Vous êtes déçu·e ?
Vous pensiez que vous alliez vous transformer en savant fou à la fin de ce cours ? Pas
vraiment !
Avant tout, il ne se plaindra pas si vous lui demandez de réaliser des actions répétitives.
Au contraire ! Déplacer vos 2 000 dernières photos de voyage une à une ? Facile ! Chercher
dans ce PDF de 200 pages toutes les occurrences de l’expression ”max tout puissant” ? Du
gâteau ! Imaginez si vous deviez en faire de même ! Transférer 2 000 photos d’un album
papier à un autre vous ennuierait à mourir...
Une seconde raison, et non des moindres : l’ordinateur va bien plus vite que nous. Il est
plus efficace ! Essayez de calculer mentalement 10 x 20 x 30 x 40 et comparez votre temps
de calcul à celui d’un ordinateur. Imbattable !
Qu’auriez-vous fait sans ordinateur ? Vous auriez déplié une carte papier et déterminé
l’itinéraire idéal en prenant en compte certains paramètres : vitesse maximale autorisée
sur chaque portion de route, péages, position actuelle. Vous auriez ensuite choisi le trajet
le plus court en fonction de tous ces paramètres.
Bravo, vous pourriez remplacer l’ordinateur ! Mais vous auriez certainement réfléchi dix
bonnes minutes... laissant ainsi à la tarte le temps de refroidir, seule et abandonnée dans
la cuisine de votre grand-mère. Heureusement, des informaticiens ont confectionné un
algorithme et l’ont implémenté dans le petit boîtier sur votre pare-brise !
Site web
Vous avez certainement remarqué que Facebook adapte votre fil d’actualité en fonction
de votre activité. Votre page d’accueil n’affiche pas les dernières publications de vos amis,
mais bien ce que Facebook considère comme étant le contenu le plus pertinent pour vous
compte tenu de votre activité.
De même, lorsque vous effectuez une recherche sur Google, vous vous attendez à ce que la
page affiche les résultats les plus pertinents et non les derniers sites parus sur le domaine.
Google a donc besoin d’un modèle pour déterminer comment calculer la pertinence de
ces résultats en prenant en compte plusieurs paramètres tels que votre historique de re-
cherche, le nombre de visites sur un site, le nombre de liens pointant vers le site, etc.
Ces deux exemples utilisent des méthodes de machine learning (ou apprentissage auto-
matique) extrêmement intéressantes.
Déplacement
Votre GPS intègre un algorithme qui lui permet de déterminer le plus court chemin entre
votre position actuelle et celle que vous lui avez indiquée. De même, les nombreux sites
de réservation en ligne intègrent un algorithme qui gère les places libres dans un train,
mais également les correspondances et les problèmes éventuels (annulation, par exemple).
Les algorithmes sont également utilisés dans des logiciels de reconnaissance d’image ou
par votre banque lorsque vous effectuez des paiements sur Internet (détection de fraude).
C’est très puissant ! Nous pourrions trouver bien plus d’exemples d’algorithmes intégrés
à notre vie quotidienne. Leur point commun : répondre à une problématique que nous
nous posons par l’utilisation d’un programme.
Les actions effectuées par un programme sont des instructions. Une instruction peut être
une opération de base, une exécution conditionnelle ou une itération.
Exemple d’algorithme :
— la lecture
— l’affichage
— l’affectation de variables
— les tests
— les boucles
milliers de langage de programmation, ayant chacun ses spécificités. On peut citer par
exemple :
Un compilateur est un programme informatique qui transforme un code source écrit dans
un langage de programmation (le langage source) en langage machine (le langage cible).
Dans le cas de langage semi-compilé (ou intermédiaire), le code source est traduit en un
langage intermédiaire, sous forme binaire, avant d’être lui-même interprété ou compilé.
Un interpréteur se distingue d’un compilateur par le fait que, pour exécuter un programme,
les opérations d’analyse et de traduction sont réalisées à chaque exécution du programme
(par un interpréteur) plutôt qu’une fois pour toutes (par un compilateur).
Conclusion
Nous avons vu dans ce cours quelques notions d’algorithme. Il convient de préciser qu’un
algorithme est une suite finie d’instructions, qui une fois exécutée correctement, conduit
à un résultat donné. Pour fonctionner, un algorithme doit donc contenir uniquement des
instructions compréhensibles par celui qui devra l’exécuter.
Nous avons aussi montré l’interêt des algorithmes. En effet, ce sont les algorithmes, une
fois bien redigés qui nous permettrons de développer des logiciels de qualité.
Dans le chapitre suivant, nous allons parlé d’une notion particulière des algorithmes : la
complexité.
Introduction
L’algorithmique est defini comme l’étude des algorithmes. Un algorithme est quant à lui,
une suite d’instructions finies qui décrit comment résoudre un problème particulier en un
temps fini.
Dans le cas de guide touristique, si l’algorithme est juste, le résultat est le résultat voulu,
et le touriste se retrouve là où il voulait aller. Si l’algorithme est faux, alors, le résultat est
aléatoire, et le touriste est perdu. Il est donc important de comprendre que pour fonction-
ner, un algorithme doit contenir uniquement des instructions compréhensibles par celui
qui devra l’exécuter.
49
Chargé du cours : M. Baïmi Badjoua ©UPM
Le raisonnement à l’origine du code peut être décrit par l’exemple suivant de tâche :
Décider si un tableau L est trié en ordre croissant. Pour ce faire :
— Raisonnement : un tableau L est trié si tous ses éléments sont dans l’ordre croissant.
L trié ⇐⇒ ∀i 0 < i < |L| et L[i] < L[i + 1]
— Algorithme : une fonction vérifiant cette propriété, supposera donc le tableau L,
de taille n, trié au départ et cherchera une contradiction.
1 F o n c t i o n t r i e (L : tab , n : e n t i e r ) : b o o l e e n
2 V a r i a b l e s i , n : e n t i e r ; Ok : b o o l e e n
3 Dé but
4 Ok <−− v r a i
5 pour i de 1 à n f a i r e
6 s i (L [ i ] > L [ i +1]) a l o r s
7 Ok <−− Faux
8 Finsi
9 Finpour
10 Retourner OK
11 Fin t r i e
— Code :
1 b o o l e a n t r i e ( tab L , i n t n )
2 {
3 OK = t r u e ;
4 f o r ( i =0 ; i < n ; i ++)
5 {
6 i f (L [ i ] > L [ i +1])
7 {
8 OK = f a l s e ;
9 }
10 }
11 r e t u r n OK ;
12 }
Cependant, on attend d’un d’un algorithme qu’il résolve correctement et de manière ef-
ficace le problème à résoudre, quelles que soient les données à traiter. Ce qui peut se
résumer en deux points essentiels :
1. La correction : l’algorithme résout il bien le problème donné ? Il faut donc trouver
une méthode de résolution (exacte ou approchée) du problème.
2. L’efficacité : en combien de temps et avec quelles ressources ? Il est souhaitable
que nos solutions ne soient pas lentes et ne prennent pas de l’espace mémoire
considérable.
Il convient donc de le préciser : ”Savoir résoudre un problème est une chose, le résoudre
efficacement en est une autre !”
Conclusion
Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.
Introduction
Pour stocker des données en mémoire, nous avons utilisé des variables simples (type int,
double…), des tableaux et des structures personnalisées. Si vous souhaitez stocker une
série de données, le plus simple est en général d’utiliser des tableaux.
Toutefois, les tableaux se révèlent parfois assez limités. Par exemple, si vous créez un
tableau de 10 cases et que vous vous rendez compte plus tard dans votre programme que
vous avez besoin de plus d’espace, il sera impossible d’agrandir ce tableau. De même, il
n’est pas possible d’insérer une case au milieu du tableau.
Les listes chaînées représentent une façon d’organiser les données en mémoire de manière
beaucoup plus flexible. Comme à la base le langage C ne propose pas ce système de
stockage, nous allons devoir le créer nous-mêmes de toutes pièces. C’est un excellent
exercice qui vous aidera à être plus à l’aise avec le langage.
54
Chargé du cours : M. Baïmi Badjoua ©UPM
Comme je vous le disais en introduction, le problème des tableaux est qu’ils sont figés.
Il n’est pas possible de les agrandir, à moins d’en créer de nouveaux, plus grands (fig.
suivante). De même, il n’est pas possible d’y insérer une case au milieu, à moins de
décaler tous les autres éléments.
Le langage C ne propose pas d’autre système de stockage de données, mais il est possible
de le créer soi-même de toutes pièces. Encore faut-il savoir comment s’y prendre : c’est
justement ce que ce chapitre et les suivants vous proposent de découvrir.
Une liste chaînée est un moyen d’organiser une série de données en mémoire. Cela consiste
à assembler des structures en les liant entre elles à l’aide de pointeurs. On pourrait les
représenter comme ceci :
Je reconnais que tout cela est encore très théorique et doit vous paraître un peu flou pour
le moment. Retenez simplement comment les éléments sont agencés entre eux : ils forment
une chaîne de pointeurs, d’où le nom de « liste chaînée ».
NB : Contrairement aux tableaux, les éléments d’une liste chaînée ne sont pas placés côte
à côte dans la mémoire. Chaque case pointe vers une autre case en mémoire qui n’est pas
nécessairement stockée juste à côté.
faire ici fait appel à des techniques du langage C que vous connaissez déjà. Il n’y a aucun
élément nouveau, nous allons nous contenter de créer nos propres structures et fonctions
et les transformer en un système logique, capable de se réguler tout seul.
1 t y p e d e f s t r u c t é l é ment é l é ment ;
2 s t r u c t é l é ment
3 {
4 i n t nombre ;
5 é l é ment ∗ s u i v a n t ;
6 };
Nous avons créé ici un élément d’une liste chaînée, correspondant à la fig. suivante que
nous avons vue plus tôt. Que contient cette structure ?
— Une donnée, ici un nombre de type int : on pourrait remplacer cela par n’importe
quelle autre donnée (un double, un tableau…). Cela correspond à ce que vous voulez
stocker, c’est à vous de l’adapter en fonction des besoins de votre programme.
— Un pointeur vers un élément du même type appelé suivant. C’est ce qui permet de
lier les éléments les uns aux autres : chaque élément « sait » où se trouve l’élément
suivant en mémoire. Comme je vous le disais plus tôt, les cases ne sont pas côte
à côte en mémoire. C’est la grosse différence par rapport aux tableaux. Cela offre
davantage de souplesse car on peut plus facilement ajouter de nouvelles cases par
la suite au besoin.
Cette structure Liste contient un pointeur vers le premier élément de la liste. En effet,
il faut conserver l’adresse du premier élément pour savoir où commence la liste. Si on
connaît le premier élément, on peut retrouver tous les autres en « sautant » d’élément en
élément à l’aide des pointeurs suivant.
Nous n’aurons besoin de créer qu’un seul exemplaire de la structureListe. Elle permet de
contrôler toute la liste (fig. suivante).
Il serait possible d’ajouter dans la structure Liste un pointeur vers le dernier élément.
Toutefois, il y a encore plus simple : il suffit de faire pointer le dernier élément de la
liste vers NULL, c’est-à-dire de mettre son pointeursuivant à NULL. Cela nous permet
de réaliser un schéma enfin complet de notre structure de liste chaînée (fig. suivante).
Je vous propose la fonction ci-dessous, que nous commenterons juste après, bien entendu :
1 Liste ∗ i n i t i a l i s a t i o n ()
2 {
3 L i s t e ∗ l i s t e = malloc ( s i z e o f (∗ l i s t e ) ) ;
4 Element ∗ e l e m e n t = m a l l o c ( s i z e o f ( ∗ e l e m e n t ) ) ;
5
6 i f ( l i s t e == NULL | | e l e m e n t == NULL)
7 {
8 e x i t (EXIT_FAILURE) ;
9 }
10
11 element−>nombre = 0 ;
12 element−>s u i v a n t = NULL ;
13 l i s t e −>p r e m i e r = e l e m e n t ;
14
15 return l i s t e ;
16 }
NB : Notez que le type de données est Liste et que la variable s’appelle liste. La majuscule
permet de les différencier.
Si tout s’est bien passé, on définit les valeurs de notre premier élément :
— la donnée nombre est mise à 0 par défaut ;
— le pointeur suivant pointe vers NULL car le premier élément de notre liste est
aussi le dernier pour le moment. Comme on l’a vu plus tôt, le dernier élément doit
pointer vers NULL pour signaler qu’il est en fin de liste.
Nous avons donc maintenant réussi à créer en mémoire une liste composée d’un seul
élément et ayant une forme semblable à la fig. suivante.
La réponse est qu’on a le choix. Libre à nous de décider ce que nous faisons. Pour ce
chapitre, je propose que l’on voie ensemble l’ajout d’un élément en début de liste. D’une
part, c’est simple à comprendre, et d’autre part cela me donnera une occasion à la fin
de ce chapitre de vous proposer de réfléchir à la création d’une fonction qui ajoute un
élément à un endroit précis de la liste.
Nous devons créer une fonction capable d’insérer un nouvel élément en début de liste.
Pour nous mettre en situation, imaginons un cas semblable à la fig. suivante : la liste est
composée de trois éléments et on souhaite en ajouter un nouveau au début.
Il va falloir adapter le pointeur premier de la liste ainsi que le pointeur suivant de notre
nouvel élément pour « insérer » correctement celui-ci dans la liste. Je vous propose pour
cela ce code source que nous analyserons juste après :
1 v o i d i n s e r t i o n ( L i s t e ∗ l i s t e , i n t nvNombre )
2 {
3 /∗ Cr é a t i o n du n o u v e l é l é ment ∗/
4 Element ∗ nouveau = m a l l o c ( s i z e o f ( ∗ nouveau ) ) ;
5 i f ( l i s t e == NULL | | nouveau == NULL)
6 {
7 e x i t (EXIT_FAILURE) ;
8 }
9 nouveau−>nombre = nvNombre ;
10
11 /∗ I n s e r t i o n de l ’ é l é ment au dé but de l a l i s t e ∗/
12 nouveau−>s u i v a n t = l i s t e −>p r e m i e r ;
13 l i s t e −>p r e m i e r = nouveau ;
14 }
La fonction insertion() prend en paramètre l’élément de contrôle liste (qui contient l’adresse
du premier élément) et le nombre à stocker dans le nouvel élément que l’on va créer.
Nous avons ici choisi pour simplifier d’insérer l’élément en début de liste. Pour mettre à
jour correctement les pointeurs, nous devons procéder dans cet ordre précis :
1. faire pointer notre nouvel élément vers son futur successeur, qui est l’actuel premier
élément de la liste ;
2. faire pointer le pointeur premier vers notre nouvel élément.
NB : On ne peut pas suivre ces étapes dans l’ordre inverse ! En effet, si vous faites d’abord
pointer premier vers notre nouvel élément, vous perdez l’adresse du premier élément de
la liste ! Faites le test, vous comprendrez de suite pourquoi l’inverse est impossible.
Cela aura pour effet d’insérer correctement notre nouvel élément dans la liste chaînée (fig.
suivante) !
Figure 4.9 – Notre liste chaînée, après ajout d’un nouvel élément au debut
Cette fonction est courte mais sauriez-vous la réécrire ? Il faut bien comprendre qu’on doit
faire les choses dans un ordre précis :
1. faire pointer premier vers le second élément ;
2. supprimer le premier élément avec unfree.
Si on faisait l’inverse, on perdrait l’adresse du second élément !
1 void suppression ( L i s t e ∗ l i s t e )
2 {
3 i f ( l i s t e == NULL)
4 {
5 e x i t (EXIT_FAILURE) ;
6 }
7
8 i f ( l i s t e −>p r e m i e r != NULL)
9 {
10 Element ∗ aSupprimer = l i s t e −>p r e m i e r ;
11 l i s t e −>p r e m i e r = l i s t e −>premier −>s u i v a n t ;
12 f r e e ( aSupprimer ) ;
13 }
14 }
On commence par vérifier que le pointeur qu’on nous envoie n’est pas NULL, sinon on ne
peut pas travailler. On vérifie ensuite qu’il y a au moins un élément dans la liste, sinon il
n’y a rien à faire.
Il ne reste plus qu’à supprimer l’élément correspondant à notre pointeur aSupprimer avec
un free (fig. suivante).
1 void a f f i c h e r L i s t e ( L i s t e ∗ l i s t e )
2 {
3 i f ( l i s t e == NULL)
4 {
5 e x i t (EXIT_FAILURE) ;
6 }
7
8 Element ∗ a c t u e l = l i s t e −>p r e m i e r ;
9
10 w h i l e ( a c t u e l != NULL)
11 {
12 p r i n t f ( ”%d −> ” , a c t u e l −>nombre ) ;
13 a c t u e l = a c t u e l −>s u i v a n t ;
14 }
15 p r i n t f ( ”NULL\n” ) ;
16 }
Cette fonction est simple : on part du premier élément et on affiche le contenu de chaque
élément de la liste (un nombre). On se sert du pointeur suivant pour passer à l’élément
qui suit à chaque fois.
On peut s’amuser à tester la création de notre liste chaînée et son affichage avec un main :
1 i n t main ( )
2 {
3 L i s t e ∗ maListe = i n i t i a l i s a t i o n ( ) ;
4
5 i n s e r t i o n ( maListe , 4 ) ;
6 i n s e r t i o n ( maListe , 8 ) ;
7 i n s e r t i o n ( maListe , 1 5 ) ;
8 s u p p r e s s i o n ( maListe ) ;
9
10 a f f i c h e r L i s t e ( maListe ) ;
11
12 return 0 ;
13 }
En plus du premier élément (que l’on a laissé ici à 0), on en ajoute trois nouveaux à cette
liste. Puis on en supprime un. Au final, le contenu de la liste chaînée sera donc :
fonctions qui manquent et que je vous invite à écrire, ce sera un très bon exercice !
Conclusion
En conclusion de ce chapitre très riche en enseignements, on peut retenir ce qui suit :
— Les listes chaînées constituent un nouveau moyen de stocker des données en mé-
moire. Elles sont plus flexibles que les tableaux car on peut ajouter et supprimer
des « cases » à n’importe quel moment.
— Il n’existe pas en langage C de système de gestion de listes chaînées, il faut l’écrire
nous-mêmes ! C’est un excellent moyen de progresser en algorithmique et en pro-
grammation en général.
— Dans une liste chaînée, chaque élément est une structure qui contient l’adresse de
l’élément suivant.
— Il est conseillé de créer une structure de contrôle (du type Liste dans notre cas)
qui retient l’adresse du premier élément.
— Il existe une version améliorée — mais plus complexe — des listes chaînées appelée
« listes doublement chaînées », dans lesquelles chaque élément possède en plus
l’adresse de celui qui le précède.
Introduction
Nous avons découvert avec les listes chaînées un nouveau moyen plus souple que les ta-
bleaux pour stocker des données. Ces listes sont particulièrement flexibles car on peut
insérer et supprimer des données à n’importe quel endroit, à n’importe quel moment.
Les piles et les files que nous allons découvrir ici sont deux variantes un peu particulières
des listes chaînées. Elles permettent de contrôler la manière dont sont ajoutés les nou-
veaux éléments. Cette fois, on ne va plus insérer de nouveaux éléments au milieu de la
liste mais seulement au début ou à la fin.
Les piles et les files sont très utiles pour des programmes qui doivent traiter des données
qui arrivent au fur et à mesure. Nous allons voir en détails leur fonctionnement dans ce
chapitre.
Les piles et les files sont très similaires, mais révèlent néanmoins une subtile différence
que vous allez rapidement reconnaître. Nous allons dans un premier temps découvrir les
piles qui vont d’ailleurs beaucoup vous rappeler les listes chaînées, à quelques mots de
vocabulaire près.
Globalement, ce chapitre sera simple pour vous si vous avez compris le fonctionnement
des listes chaînées. Si ce n’est pas le cas, retournez d’abord au chapitre précédent car nous
allons en avoir besoin.
64
Chargé du cours : M. Baïmi Badjoua ©UPM
Le plus intéressant est sans conteste l’opération qui consiste à extraire les nombres de
la pile. On parle de dépilage. On récupère les données une à une, en commençant par
la dernière qui vient d’être posée tout en haut de la pile (fig. suivante). On enlève les
données au fur et à mesure, jusqu’à la dernière tout en bas de la pile.
On dit que c’est un algorithme LIFO, ce qui signifie « Last In First Out ». Traduction :
« Le dernier élément qui a été ajouté est le premier à sortir ».
Les éléments de la pile sont reliés entre eux à la manière d’une liste chaînée. Ils possèdent
un pointeur vers l’élément suivant et ne sont donc pas forcément placés côte à côte en
mémoire. Le dernier élément (tout en bas de la pile) doit pointer vers NULL pour indiquer
qu’on a… touché le fond (fig. suivante).
Il y a des programmes où vous avez besoin de stocker des données temporairement pour
les ressortir dans un ordre précis : le dernier élément que vous avez stocké doit être le
premier à ressortir.
Pour vous donner un exemple concret, votre système d’exploitation utilise ce type d’al-
gorithme pour retenir l’ordre dans lequel les fonctions ont été appelées. Imaginez un
exemple :
1. votre programme commence par la fonction main (comme toujours) ;
2. vous y appelez la fonction jouer ;
3. cette fonction jouer fait appel à son tour à la fonction charger ;
4. une fois que la fonction charger est terminée, on retourne à la fonction jouer ;
5. une fois que la fonction jouer est terminée, on retourne au main ;
6. enfin, une fois le main terminé, il n’y a plus de fonction à appeler, le programme
s’achève.
Pour « retenir » l’ordre dans lequel les fonctions ont été appelées, votre ordinateur crée
une pile de ces fonctions au fur et à mesure (fig. suivante).
Voilà un exemple concret d’utilisation des piles. Grâce à cette technique, votre ordinateur
sait à quelle fonction il doit retourner. Il peut empiler 100 fonctions d’affilée s’il le faut, il
retrouvera toujours le main en bas !
Chaque élément de la pile aura une structure identique à celle d’une liste chaînée :
1 t y p e d e f s t r u c t Element Element ;
2 s t r u c t Element
3 {
4 i n t nombre ;
5 Element ∗ s u i v a n t ;
6 };
On pourra aussi écrire une fonction d’affichage de la pile, pratique pour vérifier si notre
programme se comporte correctement.
Allons-y !
5.1.3 Empilage
Notre fonction empiler doit prendre en paramètre la structure de contrôle de la pile (de
type Pile) ainsi que le nouveau nombre à stocker. Je vous rappelle que nous stockons ici
des int, mais rien ne vous empêche d’adapter ces exemples avec un autre type de données.
On peut stocker n’importe quoi : des double, des char, des chaînes, des tableaux ou même
d’autres structures !
1 v o i d e m p i l e r ( P i l e ∗ p i l e , i n t nvNombre )
2 {
3 Element ∗ nouveau = m a l l o c ( s i z e o f ( ∗ nouveau ) ) ;
4 i f ( p i l e == NULL | | nouveau == NULL)
5 {
6 e x i t (EXIT_FAILURE) ;
7 }
8
9 nouveau−>nombre = nvNombre ;
10 nouveau−>s u i v a n t = p i l e −>p r e m i e r ;
11 p i l e −>p r e m i e r = nouveau ;
12 }
L’ajout se fait en début de pile car, comme on l’a vu, il est impossible de le faire au milieu
d’une pile. C’est le principe même de son fonctionnement, on ajoute toujours par le haut.
De ce fait, contrairement aux listes chaînées, on ne doit pas créer de fonction pour insérer
un élément au milieu de la pile. Seule la fonction empiler permet d’ajouter un élément.
5.1.4 Dépilage
Le rôle de la fonction de dépilage est de supprimer l’élément tout en haut de la pile, ça,
vous vous en doutiez. Mais elle doit aussi retourner l’élément qu’elle dépile, c’est-à-dire
dans notre cas le nombre qui était stocké en haut de la pile.
C’est comme cela que l’on accède aux éléments d’une pile : en les enlevant un à un. On ne
parcourt pas la pile pour aller y chercher le second ou le troisième élément. On demande
toujours à récupérer le premier.
Notre fonction depiler va donc retourner un int correspondant au nombre qui se trouvait
en tête de pile :
18 r e t u r n nombreDepile ;
19 }
1 void a f f i c h e r P i l e ( P i l e ∗ p i l e )
2 {
3 i f ( p i l e == NULL)
4 {
5 e x i t (EXIT_FAILURE) ;
6 }
7 Element ∗ a c t u e l = p i l e −>p r e m i e r ;
8
9 w h i l e ( a c t u e l != NULL)
10 {
11 p r i n t f ( ”%d\n” , a c t u e l −>nombre ) ;
12 a c t u e l = a c t u e l −>s u i v a n t ;
13 }
14
15 p r i n t f ( ” \n” ) ;
16 }
Cette fonction étant ridiculement simple, elle ne nécessite aucune explication (et toc !).
En revanche, c’est le moment de faire un main pour tester le comportement de notre pile :
1 i n t main ( )
2 {
3 P i l e ∗ maPile = i n i t i a l i s e r ( ) ;
4
5 e m p i l e r ( maPile , 4) ;
6 e m p i l e r ( maPile , 8) ;
7 e m p i l e r ( maPile , 15) ;
8 e m p i l e r ( maPile , 16) ;
9 e m p i l e r ( maPile , 23) ;
10 e m p i l e r ( maPile , 42) ;
11
12 p r i n t f ( ” \ nEtat de l a p i l e :\n” ) ;
13 a f f i c h e r P i l e ( maPile ) ;
14
15 p r i n t f ( ” Je d e p i l e %d\n” , d e p i l e r ( maPile ) ) ;
16 p r i n t f ( ” Je d e p i l e %d\n” , d e p i l e r ( maPile ) ) ;
17
18 p r i n t f ( ” \ nEtat de l a p i l e :\n” ) ;
19 a f f i c h e r P i l e ( maPile ) ;
20
21 return 0 ;
22 }
On affiche l’état de la pile après plusieurs empilages et une autre fois après quelques
dépilages. On affiche aussi le nombre qui est dépilé à chaque fois que l’on dépile. Le
résultat dans la console est le suivant :
Vérifiez que vous voyez bien ce qui se passe dans ce programme. Si vous comprenez cela,
vous avez compris le fonctionnement des piles !
Il est facile de faire le parallèle avec la vie courante. Quand vous allez prendre un billet
de cinéma, vous faites la queue au guichet (fig. suivante). À moins d’être le frère du
guichetier, vous allez devoir faire la queue comme tout le monde et attendre derrière.
C’est le premier arrivé qui sera le premier servi.
En programmation, les files sont utiles pour mettre en attente des informations dans
l’ordre dans lequel elles sont arrivées. Par exemple, dans un logiciel de chat (type mes-
sagerie instantanée), si vous recevez trois messages à peu de temps d’intervalle, vous les
enfilez les uns à la suite des autres en mémoire. Vous vous occupez alors du premier
message arrivé pour l’afficher à l’écran, puis vous passez au second, et ainsi de suite.
Les événements que vous envoie la bibliothèque SDL que nous avons étudiée sont eux
aussi stockés dans une file. Si vous bougez la souris, un événement sera généré pour
chaque pixel dont s’est déplacé le curseur de la souris. La SDL les stocke dans une file
puis vous les envoie un à un à chaque fois que vous faites appel à SDL_PollEvent (ou à
SDL_WaitEvent : oui, c’est bien ! Je vois que ça suit au fond de la classe. ;) ).
En C, une file est une liste chaînée où chaque élément pointe vers le suivant, tout comme
les piles. Le dernier élément de la file pointe vers NULL (fig. suivante).
Nous allons créer une structure Element et une structure de contrôle File :
1 t y p e d e f s t r u c t Element Element ;
2 s t r u c t Element
3 {
4 i n t nombre ;
5 Element ∗ s u i v a n t ;
6 };
7
8 typedef struct File File ;
9 struct File
10 {
11 Element ∗ p r e m i e r ;
12 };
Comme pour les piles, chaque élément de la file sera de type Element. À l’aide du pointeur
premier, nous disposerons toujours du premier élément et nous pourrons remonter jusqu’au
dernier.
5.2.3 Enfilage
La fonction qui ajoute un élément à la file est appelée fonction « d’enfilage ». Il y a deux
cas à gérer :
— soit la file est vide, dans ce cas on doit juste créer la file en faisant pointer premier
vers le nouvel élément créé ;
— soit la file n’est pas vide, dans ce cas il faut parcourir toute la file en partant
du premier élément jusqu’à arriver au dernier. On rajoutera notre nouvel élément
après le dernier.
Voici comment on peut faire dans la pratique :
1 v o i d e n f i l e r ( F i l e ∗ f i l e , i n t nvNombre )
2 {
3 Element ∗ nouveau = m a l l o c ( s i z e o f ( ∗ nouveau ) ) ;
4 i f ( f i l e == NULL | | nouveau == NULL)
5 {
6 e x i t (EXIT_FAILURE) ;
7 }
8
9 nouveau−>nombre = nvNombre ;
10 nouveau−>s u i v a n t = NULL ;
11
Vous voyez dans ce code le traitement des deux cas possibles, chacun devant être géré à
part. La différence par rapport aux piles, qui rajoute une petite touche de difficulté, est
qu’il faut se placer à la fin de la file pour ajouter le nouvel élément. Mais bon, un petit
while et le tour est joué, comme vous pouvez le constater. :-)
5.2.4 Défilage
Le défilage ressemble étrangement au dépilage. Étant donné qu’on possède un pointeur
vers le premier élément de la file, il nous suffit de l’enlever et de renvoyer sa valeur.
1 int de f i le r ( File ∗ f i l e )
2 {
3 i f ( f i l e == NULL)
4 {
5 e x i t (EXIT_FAILURE) ;
6 }
7
8 i n t nombreDefile = 0 ;
9
10 /∗ On v é r i f i e s ’ i l y a q u e l q u e c h o s e à d é f i l e r ∗/
11 i f ( f i l e −>p r e m i e r != NULL)
12 {
13 Element ∗ e l e m e n t D e f i l e = f i l e −>p r e m i e r ;
14
15 n o m b r e D e f i l e = e l e m e n t D e f i l e −>nombre ;
16 f i l e −>p r e m i e r = e l e m e n t D e f i l e −>s u i v a n t ;
17 free ( elementDefile ) ;
18 }
19
20 return nombreDefile ;
21 }
Réalisez ensuite un main pour faire tourner votre programme. Vous devriez pouvoir ob-
tenir un rendu similaire à ceci :
À terme, vous devriez pouvoir créer votre propre bibliothèque de files, avec des fichiers
file.h et file.c par exemple. La même chose peut se faire avec pile.c et pile.h ou encore avec
liste_chainee.c et liste_chainee.h
Conclusion
Au terme de ce chapitre il est important de retenir ce qui suit :
— Les piles et les files permettent d’organiser en mémoire des données qui arrivent
au fur et à mesure.
Introduction
Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.
Conclusion
Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.
76
CHAPITRE 7
ÉTUDE DE QUELQUES ALGORITHMES
Introduction
Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.
Conclusion
Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.
77
CONCLUSION GÉNÉRALE
Mauris blandit aliquet elit, eget tincidunt nibh pulvinar a. Vestibulum ante ipsum primis
in faucibus orci luctus et ultrices posuere cubilia Curae ; Donec velit neque, auctor sit amet
aliquam vel, ullamcorper sit amet ligula. Vivamus magna justo, lacinia eget consectetur
sed, convallis at tellus. Curabitur arcu erat, accumsan id imperdiet et, porttitor at sem.
Praesent sapien massa, convallis a pellentesque nec, egestas non nisi. Pellentesque in ipsum
id orci porta dapibus.
78
RÉFÉRENCES BIBLIOGRAPHIQUES
79