Language C
Language C
Language C
de Marne-La-Vallée
Introduction à l'informatique
et programmation en langage C
(DUT Génie Thermique et Energie)
Jean Fruitet
[email protected]
1999
Introduction à la programmation
I.U.T. de Marne-La-Vallée
Introduction à l'informatique
et programmation en langage C
(DUT Génie Thermique et Energie)
Jean Fruitet
[email protected]
Avertissement 3
Caractérisation d’un problème informatique 3
Introduction à l’informatique 5
Le codage binaire 7
Notion d’algorithme et de machine à accès direct 14
Langage C 19
Processus itératifs 43
Fonctions et sous-programmes 44
Notion de complexité 48
Des données aux structures de données : tableaux, calcul matriciel 50
Calcul numérique : fonctions numériques, résolution d'équation, intégration 61
Structures de données : ensembles, listes, piles, files, hachage, arbres, graphes 76
Algorithmes de tri 112
Bibliographie 123
Table des matiéres 122
Avertissement
Ce cours s’adresse aux étudiants de première année de DUT de Génie Thermique et Energie
(GTE). Il leur est présenté en quelques dizaines d’heures —une trentaine— les rudiments de la
programmation numérique et des notions d’algorithmique. Ces étudiants n'étant pas destinés à une
carriére d’informaticien professionnel, je n’aborde pas l’algorithmique dans tous ses raffinements. En
particulier les notions pourtant fondamentales de preuve de programme et d’analyse de complexité
ne sont pas évoquées.
Ce cours est divisé en quatre parties :
- notion d'informatique et de codage ;
- structure d'un ordinateur : la machine à accès direct (MAD / RAM) ;
- langage de programmation : le langage C ;
- algorithmique numérique et structures de données.
Après quelques notions de théorie de l'information et de codage (codage binaire, représentation
des entiers et des flottants) j'introduis la programmation de fonctions numériques sur ordinateur PC
sous MS-DOS puis l'utilisation de quelques structures de données fondamentales (tableaux, piles,
files, arbres, graphes) et les principaux algorithmes de tri. Ce cours ne fait donc aucune place à la
technologie des ordinateurs, leur architecture, système d'exploitation et de fichiers. Il n'est pas non
plus question d'apprentissage de logiciels bureautiques (traitement de texte ou de tableur). Ce n'est
pas que ces connaissances ne soient pas nécessaires aux techniciens, mais je laisse à d'autres
enseignants le soin d'y contribuer.
S’agissant de la syntaxe d’un langage de programmation, j’introduis le langage RAM, pour passer
rapidement au langage C. J'insiste beaucoup dans ce cours sur la nécessité d'une programmation
structurée descendante. Cette démarche est recommandée depuis des lustres par tous les spécialistes.
Malheureusement l'expérience montre que livré à lui-même le programmeur moyen se permet des
libertés qui rendent rapidement ses programmes illisibles et inutilisables. Mais ce ne sera pas faute
d'avoir été prévenu...
L'art de programmer, c'est l'art de faire résoudre des problèmes par des machines. Il s’agit bien
d’un art, au sens de l’artisan, qui passe par une longue période d’apprentissage et d’imitation. Dans
cet exercice certains individus ont des dispositions naturelles ; pour les autres un apprentissage
rigoureux fournit les rudiments d’une méthode. Le reste est affaire de travail et d’investissement
personnels.
Un ordinateur est dénué d’intelligence ; il ne peut donc résoudre que les problèmes pour lesquels
existe une méthode de résolution algorithmique, c’est-à-dire une recette déterministe. De plus, même
si la recette existe en théorie pour résoudre tel problème, encore faut-il que l’énoncé du problème —
l’espace des paramètres— et l’ensemble des solutions soient de dimension finie, en raison de la
limitation en taille de la mémoire des machines. Enfin, condition ultime, la mise en oeuvre d’un
algorithme doit avoir une durée finie. Un problème dont la solution nécessite de disposer d’un temps
infini n’est pas considéré comme résoluble par ordinateur. Ces trivialités vont donc limiter nos
ambitions de programmeur à une classe de problèmes assez restreinte, d’autant que ne nous
disposons pas de puissantes machines de calcul.
Introduction à l’informatique
Langage
La plupart des informations que les Humains échangent sont supportées par un langage, c'est-à-
dire des groupes de sons (les mots), qu'il faut assembler "d'une certaine manière" (grammaire) pour
que les phrases aient un sens... Avec l'invention de l'écriture, ces mots ont eu une transcription
(recodage) sous forme de symboles graphiques (des formes) dessinés ou imprimés.
Le concept de mot est fondamental en informatique, de même que celui de langage. Un langage
est un ensemble de mots construits avec les lettres choisies dans un alphabet. Les mots sont
assemblés en phrases selon des régles de grammaire précises qui définissent la syntaxe du langage.
Le sens attaché à ces phrases, c'est-à-dire leur signification, constitue la sémantique.
L'essentiel de ce cours va consister à expliquer comment sont commandées les machines que nous
nommons ordinateurs, capables d'exécuter des tâches complexes de traitement de l'information.
Traitement de l'information
Le traitement de l'information consiste en une suite d'opérations transformant une représentation
de cette information en une autre représentation plus facile à manipuler ou à interpréter.
Exemples :
"3*2" remplacé par "6"
"Mille neuf cent quatre vingt treize" est remplacé par "1993"
"La somme des carrés des côtés de l'angle droit d'un triangle rectangle est égale au carré de
l'hypoténuse" est remplacé par "Théoréme de Pythagore"
"Championne olympique 1992 et 1996 du 400 mètres féminin" est remplacé par "Marie-José
Pérec".
Dans une entreprise, traiter l'information peut consister à établir la paye, faire la facturation, gérer
le stock, dresser un bilan. Dans un atelier, diriger un robot. En météorologie, reconnaître un cyclone
sur une photo satellite...
Ordinateur
Un ordinateur est une machine qui permet d'effectuer des traitements sur des données à l'aide de
programmes. Les données (les paramètres du problème, par exemple les notes des étudiants du cours
d'informatique) ainsi que le programme (par exemple le calcul de la moyenne des notes) sont fournis
à la machine par l'utilisateur au moyen de dispositifs de saisie (le clavier). Le résultat du traitement
est recueilli à la sortie de l'ordinateur (l'écran, l’imprimante) sous forme de texte.
Un peu d'histoire
C'est en 1945 que le principe des ordinateurs a été inventé. Mais on peut faire remonter ses
origines au boulier et aux premières machines à calculer mécaniques. Blaise Pascal (1623-1662)
inventa à l'âge de 18 ans une machine à base de roues dentées et d'engrenages qui réalise d'elle-même
les additions et les soustractions. Il suffit d'indiquer les chiffres et l'opération à faire.
Au XIXéme siècle l'anglais Babbage conçoit deux grandes machines dont le principe était correct,
mais qui ne purent être réalisées en raison de difficultés techniques et financiéres. Il était sans doute
trop tôt. Ce n'est qu'au XXème siècle que sous la pression du développement économique et des
besoins militaires (Deuxiéme guerre mondiale) des scientifiques et des ingénieurs s'attelérent à la
construction de gigantesques machines à calculer. La plus fameuse de ces machines fut la "Harvard
Mark 1" qui mesurait 16 mètres de long, pesait 5 tonnes et comprenait 800 000 éléments, et
pourtant n'avait pas plus de puissance qu'une simple calculette de poche actuelle !
La véritable révolution pour les machines à calculer viendra des progrès de l'électronique et de la
logique mathématique. Le premier calculateur électronique, l'ENIAC, destiné à calculer la trajectoire
de projectiles pour l'armée américaine, fut construit à partir de 1943. Il pesait 30 tonnes, comportait
17 468 tubes à vide et additionnait 5000 nombres en une seconde. Mais on ne peut considèrer cette
machine comme un ordinateur, car il n'était pas véritablement automatique et n'utilisait pas de
programme interne.1
Sur le plan technique les progrès décisifs seront réalisés dans les années 1950 avec l'invention en
1947 du transistor (qui donne son nom aux postes de radio portables). Les transistors sont des
composants électroniques qui remplace partout les lampes à vides ; rassemblés par dizaines puis
centaines de milliers sur des circuits intégrés ils permettent de réaliser des puces électroniques qui
envahissent les automates (lave linge, magnétoscopes, circuits d'allumage de voiture, calculettes...)
et les ordinateurs.
Sur le plan conceptuel, c'est aux anglais George Boole (1815-1864), inventeur de l'algèbre
binaire, l'algèbre de la logique, et Alan Turing (1912-1954) et aux américains d'origine européenne
John Von Neumann (1903-1957) et Norbert Wiener (1894-164) que nous devons l'avancée décisive
qui méne des calculateurs aux ordinateurs. Leurs travaux aboutissent à la construction du premier
ordinateur en 1948 à Manchester, en Grande-Bretagne, le "Manchester Mark 1".
1 Philippe BRETON "Une histoire de l'Informatique" - Collection Points Sciences - Editions La Découverte, Le Seuil
1990.
Jean Fruitet - IUT de Marne La Vallée - 6
Introduction à la programmation
- elle a une unité centrale de commande interne qui organise le travail en appliquant les
instructions du programme et dirige les échanges de données avec l'extérieur de la machine.
Matériel et logiciel
Un ordinateur est constitué de composants matériels (hardware ) et de composants logiciels
(software ). Les composants matériels sont essentiellement des cartes électroniques, des circuits
intégrés, des câbles électriques, des supports de mémoires de masse (disques durs) et des dispositifs
d'entrée/sortie (périphériques : clavier, écran, imprimante). Les logiciels, qui pilotent le
fonctionnement des composant matériels, sont des programmes stockés sous forme codée dans la
mémoire de l'ordinateur. Pour être interprétés par l'unité centrale ces programmes doivent être
traduits dans le langage des machines, le langage binaire.
Le codage binaire
Toute l'information qui transite dans un ordinateur est codée avec des mots formés seulement de
deux symboles (ou de deux 'états') notés 0 et 1. Cela tient à la nature des composants électriques et
magnétiques utilisés pour coder l'information.
Dans les mémoires d'ordinateur, chaque unité élémentaire d'information peut être représentée par
un minuscule aimant. Chaque aimant est orienté soit dans un sens (état 0), soit dans le sens opposé
(état 1)...
C'est le même principe qui est appliqué aux échanges de données. Pour transmettre un 1, il faut
appliquer sur un conducteur électrique une différence de potentiel supérieure à quelques volts
pendant une période "assez" longue, de l'ordre de la micro seconde (1/1 000 000 éme de seconde).
Pour transmettre un 0, il faut maintenir une différence de potentiel inférieure à 1 volt pendant la
même durée.
Par exemple pour coder le nombre 13 en binaire, il faut les quatre chiffres binaires 1101. En effet
13 peut être décomposé comme une somme de puissances de 2
13 = 8 + 4 + 1
= 1 * 8 + 1 * 4 + 0 * 2 + 1 * 1 en décimal
= 1 * 23 + 1 * 22 + 0 * 21 + 1 * 20 on ne conserve que les coefficients
= 1 1 0 1 en binaire
Représentation des informations en binaire
Pour coder de l'information , que ce soient des nombres, (PI=3,141592...), du texte (ce cours),
des schémas, des images, des sons, des relations ("Pierre est le pére de Jacques et le frêre de
Marie"), les circuits électroniques d'un ordinateur ne peuvent utiliser que des mots en binaire.
Montrons d'abord comment il est possible de coder n'importe quel nombre entier naturel IN={0,
1, 2, 3, ... ,1 000, ..., 1 000 000, ...} en binaire.
Puis nous en ferons autant pour les lettres et les mots de la langue française. Enfin il faudra
montrer que les images, les sons et les relations aussi peuvent se coder en binaire.
Passer du décimal au binaire
Il suffit de décomposer un nombre décimal en une somme de puissances de 2. On peut par exemple
commencer par écrire la table des premières puissances de 2 :
20 = 1 21 = 2 22 = 2x2 = 4 23 = 2x2x2 = 8
24 = 25 = 2x...x2 = 26 = 27 =
28 = 29 = 210 = 211 =
Opérateurs binaires :
+ - * DIV MOD /
Opérateurs de comparaison :
= < <=> >=
Axiomes :
associativité de + et *; distributivité de * sur +; relation d'ordre
0.8 = 20 x 0.8
0.8 x 2 = 1.6 donc 0.8 = 1.6 / 2 = 2-1 x 1.6 = 2-1 x 1 + 2-1 x 0.6
0.6 x 2 = 1.2 donc 0.8 = 2-1 x 1 + 2-2 x 1.2
0.2 x 2 = 0.4 donc 0.8 = 2-1 x 1 + 2-2 x 1 + 2-3 x 0.4
0.4 x 2 = 0.8 donc 0.8 = 2-1 x 1 + 2-2 x 1 + 2-3 x 0 + 2-4 x 0.8
0.8 x 2 = 1.6 donc ... on retrouve une expression déjà rencontrée qui va se répéter infiniment
0.8 = (-1)0 x 1. 10011001100110011.... x 2-1
"possède"
1:n
Notion d’algorithme
Un algorithme est un procédé automatique qui transforme une information symbolique en une
autre information symbolique. Seuls les problèmes qui sont susceptibles d'être résolus par un
algorithme sont accessibles aux ordinateurs.
Ce qui caractérise l'exécution d'un algorithme, c'est la réalisation d'un nombre fini d'opérations
élémentaires (instructions) ; chacune d'elles est réalisable en un temps fini. La quantité de données
manipulées au cours du traitement est donc finie.
La notion d'opération élémentaire dépend du degré de raffinement adopté pour la description du
procédé. Ainsi, chaque algorithme peut être considéré comme une opération élémentaire dans un
procédé plus important.
Exemple d’algorithme : Factorisation de ax2+bx+c quand a≠0.
Algorithme A :
soient x1 et x2 les zéros de ax2+bx+c ;
alors ax2+bx+c = a (x-x1) (x-x2).
Algorithme B
soit ∆= b2-4ac;
si ∆ = 0 alors soient x1 et x2 égaux à -b/2a
sinon si ∆ > 0 alors soient x1=(-b+√∆)/2a et x2= (-b-√∆)/2a
sinon soient x1=(-b+i√(-∆))/2a et x2= (-b-i√(-∆))/2a;
alors ax2+bx+c = a (x-x1) (x-x2).
Avant de passer à la phase de programmation il est donc nécessaire de définir très précisément le
cahier des charges du programme, c’est-à-dire dans quelles conditions initiales il devra fonctionner,
comment il devra procéder (algorithme) et sous quelle forme seront présentés les résultats.
Types de données et structures de contrôle
Dans l’exemple de la factorisation ci-dessus, les entités manipulées sont des nombres (complexes
et réels), des coefficient constants (a, b, c), une “inconnue” (x), des variables x1 et x2, le symbole ∆
du discriminant et les opérations élémentaires sur l’ensemble des nombres réels (<, >, =, +, -, *, /).
On dira que les types de données sont des constantes et des variables de type réel. L’algorithme
emploie aussi des structures de contrôle conditionnelles : Si (condition) alors instruction sinon
instruction.. Un instruction est soit une affectation — ∆= b2-4ac; —, soit un test— si ∆ = 0 alors —
.Le langage de programmation devra fournir des équivalents de toutes ces entités. Enfin le langage
doit permettre de créer un programme qui reçoive des paramètres —saisie de la valeur des
coefficients— et retourne des résultats à l’utilisateur —affichage—.
Un langage de programmation graphique
Traduisons d’abord l’algorithme de factorisation dans un langage graphique élémentaire, bien
adapté à l’expression des algorithmes peu complexes.
Afficher
a (x-x1) (x-x2);
FIN DU PROGRAMME;
Organe d'entrée
Tête de lecture
Mémoire
Compteur
UAL
Données
Tête d'écriture
Organe de sortie
La mémoire est une suite de cases dont les indices sont appelés adresses. Une partie de la
mémoire contient le programme, traduction de l'algorithme au moyen des instructions de la machine.
Le compteur d’instruction contient l'adresse de la prochaine instruction à exécuter. L'autre partie de
la mémoire contient les données.
Caractéristiques :
- l'unité centrale accéde directement (en temps constant) à une case mémoire à partir de son adresse
[random acces].
- La mémoire est infinie (mais le programme est fini).
- Chaque case mémoire contient une donnée élémentaire de taille arbitraire. On peut ainsi y
mémoriser des entiers arbitrairement grands.
- Chaque instruction s'exécute en temps constant.
Variables et types
Une case mémoire de la machine abstraite est aussi appelée une variable. Dans les programmes
on désigne une variable par un identificateur littéral plutôt que par son adresse, et, par abus de
langage, on dit "la variable x " plutôt que "la variable d'identificateur x ". Le type d'une variable
définit l'ensemble des valeurs qu'elle peut prendre —du point de vue implantation en machine, le type
décide aussi de la taille de l’espace mémoire occupé par la donnée.
2Pour préparer le lecteur aux conventions du langage C, nous conviendrons de noter l’affectation par un signe ‘=‘ et
l’égalité par ‘==‘.
Jean Fruitet - IUT de Marne La Vallée - 17
Introduction à la programmation
De façon plus précise la syntaxe du langage est définie par la grammaire formelle suivante
(simplifiée du langage C) :
Avec ce langage, traduisons l’algorithme de factorisation. Nous obtenons une fonction qui
retourne un couple de polynômes (éventuellement complexes) de degré 1.
Fonction factorisation d’un polynôme réel de degré 2
couple de polynômes de degré 1 factorisation ( polynôme réel de degré 2 ax2+bx+c);
{
réel D;
D =b2-4ac
si (D==0)
retour((x+b/2a), (x+b/2a);
sinon si (D>0)
retour((x-(-b+√D)/2a), (x-(-b-D)/2a) );
sinon
retour((x-(-b+i√ (-D))/2a), (x-(-b-i√ (-D))/2a));
}
Traduit dans le langage de la machine RAM, l’algorithme de factorisation reste encore très
proche de ses origines mathématiques. L’ultime transformation va le traduire dans un langage
effectif, le langage C, pour obtenir un programme exécutable par une machine réelle.
Conclusion
Cet exposé de la factorisation d’un polynôme nous a permis de passer par les trois étapes de
création d’un programme numérique :
définition du problème,
rédaction de l’algorithme en termes mathématiques,
traduction dans un langage de programmation (la traduction en langage C sera abordée au
chapitre suivant).
Il faut insister ici sur l’extréme rigueur de la syntaxe des langages de programmation. Toutes les
constantes, variables et fonctions utilisées doivent être typées et définies avant appel. Les
instructions respectent une grammaire précise, qui caractérise le langage ; les fonctions disponibles
(plusieurs centaines en C) fournissent une grande variété d’outils dont la maîtrise ne peut s’acquérir
que peu à peu... Cet apprentissage ne doit pas être confondu avec une formation à l’art de la
programmation qui peut débuter avec des exercices plus simples et moins rebutants.
Le langage C.
Le langage C est un langage d’ingénieur destiné à la création d’applications informatiques.
Beaucoup d’ouvrages lui ont été consacrés. Le plus important est dù aux créateurs du langage eux-
même, Denis Ritchie et Brian Kerninghan. On en trouvera la référence dans la bibliographie. Un
programme en C est constitué d’un (ou plusieurs) fichiers sources organisés d’une façon
conventionnelle. Voici une traduction en C de l’algorithme de factorisation. Les cadres (qui ne sont
pas nécessaires dans un programme, mais permettent ici de fixer les idées) délimitent cinq parties
fonctionnellement interdépendantes du fichier source.
/* déclarations de constantes
et de variables globales */ (2)
#define FALSE 0
#define TRUE 1
float a, b, c;
/* prototypes de fonctions */
void factorisation(float a, float b, float c); (3)
Le bloc (1) est celui des fichiers inclus. Sa première ligne, #include <stdio.h>, invoque la
bibliothèque des fonctions d’entrée-sortie (saisie au clavier scanf() et affichage à l’écran
Jean Fruitet - IUT de Marne La Vallée - 19
Introduction à la programmation
printf()). La deuxiéme ligne, #include <math.h>, fait appel aux fonctions
mathématiques (sqrt() : racine carrée).
Vient ensuite —bloc (2)— la définition des constantes et des variables globales, c’est-à-dire vues
depuis tous les points du programme :
#define FALSE 0
#define TRUE 1
float a, b, c;
Puis on trouve le prototype de la fonction factorisation() :
void factorisation (float a, float b, float c);
Celle-ci prend trois paramètres —a, b, c : les coefficients du polynôme à factoriser— de type
float; mais comme elle ne retourne aucune valeur, elle est typée void.
Enfin c’est le bloc (4) de la fonction main(). C’est le point d’entrée du programme. Tout
programme en Langage C a une fonction main() et une seule. Celle-ci affiche deux lignes de message
et lit ensuite le clavier —scanf(“%f %f %f”, &a, &b, &c);— jusqu’à l’entrée de trois
nombres ‘flottants’. Après avoir testé la condition (a0) la fonction factorisation() est
appelée et le programme se termine.
Le dernier bloc (5) est le code de la fonction factorisation(). Le lecteur reconnaîtra la
définition du discriminant et l’expression des différentes factorisations selon la valeur de . Nous
n’entrerons pas maintenant dans le détail de la syntaxe des fonctions printf() et scanf(), qui
sont parmi les plus compliquées du langage C. Je renvoie le lecteur aux ouvrages cités en référence
et au support du cours de langage C.
Les étapes suivantes consistent à compiler ce programme source, puis à lier le fichier objet
obtenu après compilation avec les bibliothèques standard et mathématique, ce qui produit un
programme exécutable. En cas d’erreur, ou pour modifier ce programme, il faut reprendre toute la
séquence en rééditant le fichier source...
C est étroitement associé à UNIX Le Système d'Exploitation (Operating System) UNIX développé
aux Laboratoires Bell (ATT Corporation - USA) par B.W. Kernigham et D.M. Ritchie dans les
années 70, a été écrit en C, développé pour l'occasion.
Unix est multi-tâches et multi-utilisateurs, sur mini et stations de travail. Sa diffusion a assuré le
succés de C chez les universitaires et les ingénieurs.
Unix est aujourd'hui fortement concurrent d'OS2 sur micros puissants...
Evolutions
Le futur de C est lié à la programmation parallèle, au développement des réseaux et à la
Programmation Orientée Objets (C++, Java). L'apprentissage de C est un bon investissement pour
l'ingénieur logiciel ou le chercheur amené à utiliser des stations de travail, à condition de
programmer souvent.
Structures en blocs
Un bloc est une séquence d'une ou plusieurs instructions commençant par une accolade ouvrante {
terminée par une accolade fermante }.
Constantes et variables
Le langage C utilise les constantes numériques et les constantes caractères
- entiers : 0, 1, 2, -1, -2, etc.
- flottants : 0.0, 0.3141592E1,
- caractères : 'a', 'b', 'c',..., 'A', 'B', 'C', etc.,
- constantes chaînes de caractères : "CECI est une chaîne de caractères".
Les variables sont des adresses de la mémoire désignées par des identificateurs littéraux
commençant par une lettre (exemple : i, j, x, y, entier1, entier_2, bilan_energetique)
Mots réservés
Les mots réservés ne peuvent pas servir de noms de variables.
auto extern short break float sizeof case for static char
goto struct continue if switch default int typedef do long
union double register unsigned else return while
Mots réservés supplémentaires pour la norme ANSI
const signed volatile enum void
Mots réservés supplémentaires pour le compilateur Turbo C
asm huge pascal cdecl interrupt far near
Types de données
Il faut déclarer le type de toutes les variables et de toutes les fonctions, qui indique à la fois
l'intervalle de définition et les opérations licites
Types simples
Type Signification Taille (bits) Valeurs limites
int entier 16 -32768 à +32768
short entier 16 -32768 à +32768
long entier 32 -2 147 483 648 à +2 147 483 648
char caractère 8 -128..+127
float réel +-10 E-37 à +-10 E+38
double réel +-10 E-307 à +-10 E+308
Caractères spéciaux
caractères nom symbole code code hexa décimal
\n newline LF 0A 10
\t tabulation HT 09 9
\b backspace BS 08 8
\r return CR 0D 13
\f form feed FF 0C 12
\a bell BEL 07 7
\\ backslash 5C 92
\' single quote 27 39
\" double quote 22 34
Si (e1) et (e2) sont des expressions et "op" une opération prise parmi la liste + - * / % << >> & | ^
alors
(e1) = (e1) op (e2); peut s'écrire (e1) op = (e2);
On économise une évaluation de e1.
if (condition)
{ /* début de bloc*/
instruction1;
instruction2;
} /* fin de bloc */
if (condition)
instruction_si;
else /* sinon */
instruction_sinon;
Regroupement d'instructions
if (cond1) /* premier if */
{
if (cond2)
inst1;
else if (cond3)
inst2;
}
else /* sinon se rapportant au premier if */
inst3;
Affectation conditionnelle
if (i>j) z = a; else z = b; est équivalent à z = (i>j) ? a : b;
Sélection (switch)
L'instruction switch est une sorte d'aiguillage. Elle permet de remplacer plusieurs instructions
imbriquées. La variable de contrôle est comparée à la valeur des constantes de chaque cas (case). Si
la comparaison réussit, l'instruction du case est exécutée jusqu'à la première instruction break
rencontrée.
switch (variable_controle)
{
case valeur1 : instruction1;
break; /* sortie du case */
case valeur2 : instruction2;
break;
case valeur3 : /* plusieurs */
case valeur4 : /* étiquettes */
case valeur5 : instruction3; /* pour la même instruction */
break;
default : instruction4; /* cas par défaut */
break; /* facultatif mais recommandé */
}
Boucles et sauts
Les boucles consistent à répéter plusieurs fois la même séquence d'instructions. La sortie de boucle
est réalisée en testant une condition (de type booléen VRAI ou FAUX).
while (condition)
instruction;
while (condition)
{
instruction1;
instruction2;
}
La condition est évaluée avant d'entrer dans la boucle. La boucle est répétée tant que la condition est
VRAIE.
For, Pour.
for (initialisation; condition d'arrêt; incrémentation)
instruction;
for (initialisation; condition d'arrêt; incrémentation)
{
instruction1;
instruction2;
...
}
La condition est évaluée avant d'entrer dans la boucle. L'incrémentation de la variable de contrôle est
faite à la fin de chaque tour de boucle.
Exemple :
int i;
for (i=1; i<10; i++)
printf("%d ",i); /* ce programme affiche 1 2 3 4 5 6 7 8 9 */
do {
instruction1;
instruction2;
} while (condition d'arrêt);
Trois dimensions
int parallelepipede[3][2][4]; 3 couches de 2 lignes de 4 colonnes chacune.
parallelepipede[0][0][0] désigne la première colonne de la première ligne de la première couche.
parallelepipede[2][1][3] désigne la dernière colonne de la dernière ligne de la dernière couche...
La déclaration int *p signifie que *p est un entier et donc que p est un pointeur sur un entier.
La réservation d'espace est à la charge du programmeur qui doit le faire de façon explicite, en
utilisant les fonctions standard malloc(), calloc(), ou realloc().
Ces fonctions sont prototypées dans <stdlib.h> et <alloc.h>
char * malloc( unsigned taille); réserve taille octets, sans initialisation de l'espace
char * calloc( unsigned nombre, unsigned taille); réserve nombre éléments de taille octets
chacun ; l'espace est initialisé à 0.
void * realloc( void *block, unsigned taille); modifie la taille affectée au bloc de mémoire
fourni par un précédent appel à malloc() ou calloc().
void free( void *block); libére le bloc mémoire pointé par un précédent appel à malloc(), calloc()
ou realloc().
Ces fonctions doivent être transtypées pour réserver de l'espace pour des types de données qui
sont différents du type (char *). Elles retournent le pointeur NULL (0) si l'espace disponible est
insuffisant.
Exemples :
char *s, *calloc();
s = (char *) calloc(256, sizeof(char)); /* réserve 256 octets initialisés à '\0' */
float *x;
x = (float *) malloc(sizeof(float)); /* réserve un espace pour un réel de type double */
L'allocation dynamique de mémoire se fait dans le tas (mémoire système dynamique) ; la taille de
celui-ci varie en cours d'exécution, et peut n'être pas suffisante pour allouer les variables, provoquant
le plantage du programme. Il faut donc tester le retour des fonctions d'allocation et traiter les erreurs.
L'espace alloué à une variable peut être libéré par free(), dont l'argument est un pointeur réservé
dynamiquement.
free(s);
free(x);
free(numero);
Programme principal et fonctions
Les fonctions permettent de décomposer un programme en entités restreintes. Une fonction peut
se trouver :
- dans le même module de texte (toutes les déclarations et instructions qui la composent s'y
trouvent),
- être incluse automatiquement dans le texte du programme (#include)
- ou compilée séparément puis liée (linked) au programme.
Une fonction peut être appelée à partir du programme principal, d'une autre fonction ou d'elle-
même (récursivité). Elle peut retourner une valeur (int par défaut) ou ne pas en retourner (déclarée
void, assimilable à une procédure Pascal). Une fonction peut posséder ses propres variables ; elle n'a
qu'un seul point d'entrée et peut avoir plusieurs points de sortie.
Déclaration de fonction avec prototypage
Le prototypage permet au compilateur de faire une vérification de type au moment de la définition
et de l'appel.
/* prototype de fonction : entête de déclaration suivi par ';' déclaration avec
les types des arguments entre les parenthèses */
type_retourné nom_fonction (type variable1, type variable2, ...)';'
Définition
La définition de la fonction (la liste des instructions) reprend le même texte que pour le prototype
sans ';'
type_retourné nom_fonction (type variable1, type variable2, ...)
{
/* corps de la fonction */
/* déclarations de variables locales à la fonction */
/* initialisations */
/* instructions; */
return (valeur_retournée);
}
/* Appel de fonction */
#include <stdio.h> /* Entrées/Sorties */
/* prototype */
int demo (int i, double n);
-----------------------------------------------
APPEL DE FONCTIONS
Valeurs avant appel
Variable i=2
Variable n=10.000000
Remarque :
La fonction demo(i,n) fait la division de n par i, retourne le résultat transtypé entier mais ne
modifie ni la valeur de n ni celle de i. Le contrôle de i avant la division est indispensable pour éviter
une division par zéro.
L'exemple suivant propose une fonction qui modifie (par permutation) les éléments d'un tableau;
le tableau est passé à la fonction qui renvoie un tableau modifié.
/* Appel de fonction */
Jean Fruitet - IUT de Marne La Vallée - 32
Introduction à la programmation
#include "stdio.h" /* Entrées/Sorties */
#define MAXTAB 10 /* Constante */
/* définition de la fonction */
int echange (int i, int n[]) /* un tableau en argument */
{
int j, aux;
for (i=0, j=MAXTAB-1; i < j; i++, j--)
{
printf(" Echange %4d et %4d\n",n[i],n[j]);
aux = n[i];
n[i] = n[j];
n[j] = aux;
}
return(i);
}
---------------------------------------------
APPEL DE FONCTION PAR VARIABLE
void main(void)
{
unsigned long u;
printf("Entrez un nombre ");
scanf("%ld ",&u);
printf("Factorielle %ld : %ld\n",u, fact(u));
}
Structure
Une structure est une collection de données regroupées sous un seul nom.
Déclaration
Le type hms (heure, minute, seconde)
struct hms { /* déclaration et nom de la structure */
int h, m, s; /* champs de la structure */
} heurelocale; /* nom de variable */
Le type date utilise aussi le type hms :
struct date {
char *nom_jour,
nom_mois[4];
int jour,
mois,
annee;
struct hms heure; /* structure dans une structure */
}; /* le nom de variable est facultatif */
Affectation
L'opérateur . (point) permet d'accéder aux composantes d'une structure.
heurelocale.h = 10;
heurelocale.m = 07;
heurelocale.s = 55;
printf("%02d:%02d%:%02d\n",heurelocale.h,heurelocale.m,heurelocale.s) ;
L'opérateur -> (Fléche = MoinsSupérieur) permet d'accéder aux composants d'un pointeur sur une
structure.
struct date * ddnaissance = (struct date) *calloc(1, sizeof(struct date));
strcpy(ddnaissance->nom_jour, "Lundi");
strcpy(ddnaissance->nom_mois, "MAR");
ddnaissance->jour=19; ddnaissance->mois=3; ddnaissance->annee=1999;
Union
Une union est une variable qui peut contenir (à différents moments) des objets de types et de
tailles différents.
Exemple :
Jean Fruitet - IUT de Marne La Vallée - 35
Introduction à la programmation
union mot_machine {
char quatrecar[4];
int deuxentier[2];
long unlong;
} *mot;
Cette union, dont l'encombrment mémoire est de 4 octets, peut être initialisée de différentes façons.
Soit octet par octet :
mot->quatrecar[0] = 'a';
mot->quatrecar[1] = 'b';
mot->quatrecar[2] = 'c';
mot->quatrecar[3] = 'd';
Soit par séries de deux octest :
mot->deuxentier[0] = 0;
mot->deuxentier[1] = 1;
soit d'un seul coup :
mot->unlong = 1234567;
De plus l'espace mémoire désigné par l'union peut être interprété selon chacun de ces aspects par le
compilateur...
Directives de compilation
Typedef
Le mot réservé typedef crée de nouveaux noms de types de données (synonymes de types déjà
définis).
Exemple : typedef char * STRING; fait de STRING un synonyme de "char * "
#define
#define et #include sont des commandes du préprocesseur de compilation.
#define est un mot réservé qui permet de définir une macro
Exemple :
#define ESC 27
A la compilation la chaine ESC sera remplacée par 27 dans tout le fichier contenant cette définition
de macro.
Attention : une macro n'est pas une instruction donc il ne faut pas terminer la définition d'une
macro par le caractère ';'
#include "nom.h"
Inclusion du fichier nom.h cherché dans le répertoire courant puis dans les répertoires spécifiés par
l'option de compilation -I en TURBOC.
Directives de compilatio
Les directives de compilation permettent d'inclure ou d'exclure du programme des portions de
texte selon 'évaluation de la condition de compilation.
Exemple :
#ifndef (BOOL)
#define BOOL char /* type boolean */
#endif
#ifdef (BOOL)
BOOL FALSE = 0; /* type boolean */
BOOL TRUE = 1; /* définis comme des variables */
#else
#define FALSE 0 /* définis comme des macros */
#define TRUE 1
#endif
#if 0
Cette partie du texte sera ignorée par le compilateur
cela permet de créer des commentaires imbriqués et d'écarter momentanément du
texte source des parties deprogramme.
#endif
3 Je ne saurais trop conseiller à l'apprenti programmeur l'excellent ouvrage de Jacquelin Charbonnel "Langage C, les
finesses d'un langage redoutable" Collection U Informatique - Armand Colin 1992
Jean Fruitet - IUT de Marne La Vallée - 37
Introduction à la programmation
Les fonctions de lecture getchar(), gets() et scanf() lisent leurs données sur stdin.
Les fonction getc(), fgets() et fscanf() leur sont apparentées pour les flux non
prédéfinis de type FILE *.
Les fonctions d'écriture putchar(), puts() et printf() écrivent leurs données sur la
sortie standard stdout. Les fonctions putc(), fputc() et fprintf() s'adressent à des
flux non prédéfinis, de type FILE *.
Les fonctions fread() et fwrite() permettent de lire et d'écrire des blocs d'enregistrements.
La fonction fseek() positionne le pointeur de flux à une position donnée (déplacement de la tête
de "lecture / écriture") ; la fonction ftell() indique la position courante du pointeur de flux.
Lecture du clavier / écriture à l'écran : getchar() et putchar(), gets() et puts()
getchar() : lecture (par le processeur) d'un caractère au clavier, "l'entrée standard"
Syntaxe : int getchar(void);
Renvoie un entier sur l'entrée standard (cet entier est un code ASCII) ; renvoie EOF quand elle
rencontre la fin de fichier ou en cas d'erreur.
putchar() : écriture (par le programme) d'un caractère sur l'écran, "la sortie standard"
Syntaxe : int putchar(int c);
Place son argument sur la sortie standard. Retourne c si la sortie est réalisés, EOF en cas d'erreur.
Les entrées de getchar() et les sorties putchar() peuvent être redirigées vers des fichiers.
gets() : lecture d'une chaine de caractères au clavier.
Syntaxe : char *gets(char *s);
Lit une chaine depuis l'entrée standard stdin et la place dans la chaîne s. La lecture se termine à la
réception d'un NEWLINE (saut de ligne) lequel est remplacé dans s par un caractère ASCII nul '\0'
de fin de chaîne. Renvoie NULL quand elle rencontre la fin de fichier ou en cas d'erreur.
puts() : écriture d'une chaine de caractères à l'écran.
Syntaxe : int puts(const char *s);
Recopie la chaine s dans la sortie standard stdout et ajoute un saut de ligne. Renvoie le dernier
caractère écrit ou EOF en cas d'erreur.
Formatage des entrées clavier / sorties écran : printf() et scanf()
Les fonctions de la famille de printf() et de scanf() permettent les sorties et les entrées
formatées de chaînes de caractères. Un tableau de leurs caractéristiques assez complexes est donné
en annexe.
Fichiers de données
Il s'agit de fichiers non prédéfinis, par exemple des données à traiter par programme.
On distingue selon le type de support et de contenu
• les fichier séquentiels : séquence d'enregistrements auxquels on accéde dans l'ordre séquentiel,
c'est-à-dire du premier au dernier en passant par tous les enregistrements intermédiaires. Par
exemple une bande magnétique (K7 audio). Le clavier est vu par l'ordinateur comme un fichier
séquentiel en lecture. L'écran est un fichier en écriture (pas nécessairement séquentiel).
• les fichier à accès direct : séquence d'enregistrements sur mémoire externe ou interne auxquels on
accéde directement, par l'adresse individuelle de chaque enregistrement (mémoire vive RAM :
Random Access Memory) ou par indirection (tableau indexé).
• les fichier binaires : ce sont des fichiers structurés sous forme d'enregistrements (struct en langage
C), dont le contenu doit être interprété par le programme qui les a créés.
• les fichiers textes : ce sont des documents textuels en ASCII, qu'une commande comme TYPE
peut afficher.
Compilation
La compilation de programmes en C s'effectue en plusieurs
étapes.
1°) Précompilation
Le précompilateur supprime les commentaires des fichiers sources, inclut les fichiers #include et
substitue les définitions #define dans tout le fichier <Source> (*.C)
2°) Compilation (compile)
Les fichiers sont alors recodés en fichiers <Objet> (binaire) (*.OBJ). Les noms de fonctions sont
remplacés par des étiquettes.
3°) Liaison (link)
Les différents fichiers objets sont ensuites "liés", c'est-à-dire que les étiquettes d'appel de fonctions
sont remplacées par l'adresse de celles-ci dans leur propre fichier objet. On obtient un fichier
exécutable (*.EXE).
La compilation est interrompue en cas d'erreur fatale. Les "warnings" sont des erreurs non fatales
dont il faut tenir compte pour avoir un programme à peu près fiable et portable.
Mais ce n'est pas parce que la compilation s'est passée sans erreur que le programme est correct. Le
compilateur ne vérifie pas les débordements de tableau (Range Check error), ni bien sûr les erreurs
d'allocation en mémoire dynamique ou la cohérence du programme.
Interdépendance de fichiers
Il est possible en C de partager un programme en plusieurs fichiers.
TEST.C ===========================
/* Code de la fonction test()
Peut être inclus dans une bibliothèque (Library) test.lib
avec l'utilitaire TLIB.EXE ou directement nommé dans le
fichier projet (en TURBOC) */
DEMO.C =========================
/* --- Fichier Source du Programme Principal DEMO.C ----
Programme exécutable appelant la fonction test()
Doit contenir une fonction main() */
#include <stdio.h>
#include "test.h" /* déclaration de la fonction externe test() */
Pour que la compilation se passe correctement, le compilateur doit pouvoir retrouver ses petits...
En TURBOC sous éditeur vous pouvez définir un fichier de PROJET (Alt P) associé à DEMO.C qui
indique les dépendances entre les différents fichiers du programme.
DEMO.PRJ ========================
demo.c (test.h)
test.c
=================================
COMPILATION :
de test.c avec Alt C : compile to obj
de demo.c avec F9
Processus itératifs.
Dans tous les exercices suivants, nous reprenons la même méthode pour introduire de nouvelles
notions de programmation : énoncer un problème numérique, en donner un algorithme, le traduire
dans les différents langages de programmation à notre disposition.
Exemple : Ecrire un programme qui affiche les 10 premiers nombres entiers naturels [0, 1,
2, .., 9].
1. Rappels mathématiques
L’ensemble des entiers naturels IN est infini. Il a un plus petit élément, noté 0 ; tout élément a un
successeur. Il n’y a pas de plus grand élément. Les éléments sont ordonnés. IN={0, 1, 2, 3, 4, ..., n,
(n+1), ...}
Enoncer l’ensemble des entiers naturels n’est pas une tâche que puisse réaliser un ordinateur, car
cet ensemble est infini. Par contre il n’y a pas de difficulté à énoncer un sous-ensemble de IN. Il nous
faut cependant introduire une recette de génération des entiers, c’est-à-dire un algorithme. Il est
assez légitime de considèrer la construction de IN à partir de son plus petit élément, 0, en appliquant
l’axiome “tout élément a un successeur”.
Soit le premier élément 0 Début du processus
successeur(0) = 0 + 1 == 1
successeur(1) = 1 + 1 == 2
successeur(2) = 2 + 1 == 3
..
successeur(8) = 8 + 1 == 9 Fin du processus.
On a mis en évidence un processus répétitif, nous dirons ‘itératif’, selon un terme propre à
l’informatique. Toute constante en membre droit d’une affectation passe en membre gauche à
l’itération suivante. Nous devons résoudre deux difficultés. Initialiser le processus, puis l’interrompre
quand la borne 9 est atteinte, ce qui implique de compter le nombre d’itérations.
2. Types de données et algorithmes
Définissons les types de données qui seront utilisés.
0 et 1 sont des constantes numériques. Nous utilisons aussi deux opérateurs, l’addition entre
entiers et l’affectation. Enfin il nous faut définir un compteur, qui comptera les itérations et
permettra d’interrompre le processus. Ce programme pourrait donc se traduire par “Initialiser un
compteur à 0. Tant que le compteur est inférieur à 9, incrémenter le compteur de 1”.
Traduit de cette façon, le programme est un processus dynamique, c’est-à-dire qui évolue au
cours du temps. Le compteur, une variable entiére, prend successivement les valeurs 0, 1, .., 9.
L’affichage de la valeur du compteur répond au problème. Il reste à traduire cet algorithme en
programme (4) ce qui est fait à la figure suivante.
4 Il faut insister ici sur le caractère très spécial de l’instruction d’affectation qui se trouve dans la boîte (1). Il ne s’agit
pas d’une égalité au sens mathématique. On doit plutôt la traduire par : “le contenu de l’adresse mémoire désignée par
l’identifieur ‘compteur’ est augmenté de 1”. Pour nous conformer à la syntaxe du langage C nous représenterons
dorénavant l’affectation (ou assignation) par le symbole ‘=‘, et l’égalite (comparaison) par ‘==‘.
ENTIER Ecrire(compteur);
(1) Traduit en langage de la machine RAM cela
compteur; compteur=compteur+1; donne :
compteur=0;
ENTIER compteur;
POUR (compteur=0 à 9)
OUI
{
(compteur <= 9)
ECRIRE(compteur);
}
NON FIN;
FIN DU PROGRAMME;
et en langage C:
#include <stdio.h> /* Fichier inclus : bibliothèque des Entrées/Sorties */
Nous aurions évidemment pu traduire cet algorithme en utilisant les structures de contrôle TANT
QUE (test) FAIRE instruction ; nous en laisserons le soin au lecteur.
Fonctions et sous-programmes
L’exercice suivant va introduire la notion de fonction en programmation.
Certains problèmes sont tellement complexes qu’il est préférable de les décomposer en sous-
problèmes. Cette forme de programmation, dite programmation descendante, s’attache à exprimer le
cadre général de résolution d’un algorithme puis à détailler les sous-programmes résolvant les
questions techniques. Donnons un exemple.
Exemple Ecrire un programme qui affiche les 10 premiers nombres premiers [2, 3, 5, 7,.., 29].
Comme convenu, commençons par un rappel mathématique. Un nombre premier est un nombre
entier naturel qui n’est divisible que par 1 et par lui-même (1 n’est pas considéré comme premier).
Résoudre l’exercice peut se traduire à première vue par :
TANT QUE (compteur<10)
{
Générer_un_nouvel_entier_premier;
Incrémenter(compteur);
}
Le sous-programme Générer_un_nouvel_entier_premier; nécessite d’être formulé plus
précisément : étant donné un nombre (éventuellement premier), il s’agit de trouver le nombre
premier immédiatement supérieur.
nombre=1;
TANT QUE (compteur<10)
{
nombre = Génére_premier_suivant(nombre);
Incrémenter(compteur);
}
ce qui, traduit en langage graphique, donne
Jean Fruitet - IUT de Marne La Vallée - 44
Introduction à la programmation
OUI
(compteur <= 9)
NON
FIN DU PROGRAMME;
La boucle est triviale, à peu de chose près c’est celle de l’exercice précédent, avec un compteur
comptabilisant les nombres premiers générés.
Par contre le sous-programme de la boîte (1), générant des nombres premiers, doit maintenant être
précisé.
Pour tester si un nombre n’est pas premier, il suffit de lui trouver un diviseur autre que 1 et lui-
même. Ainsi à l’exception du nombre entier 2, tous les nombres pairs sont non premiers ; idem des
multiples de 3, 5, 7, etc. C’est le crible d’Eratosthéne, qui consiste à éliminer les multiples d’un
nombre premier de la liste des nombres entiers. A la “fin” seuls les nombres premiers subsistent.
Mais ce procédé ne convient pas à un ordinateur, car l’élimination des nombres pairs est en soi un
processus infini. Il faut utiliser une autre méthode, tester par exemple pour chaque nombre s’il est
divisible par l’un de ses prédécesseurs inférieurs à n (ou inférieurs ou égaux à n pour gagner
quelques itérations)... Cet algorithme n’est pas très efficace, mais il est simple à programmer. Il fait
appel à une fonction qui retourne VRAI si un nombre est premier, FAUX sinon.
On introduit ici la notion de fonction, qui par construction est un sous-programme retournant une
valeur d’un type élémentaire (entier, réel, booléen, caractère, énuméré, pointeur).
Programmons donc la fonction GENERE_PREMIER_SUIVANT(n) qui retourne le nombre premier
strictement supérieur à n :
ENTIER GENERE_PREMIER_SUIVANT(ENTIER n)
n n=n+1;
NON
(2) (PREMIER(n))
OUI
RETOUR(n);
Le test (2) fait appel à une fonction, PREMIER(n), qu'il nous faut programmer. Cette fonction
retourne VRAI si n est premier et retourne FAUX sinon, elle est donc de type BOOLEEN, c'est
pourquoi elle est utilisable dans le test. L’algorithme de cette fonction consiste à rechercher si une
division par un entier compris entre 1 et n — 2, 3, etc.— “tombe juste”...
NON
diviseur=diviseur+1; (diviseur<n) RETOUR(VRAI);
OUI
NON
((n MODULO diviseur)==0) (3)
OUI
RETOUR(FAUX);
PROGRAMME Liste_10_Premiers_Version1
{
ENTIER nombre, compteur;
nombre=1;
compteur=0;
TANT QUE (compteur < 10)
{
nombre=genere_premier_suivant(nombre);
ECRIRE(nombre);
compteur=compteur+1;
}
Simplification du programme
Après avoir minutieusement détaillé ce programme, nous pouvons essayer de le simplifier.
Remarquons d'abord que la fonction
GENERE_PREMIER_SUIVANT ()
est une simple boucle qui peut être remplacée par une instruction et un test. Le programme devient :
PROGRAMME Liste_10_Premiers_Version2
{
ENTIER nombre, compteur;
nombre=1;
compteur=0;
TANT QUE (compteur < 10)
{
nombre=nombre + 1;
SI (premier(nombre))
ECRIRE(nombre);
compteur=compteur+1;
}
Puis, en considérant qu'au delà de 3, les valeurs successives de n sont des nombres impairs on peut
réécrire ce programme :
PROGRAMME Liste_10_Premiers_version3
{
ENTIER nombre, compteur;
nombre=3;
compteur=2;
ECRIRE(2);
ECRIRE(3);
TANT QUE (compteur < 10)
{
nombre=nombre + 2;
SI (premier(nombre))
ECRIRE(nombre);
compteur=compteur+1;
}
Ce programme peut être amélioré en recherchant les diviseurs de n inférieurs à Racine(n). Nous
laisserons le lecteur s’y exercer...
Evaluons la complexité de cet algorithme sur un jeu de données. Les cas à considèrer sont :
a et b premiers entre eux ;
a diviseur de b ;
a et b non premiers entre eux sans qu’aucun ne soit diviseur de l’autre ;
Notons entre parenthèses () le nombre d’instructions. Un test sur le modulo compte pour 3 car il
faut l’évaluer, comparer sa valeur à 0 et faire un saut.
12 9 13 ... 5 15
0 1 2 6 7
Liste des notes de l’élève Dupont
0
10 0 0 0 0 0
Ligne
1
20 90 0 40 0 0
2
30 20 10 20 50 0
Colonne 1
Ligne
10 10 10 Profondeur 0
0
1 10 10 10
2 10 10 10
B o n n e
0 1 2 3 4
En fin de processus le tableau contient [1, 3, 5, 7, 9]. La première cellule est tab[0] ; elle vaut 1. La
cellule tab[3] contient 7.
Exercice
Ecrire en C un programme qui range les 100 premiers nombres premiers dans un tableau.
Algorithme : Initialiser le tableau à 2, 3, 5, 7 puis générer la suite des nombres entiers impairs, en
testant pour chaque nombre s’il est divisible par l’un de ses prédécesseurs premiers rangés dans le
tableau et inférieur ou égal à Racine(n) (justifiez d’abord cet algorithme).
Corrigé do
{
/* Nombres premiers */ j=0;
#include <stdio.h> while ((n%premier[j]) &&
#include <math.h> /* F. Mathéma. */ (premier[j]<=sqrt(n)) && (j<i))
#define MAXTAB 100 j++;
if (n%premier[j])
void main(void) /* Programme {
principal */ printf(" %d,",n);
{ premier[i++]=n;
int i=4, j, n=9; }
int premier[MAXTAB]={2,3,5,7,}; /* n+=2;
Déclarations */ } while (i<MAXTAB);
printf("2, 3, 5, 7,");
} /* Fin du programme */
5 Nous conserverons la convention du langage C qui est de numéroter 0 la première cellule.Par conséquent pour un
tableau de 10 cellules, la dernière a l’indice 9.
Jean Fruitet - IUT de Marne La Vallée - 52
Introduction à la programmation
6 Ce chapitre est repris in extenso de l'excellent ouvrage de Cohen J.H., Joutel F., Cordier Y., Jech B. "Turbo Pascal,
initiation et applications scientifiques", Ellipses, 1989. J'ai seulement traduit les programmes en C.
Avec cette méthode la première matrice 2x3 est déclarée et initialisée par :
matrice mat_2x3 = {2, 3, {1.0, 1.2, 0.5, 0.0, -1.4, 0.9, 2.1, 0.0, 0.0, 0.0,
0.0, 0.0, 0.0, 0.0, 0.0, 0.0}};
Bien sûr toutes les valeurs ne sont pas significatives, mais le tableau de données doit être complété
avec des valeurs nulles, et c'est par programme qu'on se restreint aux lignes et colonnes utiles...
Produit de deux matrices
Il suffit de trois boucles imbriquées pour calculer le produit de deux matrices.
Avant appel il faut réserver l'espace mémoire pour la matrice produit
z = (t_matrice *) calloc(1, sizeof (t_matrice));
Triangularisation
La triangularisation de la matrice est une étape préalable à la résolution d'un système d'équations
linéaires.
Implantation
/* Résolution d'un système d'équations }
Méthode du pivot partiel */ }
Calcul du déterminant
La méthode de Gauss permet également de calculer le déterminant et l'inverse d'une matrice
carrée régulière d'ordre N. En effet, échanger deux lignes d'une matrice transforme le déterminant en
son opposé ; ajouter à une ligne une combinaison linéaire des autres lignes ne modifie pas le
déterminant.
Ainsi, lorsque dans la méthode du pivot partiel on obtient une matrice triangulaire, le déterminant
de celle-ci est, au signe près, le déterminant de la matrice dont on était parti ; ce signe dépend du
nombre d'échanges de lignes fait lors de la recherche des différents pivots : si p est ce nombre le
déterminant cherché est le produit de (-1)p et des n pivots trouvés.
Exercice : Ecrire la fonction double determinant(int ordre, matrice_carree
x) qui calcule le déterminant d'une matrice carrée d'ordre N par l'algorithme du pivot.
Inversion de matrice
Les manipulations des lignes d'une matrice reviennent à multiplier celle-ci à gauche par des
matrices convenables. Il n'est pas difficile de prouver que le produit de ces matrices est la matrice
inverse de la matrice de départ lorsque par des manipulations de lignes on transforme cette matrice
en l'identité. Pour effectuer ce produit (et en garder la trace) il suffit donc d'appliquer l'ensemble de
ces manipulations à la matrice identité : le résultat final est bien ma matrice inverse cherchée.
Le code de la fonction
int matrice_inverse(int ordre, matrice_carree a, matrice_carre inv)
se déduit donc directement de la fonction gausspartiel() :
Jean Fruitet - IUT de Marne La Vallée - 58
Introduction à la programmation
return (1);
}
/* ---------------------------- */
/* Programme principal */
clrscr();
printf("Matrice à inverser \n");
affiche_matrice(N, a);
if (matrice_inverse(N, a, inv))
{
printf("Matrice inverse \n");
affiche_matrice(N,inv);
printf("Verification \n");
produit_matrice (N, a, inv, z);
affiche_matrice(N,z);
}
getch();
}
Rappels :
La moyenne (ou espérance) d’une distribution (Xi) s’obtient en calculant
1 n −1
x = ∑ xi
n i =0
L’écart type est la racine carrée de la variance :
n −1
1
σ=
n
∑
i =0
(x i − x ) 2
Le minimum (respectivement le maximum) d’une distribution est la grandeur Xmin (Xmax) qui est
inférieure (respectivement supérieure) ou égale à toutes les autres valeurs Xi.
La seule difficulté de l’exercice consiste à définir correctement les structures de données permettant :
1) de stocker la distribution en mémoire
REEL tab[10]; et en langage C float tab[10];
2) restituer les grandeurs demandées.
REEL moyenne(); float moyenne();
REEL min(); float min();
REEL max(); float max();
REEL sigma(); float sigma();
Le tableau de réels s’impose. Mais le type réel n’étant pas disponible en langage C, il faudra utiliser
le type flottant (float). Pour que le programme conserve une certaine généralité, on définira une
constante entiére TAILLE de valeur 10 et on écrira les calculs sous forme de fonctions
indépendantes lisant leurs entrées dans la variable globale tableau[], recevant le nombre d’éléments
du tableau en paramètre, et retournant un flottant.
Remarques
La bibliothèque mathématique est indispensable pour la fonction sqrt() qui retourne la racine
carrée d’un nombre flottant. La déclaration de constante #define TAILLE 10 permet de
modifier aisément en une ligne la taille du tableau float tableau[TAILLE]. La définition
de macro
#define CARRE(x) ((x)*(x)) rend la lecture du programme plus claire.
Les fonctions
float min_tab(int n);
float max_tab(int n);
float moyenne_tab(int n);
float ecart_type_tab(int n);
calculent entre l’indice 0 et l’indice (n-1) du tableau, ce qui permet une certaine souplesse.
Par contre il faudra veiller, si on reprend ces fonctions dans un autre programme, à ce que le
paramètre n ne soit jamais nul à l’appel de moyenne_tab (int n) et ecart_type_tab
(int n) sous peine d’avoir une division par zéro et l’arrêt brutal du programme.
Considérons la suite (u0= 15001) ; on obtient la suite (ui) = (15001, 45004, ...).
45004 est en dehors de l’intervalle de définition des entiers sur deux octets [-32768.. 32767]. Donc
même si l’algorithme est correct, le calcul n’est pas possible. Il faudrait programmer les opérations
avec des entiers longs (4 octets), ce qui ne fait que repousser les difficultés à des nombres plus
grands. Et l’utilisation de nombres flottants ne résoudrait rien.
Fonctions récursives
Certaines suites ont une définition récurrente qui se prête bien à la programmation dite récursive.
Factorielle
Programmer factorielle(n) notée (n!) définie par :
(0!) = 1; (1!) = 1; (2!) = 2 x (1!); ...; n!= n x ((n-1)!)
Cela se traduit directement en langage C par
∑
1
Sn = 2
Calculez la limite de la suite i =1 i
sachant que l’on arrête l’exécution lorsque
S n+1 − S n < ε avec ε un réel proche de zéro .
PROGRAMME suiteS(ENTIER n)
• Valeur approchée de π 2 /6
i =∞
∑
1 2
Sn = π
2
Vérifiez expérimentalement que i =1 i est une bonne approximation de 6 .
Pour la valeur de π on pourra utiliser la fonction arctan(1) = π/4.
Cet exercice est une variante du précédent. Il suffit de tester la différence entre Sn et */6 jusqu’à
ce qu’elle soit inférieure en valeur absolue à un ε donné.
• Valeur approchée de log(n)
n
∑
1
Sn =
i =1
i
Calculer à une précision fixée la différence entre la somme et Ln(n).
Dans cet exercice, la convergence de Sn vers Ln(n) —logarithme népérien de n— n’est pas du tout
garantie. Il faut donc choisir soigneusement le test d’arrêt, par exemple en vérifiant la croissance
respective de la série et de la fonction.
• Termes d’une suite
Ecrire un programme qui calcule et affiche les valeurs de la suite (un)n1 (n donné par l’utilisateur)
définie par
u n = 1+ 2 + 3 + 4 + . .. + n
Contrairement à l’exercice précédent, il n’y a pas de formule de récurrence immédiate entre les
termes de la suite Un ; par contre il est facile d’évaluer le n-iéme terme de la suite par une itération
avec un invariant de boucle sous le radical :
U = n
de i = n à 1 faire
U = (i − 1) + U
Ce qui donne en langage RAM :
REEL suiteU(ENTIER n)
{ PROGRAMME ListeUn(ENTIER n)
ENTIER i=n; {
REEL U=n; ENTIER i=1;
TANT QUE (i>0) FAIRE REEL S=0.0;
{ REEL EPSILON=0.000001;
U=(i-1)+√U;
i=i-1; POUR (i=1 jusqu’à n) FAIRE
} {
RETOUR (U); ECRIRE(suiteU(n);
} }
φn
On pourra utiliser la fonction Puissance(x,n) de l’exercice précédent pour calculer 5.
Nous laissons au lecteur le soin de vérifier la convergence des deux suites.
• Décomposition d’un cube en somme de nombres impairs
Le mathématicien grec Nikomakhos (1er siècle après JC) écrit dans son Introduction Arithmétique
que tout cube est égal à la somme de nombres impairs consécutifs. Par exemple :
13 = 1 = 1
23 = 8 = 3 + 5
33 = 27 = 7 + 9 + 11
Jean Fruitet - IUT de Marne La Vallée - 67
Introduction à la programmation
43 = 64 = 31 + 33
= 1 + 3 + 5 + 7 + 9 + 11 + 13
= 13 + 15 + 17 + 19
Ecrire un programme qui lit un entier n et donne une décomposition de n3 (on pourra utiliser une
boucle TANT QUE et un BOOLEEN trouvé qui indiquera que l’on a trouvé une solution).
Le cas des cubes pairs est trivial, il suffit de décomposer n3 en (n3/2 - 1) et (n3/2 +1). Pour les cubes
impairs, on peut chercher à décomposer à partir de 1 :
S = (1 + 3 + ... + (2p-1) + (2p+1)) en incrémentant p jusqu’à ce que S n 3...En cas de
dépassement, il faut alors éliminer les nombres impairs les plus faibles à gauche :
S = ((2q-1) + (2q+1) + ... + (2p-1) + (2p+1)) jusqu’à ce que S n 3. En incrémentant
successivement q et p on finit par atteindre n3 exactement.
Résolution de f(x)=0 (7)
Soit f une fonction continue dans l’intervalle [a,b] avec a<b et telle que f(a).f(b)<0 ; il existe donc
un réel x compris strictement entre a et b vérifiant f(x) = 0. Trouver cette valeur x c’est résoudre
l’équation f(x)=0. Plusieurs méthodes numériques s’y emploient. La méthode dichotomique est la
plus simple à programmer. Cependant le test d’arrêt doit être soigneusement sélectionné pour éviter
les erreurs dues à la représentation approchée des nombres réels par des flottants...
Méthode dichotomique
Considérons c = (a+b) / 2. Si f(c).f(a) 0, il existe une racine de l’équation dans l’intervalle ]a,
c], sinon il en existe une dans l’intervalle ]c, b[. On recommence alors la recherche dans l’intervalle
qui contient sûrement une racine, intervalle dont la longueur est la moitié de celle de l’intervalle de
départ. En un nombre fini d’étapes, on arrive à encadrer une racine de l’équation par deux réels dont
la différence est plus petite que la précision demandée pour la recherche.
f(b) y=f(x)
f(c)
a c b
f(a)
c = (a+b)/2.
PROGRAMME Dichotomie(REEL a, REEL b, REEL epsilon)
{
REEL c;
SI (f(a)*f(b)) < 0.0
{
TANT QUE(b-a>epsilon)
{
c=(a+b) / 2;
7J.-H. Cohen, F. Joutel, Y. Cordier, B. Jech, “Turbo Pascal, initiation et applications scientifiques”, Elipses, pp127-
131
Jean Fruitet - IUT de Marne La Vallée - 68
Introduction à la programmation
SI (f(a)*f(c)<=0.0)
b=c;
SINON
a=c;
}
}
}
L’inconvénient de cette méthode est que si f(x) est de croissance (respectivement décroissance) lente
la précision sur x sera très longue à atteindre. D’autre part il peut se produire que (b/2) et (a/2)
soient représentés par le même nombre flottant (la même représentation binaire). En ce cas
l’algorithme echoue ! On préférera donc tester une valeur de f(x) proche de zéro à ε près.
Exercice : Programmer cet algorithme en Langage C pour les fonctions y= x5/100 et y=2sin(x)-
1 sur un intervalle convenable avec une précision de 10-5. Indiquer le nombre d’itérations.
/* Résolution de f(x) = 0 */ printf("Calcul à EPSILON=%2.6f
/* Méthode Dichotomique */ près\n",EPSILON);
/* f continue dans l'intervalle [a,b] fa=FONCTION(a);
et (f(a).f(b)<0 fb=FONCTION(b);
La méthode consiste à calculer c= if ((fa*fb)>=0.0)
(a+b)/2 et f(c) et à remplacer a par {
c si f(a).f(c)>=0, b par c sinon. */ printf("Méthode dichotomique
inapplicable \n");
#include <stdio.h> return(0);
#include <math.h> }
Il suffit de modifier ces cinq ligne de programme pour traiter tout autre fonction.
Méthode du balayage
f continue dans l’intervalle [a,b] et f(a)f(b)<0.
Jean Fruitet - IUT de Marne La Vallée - 69
Introduction à la programmation
La méthode consiste à déterminer n intervalles sur [a,b], à calculer c = a + k*(b-a)/n et d = a +
(k+1)*(b-a)/n et f(c) et f(d) tant que f(c).f(d)>0 et à remplacer a par c et b par d sinon.
Méthode par balayage avec n=3
y=f(x)
f(b)
f(d)
f(c)
a c d b
X
f(a)
f(b)
y=f(x)
f(c)
a c b X
f(a)
Méthode de Newton
La méthode de Newton abandonne l’idée de l’encadrement, pour essayer d’augmenter la vitesse
de convergence. Cette méthode n’assure plus la découverte systématique de la racine, mais
lorsqu’elle le fait elle est en général plus rapide que les méthodes précédentes.
Supposons f suffisament dérivable. La formule de Taylor appliquée au point a s’écrit :
h2
f (a + h ) = f (a) + hf ' (a) + f "(a + θh), θ ∈]0, 1[
2
Si on suppose que a+h est une racine, h est solution de l’équation f(a+h) ; en supposant h
f (a )
h =−
suffisament petit , on en déduit, en première approximation : f ' (a )
Implantation
f continue et dérivable sur l’intervalle [a,b].
Dérivée f’ de signe constant (0) sur ]a, b[
La méthode consiste à construire la tangente T en B(b, f(b)) et à prendre comme valeur approchée
de la racine l’intersection de T avec l’axe des X. b est remplacé par cette valeur et on réitére le
processus jusqu’à coïncidence.
f(b) y=f(x)
a
c b X
f(c)
f(a)
Intégration numérique
Soient a et b deux réels (a<b), f une fonction numérique continue sur [a,b] ; on cherche à approximer
b
∫a f (t )dt . Prenons une subdivision régulière de [a,b] en n intervalles : posons
b− a
h= et x k = a + kh ,
n pour tout entier k compris entre 0 et n.
On a :
b n−1 x k+1
∫a f (t)dt = ∑ ∫x k
f (t)dt
k =0
Méthode de Simpson
C’est une généralisation de la méthode des trapèzes consistant à approximer la fonction sur chaque
sous-intervalle [xk,xk+1] de la subdivision par un polynôme du second degré (interpolation de
Lagrange avec les points xk, (xk+xk+1)/2 et xk+1).
Interpolation (8)
Supposons qu’en étudiant un certain phénomène, on ait démontré l’existence d’une dépendance
fonctionnelle entre des grandeurs x et y exprimant l’aspect quantitatif de ce phénomène ; la fonction
y=f(x) n’est pas connue, mais on a établi en procédant à une série d’expériences que la fonction
y=f(x) prend respectivement les valeurs y0, y1, y2, y3, ..., yn quand on donne à la variable
indépendante les valeurs différentes x0, x1, x2, x3, ..., xn appartenant au segment [a, b].
Le problème qui se pose est de trouver une fonction aussi simple que possible (un polynôme par
exemple), qui soit l’expression exacte ou approchée de la fonction inconnue y=f(x) sur le segment [a,
b]. D’une manière plus générale le problème peut être posé comme suit : la valeur de la fonction
y=f(x) est donnée par n + 1 points différents x0, x1, x2, x3, ..., xn du segment [a, b] :
y0 = f(x0), y1 = f(x1), ..., yn = f(xn) ;
on demande de trouver un polynôme P(x) de degré n exprimant d’une manière approchée la fonction
f(x).
Il est tout naturel de choisir le polynôme de manière qu’il prenne aux points x0, x1, x2, x3, ..., xn les
valeurs y0, y1, y2, y3,. .., yn de la fonction f(x).
Y
y=f(x)
y0 y1
yn
y=P(x)
0 ax x x x b X
0 1 2 n
En substituant les valeurs ainsi trouvées des coefficients dans la formule (1) nous avons :
( x − x1 )( x − x 2 ). .. ( x − x n ) ( x − x 0 )( x − x 2 ). . . ( x − x n )
P( x ) = y0 + y
( x 0 − x1 )( x 0 − x 1 ). .. ( x 0 − x n ) ( x 1 − x 0 )( x1 − x 2 ). .. ( x 1 − x n ) 1
( x − x 0 )( x − x 1 ). .. ( x − x n−1 )
+. .. .. . . + yn . ( 3)
( x n − x 0 )( x n − x1 ). . . ( x n − x n−1 )
Cette formule est appelée formule d’interpolation de Lagrange. Rappelons que si aux points
considérés (x0, y0), (x1, y1), ..., (xn, yn), P(x) et f(x) coïncident, en tout autre point de l’intervalle
[a, b] il peut en aller tout autrement.
Il existe d’autres formules d’interpolations, dont celle de Newton.
Pour programmer la formule d’interpolation de Lagrange il faut saisir les points (x0, y0), (x1, y1),
..., (xn, yn), dans un tableau de couples de flottants et calculer le tableau des coefficients.
Structures de données
Il est souvent nécessaire en programmation de stocker des informations complexes. Par exemple
un répertoire téléphonique. Chaque fiche rassemble les informations caractéristiques d’une entité, en
l’occurence une personne, nom, prénom, adresse, date de naissance, téléphone. Chaque champ
d’une fiche est un attribut, exactement défini par son domaine (les valeurs acceptables), sa taille
(espace mémoire occupé), et sa valeur (ou occurence). L’information est structurée, puisque chaque
fiche est organisée d’une manière figée. Le répertoire téléphonique lui-même est une collection de
fiches de même structure.
Structures
Le type du langage C struct permet de représenter en machine la fiche que nous venons
d’évoquer (en Pascal lui correspond le type record — enregistrement).
struct fiche {
char nom[20]; char prenom[10]; char adresse[40];
char ddn[6]; long tel;
} personne;
On peut aussi inclure dans la définition d’une structure la référence à d’autres structures, par une
sorte d’imbrication. Par exemple près avoir défini un type de données date plus approprié,
struct date { int jour; int mois; int annee; } ;
s’y référer dans la définition d’un type fiche.
struct fiche {
char nom[20]; char prenom[10]; char adresse[40];
struct date ddn; char tel[10];
} personne;
Une personne est alors une variable occupant 86 octets en mémoire (20+10+40+6+10).
La mise à jour de la fiche de Dupond D. est réalisée par les instructions :
strcmpy(personne.nom,”Dupond”);
strcmpy(personne.prenom,”D.”);
strcmpy(personne.adresse,”Moulinsac”);
personne.ddn.jour=24;
personne.ddn.mois=12;
personne.ddn.annee=1901;
strcmpy(personne.tel,”98765432”);
Nous renvoyons le lecteur à son cours de langage C pour les détails de program-mation.
Exercices
• Le type <Date>
On considère le type <date> défini par une structure C
typedef struct date
{int annee; int mois; int jour;} date;
et la déclaration :
date date_courante = (1997, 10, 26);
1) Quelles opérations sur ce type de données peut-on définir ?
Jean Fruitet - IUT de Marne La Vallée - 76
Introduction à la programmation
2) Est-il possible d’additionner deux dates ? Que signifierait :
(1994, 10, 26) + (1994, 10, 26) ?
3) La différence de deux dates est une <durée> :
long duree; /* exprime le nombre de jours entre deux dates */
duree = difference_date((1994, 10, 26), (1994, 09, 26));
duree = 30; /* en jours */
Connaissant la date de naissance d’une personne et la date courante, il est possible d’exprimer l’âge
de cette personne en jours :
age = difference_date(date_naissance, date_courante);
Ensembles
La gestion d’informations structurées est un domaine dans lequel les ordinateurs excellent, au prix
d’une représentation astucieuse des données. Nous allons passer en revue quelques structures de
données classiques.
Définition
Un ensemble est une collection d’objets, appelés éléments de l’ensemble.
Classiquement, un ensemble est défini
• soit en intention, par une propriété vérifiée par tous les éléments de l’ensemble.
Exemples :
- ensemble des entiers naturels impairs
I = {n / n=2p+1} avec p entier naturel ;
- ensemble des nombre pairs
M2 = {n / n=2p} ;
- ensemble des multiples de trois
M3 = {n / n = 3p}
- ensemble des étudiants de Terminale B2 ;
- ensemble des ouvrages de philosophie, etc.
• soit en extention, par l’énumération exhaustive des éléments (ce qui suppose que l’ensemble soit
énumérable).
Exemple :
- Arc_en_ciel = {violet, indigo, bleu, vert, jaune, orange, rouge} ;
- Alphabet = {a, b, c, d, e, ...} ;
- Famille Duraton = {Annie, Bernard, Camille, Dominique, Edmond} ;
Cours des monnaies Change = {(Mark, 3.4553), (Ecu, 6.33), (Dollar, 4.89), (FB, 0.1680), ...}
Opérations sur les ensembles
L’algèbre sur les ensembles repose sur quelques opérations élémentaires dont :
• L’appartenance d’un élément à un ensemble (opérateur ∈)
Exemple : (Lire, 0.00306) ∈ Change ; Patrick ∉ Duraton
• La réunion de deux ensembles, opérateur ∪
Exemple : Alphabet = Voyelle ∪ Consonne
• L’intersection de deux ensembles, opérateur ∩
Exemple : M6 = M2 ∩ M3
Rappelons que la notion d’ensemble n’implique pas de relation d’ordre entre les éléments (bien
qu’un ensemble puisse être ordonné), et qu’un ensemble n’admet pas de doublons.
Représentation des ensembles : tableaux, listes, piles, files
Il faut distinguer d’une part la représentation en machine des éléments d’un ensemble, et d’autre
part l’implémentation des opérations élémentaires (appartenance, union, intersection). Ces
opérations dépendront en partie de la représentation des données (les éléments).
• Tableaux
La représentation sous forme de tableau est la plus immédiate. En reprenant le cas d’un répertoire
téléphonique, constitué de 200 fiches on peut définir :
struct fiche personne[200];
Appartenance
Jean Fruitet - IUT de Marne La Vallée - 79
Introduction à la programmation
Vérifier l’appartenance d’un élément consistera alors à parcourir tout le tableau (soit n comparaisons
s’il y a n fiches...)
Il faut disposer d’une fonction permettant de comparer deux éléments, qui doit être adaptée aux
données à comparer, mais dont la spécification est en général toujours la même :
si a==b, comparer(a,b) retourne 0
si a<b, comparer(a,b) retourne -1
si a>b, comparer(a,b) retourne +1.
#define FAUX 0
#define VRAI 1
int comparer(struct fiche e1, struct fiche e2);
/* retourne -1 si e1<e2 ; 0 si e1==e2; 1 si e1>e2 */
Réunion
La réunion de deux ensembles peut se concevoir de différentes façons, par exemple en insérant un à
un les éléments de l’un des ensembles dans l’autre ensemble.
int inserer(struct fiche elt,
int *p, struct fiche e[])
{
e[(*p)++]=elt;
}
int union_ensemble (int nbeltA, int nbeltB, struct fiche eA[], struct fiche
eB[])
{
int i;
for (i=0; i<nbeltA; i++)
if (! appartient(eA[i], nbeltB, eB)
inserer(eA[i], &nbeltB, eB);
}
Intersection
L’intersection de deux ensembles nécessite de créer un nouvel ensemble :
int inter_ensemble(int nbeltA, int nbeltB, int *nbelC, struct fiche eA[], struct
fiche eB[], struct fiche eC[])
{
int i;
for (i=0; i<nbelementA; i++)
if (appartient(eA[i], nbeltB, eB))
inserer(eA[i], &nbelC, eC);
}
Complémentaire
Le complémentaire de A dans B peut se programmer en calculant B-A ; il faut disposer en ce cas
d’une fonction de suppression d’un élément d’un ensemble. L’autre méthode consiste à créer un
nouvel ensemble C ne contenant que les éléments de B n’appartenant pas à A :
int difference_ensemble(int nbeltA, int nbeltB, int *nbelC, struct fiche eA[],
struct fiche eB[], struct fiche eC[])
{
100 200 20
355
L’ordre sur les éléments n’est pas nécessaire pour implanter le type ensemble mais alors la liste ne
doit pas contenir de doublon. Du point de vue informatique, l’ajout d’un nouvel élément nécessite la
création d’une nouvelle cellule, ce qui se réalise de façon dynamique par allocation de mémoire —
fonction malloc() ou calloc()— en cours d’exécution. Par ce procédé la taille de l’ensemble n’est
limitée que par l’espace mémoire disponible ; elle n’a pas à être fixée a priori.
Allocation
Considérons par exemple un ensemble de nombres entiers. Une cellule sera définie par :
Appartient
Il nous faut écrire une fonction int appartient(int x , listel )
qui renvoie VRAI si la valeur x appartient à la liste l , FAUX sinon.
Vérifier si x appartient à la liste, cela consiste à comparer x avec l’élément en tête de liste, puis à
parcourir la liste en suivant le chaînage, soit jusqu’à trouver l’élément recherché, soit jusqu’à
atteindre la fin de liste. La programmation récursive s’impose.
....
(3) (2)
Tête de liste
x
nouvelle_cellule
(1)
Insertion en tête : (1) créer une cellule ; (2) chaîner avec la première cellule de la liste ; (3) faire
pointer la tête de liste vers la nouvelle cellule.
Si l’ordre des éléments doit être respecté, l’insertion dans le corps de la liste s’impose. Il faut alors
repérer la cellule précédant immédiatement la nouvelle cellule à insérer, et modifier les liens comme
indiqué sur le schéma suivant.
(3) (2)
cellule_courante
x
nouvelle_cellule
(1)
Suppression
d'une cellule
(1)
(2)
cellule_courante
x
cellule_supprimée
(3)
Complexité
La complexité de ces opérations est en O(n*m), avec n (respectivement m) éléments dans A
(respectivement B).
Le programme complet suivant génére deux ensembles d’entiers tirés au hasard, et en calcule
l’union, l’intersection et la différence.
#include <stdio.h> return (appartient(x, l-
#include <stdlib.h> >suiv));
}
#define VRAI 1
#define FAUX 0
liste *position_element(int x, liste
*l)
typedef struct cellule { {
int elt; if (l==NULL)
struct cellule *suiv ; } liste; return (NULL);
else if (l->elt==x)
return (l);
else
liste * alloc_cellule (int x ) ; return (position_element(x, l-
int appartient (int x, liste *l); >suiv));
liste *position_element(int x, liste }
*l);
liste *insere_en_tete(int x, liste *l
); liste *insere_en_tete(int x , liste
*l )
liste *insere_liste(int x, liste *l {
); liste *c;
liste *union_liste(liste *a, liste c = (liste *) calloc(1,
*b); sizeof(liste));
liste *inter_liste(liste *a, liste if (c!=NULL)
*b); {
liste *difference_liste(liste *a, c->elt=x; /* assignation de x
liste *b); */;
int affiche_liste(liste *a); c->suiv=l; /* assignation de
suivant */
l=c; /* chainage */
liste * alloc_cellule (int x ) }
{ return (l);
liste *c; }
c = (liste *) calloc(1,
sizeof(liste)); liste *insere_liste(int x , liste *l
if (c!=NULL) )
{ /* verifie si x appartient a l avant
c->elt=x; /* assignation de x d'insérer */
*/ {
c->suiv = NULL; if (! appartient(x, l))
} l=insere_en_tete(x, l);
return (c); return (l);
} }
• Pile et file
Si l’ordre d’insertion et de suppression des éléments dans la liste importe, deux structures de
données sont particulièrement adaptées : la pile et la file.
Pile [stack, lifo]
Une pile est une liste qui respecte la régle “dernier arrivé, premier sorti”, alors que la file respecte
la régle “premier arrivé, premier sorti”.
La pile est une structure de données pour laquelle l’ajout et la suppression d’un élément ne sont
autorisés qu’à une seule extrémité, appelée sommet de la pile. Les opérations sur les listes sont —P
est une pile, x un élément— :
Vider(P) [clear()] P ne contient plus aucun élément.
Vide(P) VRAI ssi P est vide.
Premier(P) retourne le premier élément de P.
Dépiler(P) [pop()] retourne le premier élément de P et supprime celui-ci de P.
Empiler(x, P) [push()] place x dans P comme premier élément.
Opérations sur une pile
x sommet
sommet
dépiler
... ...
2 2
empiler
1 1
0 0
Il est assez facile de réaliser l’implantation d’une pile à l’aide d’un tableau. Il faut seulement définir
une variable auxiliaire, le pointeur de pile, qui indique l’adresse du sommet de la pile (qui est aussi le
nombre d’éléments de celle-ci).
printf("\n%d éléments\n",pp);
/* vider la pile */
while(! pile_vide())
printf("%3.2f ",pop());
printf("\n");
On dispose de deux index, tête et queue, et d’un tableau de taille MAXELEMENT, dont les adresses
sont désignées modulo MAXELEMENT pour obtenir une structure de données circulaire...
File Sens de la file qui se vide par la
0 1 2 3 tête
.... MAXELT-1
x x x x
File circulaire
/* File circulaire de flottants */ }
#include <stdio.h>
#include <stdlib.h> int file_pleine(void)
{
#define MAXELEMENT 20 return ((qf+1) % MAXELEMENT == tf);
typedef float typefile; }
void clear(void)
{
tf=0;
qf=0;
}
int file_vide(void)
{
return (tf==qf);
Jean Fruitet - IUT de Marne La Vallée - 89
Introduction à la programmation
typefile dernier_file(void) qf=(qf+1) % MAXELEMENT;
{ }
if (qf>0)
return(file[qf-1]); /* programme principal */
else void main (void)
return(file[MAXELEMENT-1]); {
} int i, j, n;
L’implantation d’une pile ou d’une file sous forme de liste chaînée ne présente pas de difficulté
particulière.
• Hachage
Vecteur booléen
Soit E un sous-ensemble de l’ensemble d’entiers {0, 1, 2,...,N}, par exemple E = {2, 7, 9}
Pour représenter l’ensemble E, on utilise une fonction caractéristique F:
F :E --> {0, 1}
x --> F(x)==0 si x n’appartient pas à E
x --> F(x)==1 si x appartient à E
0 0 1 0 0 0 0 1 0 1 0 ... 0
0 1 2 3 4 5 6 7 8 9 10 ... N
0 0 1 0 0 0 0 1 0 1 0 ... 0
0 1 2 3 4 5 6 7 8 9 10 ... N
0010000101 F
collecte
E’={ 2, 7, 9}
Après ajout de Paul et Noe, les cases 2, 3 et 6 sont occupées par des éléments ayant une valeur de
hachage identique.
Pour retrouver Noe, il faut :
- calculer h(Noe) = 2
- comparer T[h(Noe)] et Noe
si identité SUCCES
sinon parcourir le tableau circulairement jusqu’à
- trouver Noe : SUCCES
- trouver une case libre : ECHEC
Hachage ouvert
0 Lou
1
VIDE
2 Anne Noe
3 Paul
4 Luc
5 Guy
6 VIDE
7 VIDE
• Tri lexicographique
On considère un ensemble de mots ; une table indexée par les lettres de l’alphabet et une fonction de
hachage qui à un mot fait correspondre sa iéme lettre.
Donner un algorithme permettant de classer les mots dans l’ordre lexicographique (alphabétique).
Indication : pour classer des mots de même longueur, il suffit de les classer successivement suivant la
dernière lettre, puis avant-dernière lettre, etc.
• Dictionnaire d’un texte
On considère un texte T constitué de mots (chaîne ASCII alphanumérique) séparés par des codes
<ESPACE>, <TAB>, <NL>, <CR>
Programmer en C le dictionnaire du texte par fonction de hachage fermée.
A chaque mot sera associé un triplet
<mot, position de la première occurrence, nombre d’occurrences>
Les mots seront “case sensitive” (MAJUSCULES et minuscules)
Proposer plusieurs fonctions de hachage et afficher la distribution des valeurs de hachage et le
nombre de collisions en fonction de la taille de la table de hachage...
12 -2 10 12 -2 10
Arbre singleton
23 78 22 -77 78 22 -77
9 9 9
12 10 9 23
-2 78 22 -77 -2 10 22 78
9 -77 9
9
sous-arbre gauche sous-arbre droit
Chaque étiquette d'un noeud du sous-arbre gauche
est inférieure ou égale à l'étiquette de la racine qui est
inférieure aux étiquettes du sous-arbre droit... cette
propriété est maintenue dans l'arbre tout entier.
Par définition un arbre est une structure de données constituée d’un noeud appelé racine et de
sous-arbres fils de la racine. C’est donc une définition récursive.
Chaque noeud d’un arbre a un ou zéro pére et zéro ou plusieurs fils. Un noeud sans pére est la
racine de l’arbre. Un noeud sans fils est une feuille. Un noeud ayant un pére et au moins un fils est
un noeud interne. L’étiquette d’un noeud est un élément de l’ensemble représenté par l’arbre. Les
liens entre un noeud et ses sous-arbres fils sont les branches de l’arbre.
La hauteur de l’arbre est le plus long chemin (sans retour en arriére) entre la racine et une feuille.
Un arbre vide n’a aucun noeud. Un singleton a une racine sans fils. Un arbre binaire est un arbre
dont chaque noeud a au plus deux fils. Un arbre binaire de recherche est un arbre binaire dont les
étiquettes sont ordonnées selon une clé et tel que toutes les clés du sous-arbre gauche sont
inférieures ou égales à la clé de la racine, et celles du sous-arbre gauche sont supérieures, et ceci
pour tous les sous-arbres...
La hauteur de l’arbre permet d’estimer le nombre n d’éléments de celui-ci. En particulier si A est un
arbre binaire complet de hauteur h (tous les noeuds ont zéro ou deux fils) il a au plus
20 + 21 + 22 + .. + 2h-1 + 2h éléments
Card(A) Σ i=0,h 2i = 2h+1 -1
12 1 noeuds
9 23 2 noeuds
hauteur :3
10 78 ...
-2 22
12 -2 10 S A
-2 22 -77 Z A T
9 B
Pour éviter d’avoir à gérer un nombre variable de pointeurs par noeud d’un arbre quelconque, on
peut préférer la structure suivante — fils gauche, frére droit—
Arbre quelconque
Pointeur vers un frère
23
12 -2 10
-2 22 -77
Implantation en Langage C
• Opérations
• Vide consiste à tester si la racine est un pointeur NULL :
int vide (arbre racine)
{
return (racine == NULL);
}
• Elt : consiste à retourner l’étiquette d’un noeud :
par exemple pour les arbres quelconques :
type_element element(arbre racine)
{
return (racine->elt);
}
• Nouveau : il faut faire une allocation mémoire et placer l’étiquette. En cas d’erreur d’allocation le
pointeur renvoyé est NULL (l’arbre est vide) :
arbre nouveau_binaire(type_element elt, arbre racine)
{
racine = (struct cellule *) calloc(1, sizeof (struct cellule));
if (! vide(racine))
{
racine->elt = elt;
racine->fils_gauche= NULL;
racine->fils_droit=NULL;
}
return(racine);
}
• Cons : il faut relier un noeud à un ou plusieurs sous arbres.
arbre cons_binaire(arbre racine, arbre ss_arbre_g, arbre ss_arbre_d)
{
racine->fils_gauche = ss_arbre_g;
racine->fils_droit = ss_arbre_d;
return(racine);
}
• Insertion
Les opérations d’insertion, suppression et recherche d’un élément se programment de façon
récursive. Dans le cas d’un arbre binaire de recherche, l’insertion est guidée au cours de la descente
dans l’arbre par la relation d’ordre sur les clés.
Il faut donc disposer d’une fonction de comparaison des clés, qui de façon classsique en langage C
retourne un entier négatif, nul ou positif selon l’ordre sur les clés :
int clef(type_element e)
/* renvoie la valeur de la clé */
{
return (e.cle);
Jean Fruitet - IUT de Marne La Vallée - 99
Introduction à la programmation
}
La complexité de cet algorithme est proportionnelle à la hauteur de l’arbre et donc en O(log2n) pour
un arbre binaire de recherche équilibré.
• Suppression
La suppression d’un élément doit conserver l’arbre binaire de recherche. Il faut donc remplacer
l’étiquette supprimée par l’étiquette la plus à droite du sous-arbre gauche.
E Max FG
FG FD FG FD
Max FG
12
9 23
-2 10 22 78
-77 9
struct liste {char n[MAXCAR]; struct arbre recherche (char *element, arbre
liste * suiv;} *ta[MAXHAUTEUR]; /* racine)
tableau de listes */ {
int test;
FILE *f; if (racine!=NULL)
{
/* PROTOTYPES */ test = strcmp(racine->elt,
element);
arbre insere(char *element, arbre if (test==0)
racine); /* insere un element dans return (racine);
l'arbre binaire */ else if (test<0)
arbre recherche (char *element, arbre return( recherche(element,
racine); /* recherche un element */ racine->fd));
arbre min_arbre(char *element, arbre else
racine); /* supprime le min de return( recherche(element,
l'arbre */ racine->fg));
arbre supprime(char *element, arbre }
racine); /* supprime element de else return (NULL);
l'arbre */ }
void affiche_p(arbre racine); /*
affiche en profondeur infixe */ arbre min_arbre(char *element, arbre
racine)
struct liste * /* supprime la valeur minimum de
insere_queue_liste(struct liste * l, l'arbre et la place dans element */
char n[]); /* ajoute eun element n {
queue de liste */ arbre aux;
void cree_l(int h, arbre racine); if (racine!=NULL)
/* transforme un arbre en {
tableau de listes */ if (racine->fg==NULL)
void affiche_liste(struct liste *l); {
/* affiche le tableau de aux=racine;
listes niveau par niveau */ strcpy(element, racine->elt);
void affiche_l(arbre racine); racine=racine->fd;
/* affiche un arbre en largeur free(aux);
*/ }
else
/* FONCTIONS */ {
racine->fg=min_arbre(element,
arbre insere(char *element, arbre racine->fg);
racine) }
/* insere un element dans un arbre */ }
{ return(racine);
if (racine==NULL) /* creer l'arbre }
*/
{ arbre supprime(char *element, arbre
racine = (struct cellule *) racine)
calloc(1, sizeof(struct cellule)); /* supprime element de l'arbre */
if (racine != NULL) {
{ int test;
strcpy(racine->elt, element); arbre aux;
Jean Fruitet - IUT de Marne La Vallée - 102
Introduction à la programmation
aux = racine; else
if (racine!=NULL) {
{ strcpy(l->n, n);
test = strcmp(racine->elt, l->suiv=NULL;
element); /* printf("Insertion %s\t",n); */
if (test==0) }
{ }
/* remplacer cet element */ else
if (racine->fd==NULL) {
/* faire monter le sous-arbre aux=l;
gauche */ while (aux->suiv != NULL)
{ aux=aux->suiv;
aux=racine; aux->suiv=(struct liste *)
racine=racine->fg; calloc(1, sizeof(struct liste));
free(aux); if (aux->suiv==NULL)
} perror("Erreur allocation
else if (racine->fg==NULL) memoire \n");
/* faire monter le sous-arbre else
droit */ {
{ strcpy(aux->suiv->n, n);
aux=racine; aux->suiv->suiv=NULL;
racine=racine->fd; /* printf("Adjonction %s\t",n);
free(aux); */
} }
else /* remplacer par min_fd */ }
{ return (l);
racine->fd = }
min_arbre(racine->elt, racine->fd);
} void cree_l(int h, arbre racine)
} {
else /* rechercher sur les fils if (racine!=NULL)
*/ {
if (test<0)
racine->fd=supprime(element, ta[h]=insere_queue_liste(ta[h],raci
racine->fd); ne->elt);
else cree_l(h+1,racine->fg);
racine->fg=supprime(element, cree_l(h+1,racine->fd);
racine->fg); }
} }
return (racine);
} void affiche_liste(struct liste *l)
{
void affiche_p(arbre racine) while (l != NULL)
{ {
if (racine!=NULL) printf("%s\t", l->n);
{ l= l->suiv;
affiche_p(racine->fg); }
printf("%s\t",racine->elt); }
affiche_p(racine->fd);
} void libere_liste(struct liste *l)
} {
struct liste *aux;
while (l != NULL)
/* affichage en largeur */ {
aux=l->suiv;
struct liste * free(l);
insere_queue_liste(struct liste * l, l = aux;
char n[]) }
{ }
struct liste *aux;
if (l==NULL) /* creer la liste */ void libere_l(void)
{ {
l=(struct liste *) calloc(1, int i ;
sizeof(struct liste)); for (i=0; i<MAXHAUTEUR; i++)
if (l==NULL) {
perror("Erreur allocation libere_liste(ta[i]);
memoire \n"); ta[i]=NULL;
Jean Fruitet - IUT de Marne La Vallée - 103
Introduction à la programmation
} printf("Etiquette a inserer
} ?\n");
if (scanf("%s",str)!=0)
void affiche_l(arbre racine) {
{ printf("Insertion de %s\n
int i ; ",str);
libere_l(); racine= insere(str,
cree_l(0,racine); racine);
printf("\n"); }
for (i=0; i<MAXHAUTEUR; i++) break;
if (ta[i]!=NULL) case 's':
{ case 'S' :
printf("%d\t",i); rewind(stdin);
affiche_liste(ta[i]); str[0]=0;
printf("\n"); printf("Etiquette a
} supprimer ?\n");
} if (scanf("%s",str)!=0)
{
/* MAIN */ printf("Suppression de
%s\n ",str);
racine= supprime(str,
void main (int argc, char *argv[]) racine);
{ }
int i, c; break;
char str[20]; case 'a' :
struct cellule *racine; case 'A' :
i=0; printf("Affichage en
profondeur \n");
affiche_p(racine);
if (argc>1) printf("\n");
{ fflush(stdout);
if ((f=fopen(argv[1], "r"))!=NULL) break;
{ case 'l' :
printf("Ouverture en lecture de case 'L' :
%s\n",argv[1]); printf("Affichage en largeur
while ((c=getc(f))!=EOF) \n");
{ printf("Niveau\tValeurs
if ((c==' ') || (c=='\n') || \n");
(c=='\t')) affiche_l(racine);
{ fflush(stdout);
str[i]=0; break;
i=0; default : break;
printf("%s\n",str); }
racine=insere(str, racine); } while ((c!='q') && (c!='Q') );
}
else }
str[i++]=c;
}
fclose(f);
}
}
do
{
printf("MENU : i:inserer s:supprimer
a:arbre l:liste q:quitte\n");
do {
c=getchar();
} while ((c=='\n') || (c=='\t') ||
(c==' '));
/* boucler sans rien faire */
switch (c) {
case 'i':
case 'I' :
rewind(stdin);
str[0]=0;
Jean Fruitet - IUT de Marne La Vallée - 104
Introduction à la programmation
Graphes
Les graphes interviennent chaque fois qu'on veut représenter et étudier un ensemble de liaisons
(orientées ou non) entre les éléments d'un ensemble fini d'objets.
Par exemple représenter un réseau routier, électrique, un circuit électronique, un réseau
informatique, l'ordonnancement de tâches, une application hyper-média, etc.
Après des définitions et notions fondamentales sur les graphes, nous présenterons quelques
algorithmes élémentaires.
Définition
Un graphe G est un couple (S,A) où :
- S est un ensemble fini de sommets ;
- A est un sous-ensemble de SxS, ensemble des arcs de G.
On prendra généralement pour S un segment [1,n].
Exemple
Représentation du graphe orienté
G1 = (S1,A1) = ({1,2,3,4,5}, {(1,2), (1,3), (2,2), (2,4), (3,1), (4,2), (4,4), (5,4)})
2 4
1 3
5
Un arc (x,y) représente une liaison orientée entre l'origine x et l'extrémité y. Si (x,y) est un arc, x est
le prédécesseur de y et y est le successeur de x ; si x=y l'arc est une boucle.
Graphe non orienté
On dit qu'un graphe G = (S,A) est non orienté si et seulement si, pour tout si et sj de l'ensemble des
sommets S, si (si, sj) est un arc de A, alors (sj, si) aussi. Les arcs s'appellent alors des arêtes et sont
représentées par des paires {si, sj}, non orientées bien sûr.
Exemple
Le graphe non orienté G2 = (S2, A2), représenté graphiquement par :
S2 = {1,2,3,4,5};
A2 = {(1,5), (2,3), (2,4), (3,2), (3,5), (4,2), (4,5), (5,1), (5,3), (5,4)}
1 5 2
Propriétés
Deux sommets x et y d'un graphe orienté (respectivement non orienté) sont adjacents s'ils sont les
extrémités d'un arc (respectivement d'une arête).
Jean Fruitet - IUT de Marne La Vallée - 105
Introduction à la programmation
Soit un graphe G=(S,A) et T un sous-ensemble de S. L'ensemble des arcs de A dont une extrémité
est dans T et l'autre est dans S-T est appelé le cocycle associé à T. L'ensemble des sommets de S-T
adjacents à au moins un sommet de T est la bordure de T.
Le graphe G=(S,A) est dit biparti s'il existe un sous-ensemble de sommets T tel que A= cocycle(T).
Un sous-graphe du graphe G = (S,A) est un couple G' = (S',A') pour lequel S' est inclus dans S, et A'
inclus dans A. Le sous-graphe G' est un graphe partiel de G si S'=S. Si A' est l'ensemble des arcs de
A dont les deux extrémités sont dans S', le sous-graphe G' est dit induit par S'.
Exemple :
G3 = (S3, A3) = ({1,2,3,4,5}, {(1,4), (1,5), (2,4), (3,5), (4,3), (5,2)})
(a) (b)
1 4 1 4
2 2
5 3 3
(c) (d)
1 4 4
1
3 3
M3 1 2 3 4 5 3 1 2 3 4 5 6
1 0 0 0 1 1 1 1 0 0 1 0 0
2 0 0 0 1 0 2 0 0 0 0 1 -1
3 0 0 0 0 1 3 0 -1 1 0 0 0
4 0 0 1 0 0 4 -1 1 0 0 -1 0
5 0 1 0 0 0 5 0 0 -1 -1 0 1
1 4
1
5
4 2
2
6
3
5 3
Exemple :
1 2 3 4 5
1 4
2
5 4 5 3 2
5 3
Dans le cas de graphes non orientés, l'équivalent d'un chemin est une chaîne et celui d'un circuit est
un cycle. Un graphe non orienté est connexe si pour tout couple de sommets il existe une chaîne
ayant ces deux sommets pour extrémités. Autrement dit on peut aller de tout sommet du graphe à
tout autre... Par extension un graphe orienté est connexe si sa version non orientée (obtenue en
supprimant les orientations et les boucles) est connexe. La connexité est une relation d'équivalence
entre les sommets du graphe et ses classes d'équivalences sont appelées composantes connexes du
graphe.
Les méthodes de test d'existence d'un chemin entre deux sommets sont fondées sur le lemme de
König qui assure que s'il existe un chemin entre deux sommets d'un graphe, il en existe alors un de
longueur inférieure ou égale à N, nombre de sommets du graphe. Autrement dit il existe un chemin
élémentaire...
2
4
1
5
Arcs initiaux
Arc créés par transitivité
Pour calculer la fermeture transitive d'un graphe, nous allons définir d'abord deux opérations sur
les matrices d'adjacence à valeurs booléennes :
Soient deux matrices carrées M=(mi,j) et N=(ni,j) d'ordre N à valeurs booléennes {0,1} ; on définit
leur somme S et leur produit P par les formules :
si,j = MIN (1, mi,j+ ni,j)
pi,j = MIN (1, (m i,k x nk,j)1kn )
Pour calculer la fermeture transitive d'un graphe G de N sommets associé à une matrice M, on
définit une suite de matrices d'ordre N, à valeurs dans {0,1} définie par :
M1 = M
Mp = M x Mp-1
On montre qu'il existe un chemin de longueur k entre deux sommets i et j de G si et seulement si on
a Mk(i,j)=1.
Cette remarque combinée avec le lemme de König fournit un algorithme : la somme booléenne des n
matrices M1, M2 ... Mn n'est autre que la matrice associée à la fermeture transitive de G.
Exercice 1
Ecrire un programme qui posséde les fonctionnalités suivantes :
- Saisie de la matrice d'adjacence associée à un graphe G.
- Affichage de la matrice de la fermeture transitive de G.
- Réponse à la question : “Existe-t'il un chemin de longueur k entre les sommets i et j ?”.
Tris
La recherche d’un élément dans un ensemble est bien améliorée si les éléments sont ordonnés.
C’est en particulier le cas des recherches de mots dans un dictionnaire. Ordonner les élément
consiste à effectuer une permutation entre les éléments de sorte qu’après celle-ci les éléments soient
classés —triés— en croissant (respectivement décroissant)...
Données :
N, nombre fini des éléments.
Les éléments sont tous du même type et occupent un espace mémoire fini.
Les données sont par exemple représentées dans un tableau.
Pour effectuer le réarrangement, il faut disposer d’une relation d’ordre total sur les éléments.
Relation d’ordre
Une relation d’ordre notée R sur les éléments d’un ensemble E est une relation binaire ayant les
propriétés :
Réflexivité : aRa
Transitivité : si aRb et si bRc alors aRc
Antisymétrie : si aRb et si bRa alors a=b.
En général une relation d’ordre est notée <. Elle est d’ordre total si tous les éléments de l’ensemble
sont comparables deux à deux.
Pour les données complexes, la relation d’ordre sera définie sur une clé, c’est-à-dire sur une liste
d’attributs qui identifient de façon unique chaque élément de l’ensemble.
La droite euclidienne D
M2 IR
u
O x2
M1
x1
La figure montre que la relation d’ordre des projections sur une seule droite —par exemple OX
(droite orientée par u)— ne suffit pas à ordonner sans ambiguité les points M1, M2 et M3.
Par contre en combinant la relation d’ordre sur les projections sur OX avec une relation d’ordre sur
les projections sur OY, on peut induire un ordre total sur P.
Selon OX : M1 < M3 < M2
Le plan euclidien X
M2
y3 IR
y2 u x2
M3 O
v
x1=x3
y1
M1 Y
IR
Selon OY : M3 < M2 < M1
Pour ordonner le Plan euclidien il faut définir un ordre
total : par exemple
ordre selon OX, et si les abscisses sont égales, ordre
selon OY.
En ce cas M3 < M1 < M2
Fiche de recensement
Un recensement porte sur une population. Chaque élément est représenté par une fiche : (Numéro,
Nom, Prénom, Date de naissance, Adresse, Sexe).
Plusieurs ordres sont possibles, selon le numéro, selon le nom et le prénom, etc.
Il importe de définir précisément quelle sera la clé puis on induira l’ordre sur les objets à partir de
l’ordre sur les clés.
Programme C :
Complexité de l’algorithme.
La recherche du minimum d’une liste non triée de N éléments nécessite de parcourir toute la liste et
N-1 comparaisons. La longueur de la liste diminue à chaque itération. Donc le nombre de
comparaisons est donné par :
(N-1) + (N-2) + .... + 1, soit N (N-1) / 2 comparaisons.
L’algorithme naïf est en O(N2) dans tous les cas. S’il y a 1000 éléments, cela fait un million de
comparaisons !
Cette complexité peut être améliorée si la structure de données utilisée permet une recherche
accélérée de l’élément minimum. C’est le cas du tri maximier ou tri par tas.
Principe :
On opére en deux étapes :
1. On fabrique un tas [heap], c’est-à-dire un arbre binaire partiellement ordonné dit arbre “maximier”
qui peut être représenté par un tableau tel que a[i] a[2i] et a[i] a[2i+1] pour 1iN. Si on
représente les éléments 2i et 2i+1 comme les fils du noeud i, alors a est un arbre binaire équilibré
pour lequel la clé d’un noeud n’est jamais dépassée par les clés de ses descendants. Pour cela on
tamise successivement a[N/2], ..., a[1].
Jean Fruitet - IUT de Marne La Vallée - 114
Introduction la programmation
2. Puis on extrait successivement le maximum de l’arbre, qui par construction se trouve en a[1] en
l’échangeant avec a[M] pour M = N, N-1, ..., 1. Et comme cette opération a supprimé la propriété
de tas, on la rétablit en tamisant le nouveau a[1].
Arbre maximier
a[i] • a[2i] et a[i] • a[2i+1]
1 99 1 99
60 2
2 60 3 59 3 59
40 4
4 40 5 50 6 29 7 39
5 50
29 6
8 10 9 20 10 20 11 30 12 19 13 9 14 39 15 19
7 39
10 8
9 20
20 10
11 30
Le tamisage
Le tamisage de a[k] consiste à échanger itérativement a[k] avec le maximum de a[2k] et de a[2k+1]
jusqu’à ce que la condition de tas soit satisfaite.
Exemple :
Tamisage Heap sort Liste triée
1 24 24 81 81 5 45 45 2 24 2 5 2 2
2 5 45 45 45 45 5 5 5 5 5 2 5 5
3 81 81 24 24 24 24 24 24 2 24 24 24 24
4 2 2 2 2 2 2 2 45 45 45 45 45 45
5 45 5 5 5 81 81 81 81 81 81 81 81 81
Programme C :
for (k= (N/2)-1; k>=0; k- -)
void tamiser(int k; int M) tamiser(k, N);
{ for (k=N-1; k>0; k- -)
int j, v; {
v = a[k]; echange(&a[0], &a[k]);
while (k<= M/2) tamiser(0,k);
{ }
j = 2*k; }
if ((j<M) && (a[j+1] > a[j]))
j++; Complexité :
if (v>=a[j]) O(n log n).
break;
a[k] = a[j]; /* Tri du TAS ou tri maximier [Heap
k = j; sort] */
} /* JF */
a[k] = v; #include <stdio.h>
} #include <stdlib.h>
Tri à bulle
Le tri à bulle (buble sort) consiste à comparer les éléments consécutifs et à les échanger si l'ordre
recherché est violé (les bulles légéres remontent vers le haut).
Le tri peut se faire sur place, et la fin du tri survient quand plus aucun échange n’a eu lieu au cours
d’une traversée de la liste.
Principe :
Faire venir en a[i] l’élément minimum de a[i], a[i+1], ..., a[N-1] par échange d’éléments adjacents.
Exemple :
0 5 2 2 2 2 1 1
1 2 5 1 1 1 2 2
2 1 1 5 3 3 3 3
3 3 3 3 5 4 4 4
4 4 4 4 4 5 5 5
Programme C :
Jean Fruitet - IUT de Marne La Vallée - 116
Introduction la programmation
for (i=0; i<N-1; i++)
for(j=N-1; j>i; j--)
{
if (a[j]<a[j-1])
echange(&a[j-1],&a[j]);
}
Exercice
- Ecrire un algorithme de tri à bulle amélioré en détectant la fin du tri (aucun échange n'a eu lieu lors
de la dernière passe) et en ne repassant pas sur les valeurs qui sont déjà ordonnées.
- Quelle est sa complexité dans le meilleur des cas, dans le pire des cas, et en moyenne ?
Principe : A la i-éme étape, insérer a[i] parmi a[1], a[2], ..., a[i-1] déjà triés.
Exemple :
Indice
1 2 3 4
0 5 2 1 1 1
1 2 5 2 2 2
2 1 1 5 3 3
3 3 3 3 5 4
4 4 4 4 4 5
Programme C:
void tri-insertion(typelement a[], int N)
{
typelement v; /* une variable auxiliaire */
int i,j;
for (i=1; i<N; i++)
{
v=a[i];
j=i;
while ((j>0) && (a[j-1]>v))
{
a[j]=a[j-1];
j--;
}
t[j]=v;
}
}
La tête de liste est désignée par un pointeur tête de type liste ; c’est une cellule qui est chaînée aux
cellules suivantes par l'intermédiaire du pointeur suiv.
La dernière cellule n'a pas de successeur. Elle pointe sur NULL.
tête
val val
val
On dira qu'une liste de valeurs <n1, n2, ..., nk > est triée si n1<=n2 <= ...<= nk .
Exercice 5 : Complexité
- Que pensez-vous de ces deux fonctions de tri, du point de vue de la clarté des algorithmes
(comment prouver leur correction) ?
- Du point de vue de la consommation d'espace mémoire ?
- Du point de vue du nombre de comparaisons ?
- Montrer que si deux éléments t[i ] et t[i+n/2k ] ont été échangés au kiéme passage, alors ils restent
triés par la suite.
- Programmer un shell-sort sur un tableau tiré aléatoirement de 1000 valeurs. Comparer avec le tri à
bulle sur le même ensemble de données (nombre de comparaisons et d’échanges).
2) Fusion
Une fonction dont la déclaration est :
liste fusion ( liste n , liste m)
telle que le résultat de l'appel de fusion sur deux listes triées (hypothése essentielle) n = <n1, n2, ...,
nk > et m = <m1, m2, ..., mp > soit une liste triée de longueur k+p contenant tous les éléments de n
et de m (autrement dit une permutation triée de la liste <n1, n2, ..., nk , m1, m2, ..., mp >). Mais
attention, on veut que le nombre de permutations réalisées pour effectuer cette opération soit borné
par k+p.
Ce tri procéde aussi comme le précédent selon le principe "diviser pour résoudre". Il différe
essentiellement par la phase "diviser";
1) Découpage ordonné
- Ecrire une fonction de découpage :
void decoupe_ordre ( liste l, int x , liste i, liste s)
telle que, après appel sur les arguments l = <n1, n2, ..., nk > , x , i, s , la variable i contienne la liste
des éléments qui sont inférieurs ou égaux à x, et la variable s contienne la liste des éléments de l qui
sont strictement supérieurs à x. L'entier x est appelé pivot . Cette fonction opére donc une partition
de l.
2) Quick-sort.
- Ecrire une fonction récursive de tri :
liste tri_quick (liste l )
dont le principe est le suivant : si l'argument est vide, ou n'a qu'un seul élément, on rend l'argument
comme résultat. Sinon, l'argument est de la forme <n, n1, n2, ..., nk > avec (k>=1 ). On utilise la
fonction decoupe_ordre pour partitionner la liste <n1, n2, ..., nk > en utilisant n comme pivot. On
obtient alors deux listes. Appelons les l1 et l2 . Tous les éléments de l1 sont inférieurs ou égaux à n,
et tous ceux de l2 sont supérieurs à n.
On trie ces deux listes au moyen de tri_quick et on obtient deux listes triées l'1 et l'2 . On renvoie
alors la concaténation de l'1 et n .l'2 qui est une liste triée.
Complexité
Les algorithmes de tri à bulle sont en O(n*n). L’algorithme quick-sort en O(n log2(n)).
Bibliographie
Informatique et calcul numérique
Breton Ph., Dufourd G, Heilman E., "Pour comprendre l'informatique",Hachette, 1992
Cohen J.H., Joutel F., Cordier Y., Jech B. "Turbo Pascal, initiation et applications scientifiques",
Ellipses, 1989
Leygnac C., Thomas R. "Applications de l'informatique, études de thèmes en mathématiques,
physique et chimie", Bréal, 1990
Piskounov N., “Calcul différentiel et intégral”, Editions Mir, 2 tomes.
Algorithmique
Aho A., Hopcroft J., Ulman J., “Structures de données et algorithmes” - InterEditions 1987
Beauquier D., Berstel J., Chretienne Ph., “Eléments d’algorithmique” - Masson 1992.
Carrez C. “Des structures aux bases de données” - Dunod 1990.
Crochemore M. “Méthodologie de la programmation et algorithmique” - UMLV 1990.
Perrin D. "Cours d'Informatique DEUG SSM Module M2" - UMLV 1997
Programmation
PATTIS R. E. “Karel the Robot, a gentle intrduction to the art of programming” - John Wilez &
Sons, 1995.
Kerninghan B. , Ritchie D. "Le langage C" - Masson 1984
BORLAND, "Turbo C 2.0 "Manuel de l'utilisateur"
BORLAND, "Turbo C 2.0 "Manuel de référence"
Leblanc G., "Turbo C" - Eyrolles 1988
Charbonnel J., "Langage C - Les finesses d'un langage remarquable" - Armand Colin 1992
Avertissement 3
Démarche 4
Introduction à l’informatique 5
Langage 5
Traitement de l'information 5
Ordinateur 6
Un peu d'histoire 6
Ce qui caractérise un ordinateur 6
Matériel et logiciel 7
Le codage binaire 7
Notion d’algorithme 14
Le langage C. 19
Processus itératifs. 43
1. Rappels mathématiques 43
2. Types de données et algorithmes 43
Fonctions et sous-programmes 44
Tableaux. 50
Déclaration d’un tableau. 51
Tableau de tableaux 51
Assignation à un tableau 52
Calcul matriciel () 53
Opérations sur les matrices 53
Résolution d'un système d'équations (méthode de Cramer) 55
Calcul du déterminant 58
Inversion de matrice 58
Structures de données 76
Structures 76
Exercices 76
Ensembles 79
Définition 79
Opérations sur les ensembles 79
Représentation des ensembles : tableaux, listes, piles, files 79
Arbres et ensembles ordonnés 95
Exemples d’arbres 95
Représentation symbolique des arbres 96
Définition axiomatiques des arbres 97
Représentation informatique 98
Implantation en Langage C 98
Graphes 105
Définition 105
Graphe non orienté 105
Propriétés 105
Implémentation d'un graphe 106
Matrice d'adjacence 106
Jean Fruitet - IUT de Marne La Vallée - 125
Introduction la programmation
Matrice d'incidence 107
Liste des successeurs ou liste d'adjacence 108
Chemins, chaînes, circuits, cycles 108
Fermeture transitive d'un graphe 108
Tris 112
Bibliographie 121