ALGORITHMIQUE ET PROGRAMMATION EN C

74
ALGORITHMIQUE ET PROGRAMMATION EN C Pierre Tellier 17/05/2006 Département d'Informatique de l'Université Louis Pasteur, 7 rue René Descartes, F67084 STRASBOURG CEDEX, e-mail : [email protected], Tel : +33 (0) 3 90 240 300

Transcript of ALGORITHMIQUE ET PROGRAMMATION EN C

Page 1: ALGORITHMIQUE ET PROGRAMMATION EN C

ALGORITHMIQUE ET PROGRAMMATION

EN C

Pierre Tellier∗

17/05/2006

∗ Département d'Informatique de l'Université Louis Pasteur, 7 rue René Descartes, F67084 STRASBOURG CEDEX, e-mail : [email protected], Tel : +33 (0) 3 90 240 300

Page 2: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 2 Pierre Tellier

Table des matières Table des matières ............................................................................................................................................ 2 Premiers programmes ....................................................................................................................................... 4

Objets : types, constantes, variables ............................................................................................................ 4 Objets élémentaires .................................................................................................................................. 4 Objets élémentaires en C ......................................................................................................................... 4 Expressions............................................................................................................................................... 4 Identificateurs............................................................................................................................................ 6 Constantes ................................................................................................................................................ 6 Variables ................................................................................................................................................... 7 Expressions avec des variables et des constantes .................................................................................. 7

Exécution : la séquentialité............................................................................................................................ 7 Séquentialité ............................................................................................................................................. 7 Affectation ................................................................................................................................................. 8 Entrées/sorties : écriture et lecture ........................................................................................................... 9 Premiers programmes ............................................................................................................................ 10 Remarques.............................................................................................................................................. 13

Analyse descendante .................................................................................................................................. 13 Fonctions ......................................................................................................................................................... 15

Fonctions avec paramètres et résultat ........................................................................................................ 15 Fonctions sans résultat : procédures .......................................................................................................... 16 Fonctions et procédures sans paramètres.................................................................................................. 17 Fonctions avec plusieurs résultats .............................................................................................................. 18

Résultat n-uplet ....................................................................................................................................... 18 Les structures en C................................................................................................................................. 18 Paramètres modifiables (passage par adresse) en C ............................................................................ 19

Variables et portée des variables................................................................................................................ 19 Variables locales, variables globales...................................................................................................... 19 Variables statiques et automatiques en C .............................................................................................. 20

Fonctions en paramètre de fonctions.......................................................................................................... 21 Exemples de programmes avec des fonctions ........................................................................................... 22

Volume de l'écrou ................................................................................................................................... 22 Calcul de la durée ................................................................................................................................... 23

Langage C : arguments de main................................................................................................................. 25 Langage C : nombre quelconque de paramètres ....................................................................................... 25

Conditionnelle .................................................................................................................................................. 26 Quelques exemples..................................................................................................................................... 26

La deuxième pizza à moitié prix.............................................................................................................. 26 Équation du second degré ...................................................................................................................... 27

Autres exemples simples ............................................................................................................................ 28 Macros en C ................................................................................................................................................ 29 Condition et opérations Booléennes ........................................................................................................... 30 Quelques exemples portant sur la conditionnelle ....................................................................................... 32

Calcul du temps de parcours .................................................................................................................. 32 Calcul simplifié de l'impôt sur le revenu.................................................................................................. 33

Énumération des cas................................................................................................................................... 35 Syntaxe ................................................................................................................................................... 35 Calcul de la position d'un jour dans l'année : quantième........................................................................ 37

Itération et récursivité ...................................................................................................................................... 40 Itérations et conditions d'arrêt ..................................................................................................................... 40

faire N fois ............................................................................................................................................... 41 Répétition d'un calcul .............................................................................................................................. 44 Itérations et conditions d'arrêt................................................................................................................. 46 Itérations et compteurs ........................................................................................................................... 48

Récursivité................................................................................................................................................... 50 Cas naturels ............................................................................................................................................ 51 Compteurs............................................................................................................................................... 51 Moyenne d'une série de nombres........................................................................................................... 52 Répétition d'un calcul .............................................................................................................................. 53

Page 3: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 3 Pierre Tellier

Itération et récursivité .................................................................................................................................. 54 Exemples simples ................................................................................................................................... 54 Calcul de l'intégrale par la somme de Riemann ..................................................................................... 58 Tableau d'amortissement du remboursement d'un emprunt .................................................................. 60 Calcul du jour dans l'année : quantième................................................................................................. 62

Récursivité croisée ...................................................................................................................................... 62 Tableaux statiques 1D..................................................................................................................................... 63

Opérations élémentaires sur les tableaux .............................................................................................. 64 Application : les ensembles .................................................................................................................... 70 Tri de tableaux ........................................................................................................................................ 72 Tableaux à trous ..................................................................................................................................... 74

Page 4: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 4 Pierre Tellier

Premiers programmes

Objets : types, constantes, variables Objets élémentaires

• Nombres entiers (relatifs) : on les note sous la forme habituelle, soit une suite de chiffres : 26, -172. • Nombres réels : Leur notation est elle aussi classique, identique à celle qu'on utilise pour les machines

à calculer. Le séparateur décimal est le point. On peut également écrire les réels en notation scientifique : 2.7, 3.14, 2.0, -3.4 10-3.

• Caractères : On les note entre apostrophes comme par exemple 'A' (caractère A), 'a' (caractère a), '0' (caractère 0), ',' (caractère virgule) etc. Les caractères spéciaux peuvent être désignés directement par leur symbole : ENTER (retour chariot), TAB (tabulation), BEEP (son) etc.

• Chaînes de caractères : Ce sont des suites de caractères délimitées par des guillemets. On procède comme cela dans la plupart des langages. Exemples : "oui", "bonjour", "ceci", "ceci est une chaîne".

• Booléens : Ce sont des objets particuliers qui peuvent prendre uniquement 2 valeurs : VRAI ou FAUX.

Objets élémentaires en C

• Nombres entiers (relatifs) : En langage C, on a en plus la possibilité de noter les entiers sous forme hexadécimale (base 16) : 0x12 vaut 18 (1*16+2), 0x2A vaut 42 (2*16+10), ou octale (base 8) : 012 vaut 10 (1*8+2).

• Nombres réels : En langage C on utilise aussi la notation classique. Exemples : 12.38, 0.15, .15, 2.0, 2., -3.17, 1.e3, 0.1e-2, 123.14E19.

• Caractères : En C les caractères sont aussi notés entre apostrophes : 'a', 'Z', '?' etc. Les caractères spéciaux peuvent être désignés directement par leur symbole : ('\n' (retour chariot), '\t' (tabulation) etc.), ou par leur code ASCII, '\007' en octal ou '\x07' en hexadécimal (caractère de code ASCII 7 = bip), '7' (caractère 7). En fait, au niveau de la machine (et du langage C), chaque caractère correspond à un code (ASCII), ainsi 'A' correspond au code ASCII 65, 'a' au code ASCII 97, '0' au code ASCII 48, ',' au code ASCII 44, '\n' au code ASCII 10 etc.

• Chaînes de caractères : Exemples : "oui", "bonjour", "ceci", "ceci est une chaîne". • Booléens : Attention, de tels objets n'existent pas explicitement en C, mais existent dans de nombreux

autres langages de programmation. En principe, on attribue la valeur entière 0 pour FAUX et 1 pour VRAI.

Expressions Le but étant de réaliser des calculs qui font intervenir les objets décrits précédemment, nous allons les combiner pour créer des expressions à l'aide d'opérateurs. Là encore, la notation est proche de la notation usuelle des mathématiques ou des calculettes.

Opérateurs

• arithmétiques addition +, soustraction -, moins (unaire) -, multiplication *, division /, division entière div, modulo (reste de la division entière) mod.

• Booléens ET, OU, NON. • de comparaison = (égal), <> (différent), <, >, <=, >=.

Opérateurs en langage C

• arithmétiques +, - (unaire et binaire), *, / (entière, réelle), % (modulo), incrémentation ++ (augmente la valeur de 1), décrémentation -- (diminue la valeur de 1), accumulations : -=, +=, /=, *= (des explications sur ces notations seront fournies plus loin).

• Booléens && (ET), || (OU), ! (NON)

Page 5: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 5 Pierre Tellier

• de comparaison == (égal), != (différent) , <, >, <=, >=. Ces opérateurs ne s'appliquent pas aux chaînes de caractères en C.

• sur les bits | (OU bit à bit), & (ET bit à bit), << (décalage à gauche), >> (à droite), ^ (ou exclusif bit à bit), ~ (inversion = NON bit à bit).

Expressions arithmétiques La formulation des expressions arithmétiques est elle aussi similaire à la notation mathématique : • 2 + 3 • 17 * 73 + 2 • 7 mod 2 • 7 div 2 (vaut 3 : division entière) • 7 / 2 (vaut 3.5 : division réelle) • 0.3 * 168.2 + (4. + 0.11)/5.

Expressions arithmétiques en langage C

• 2 + 3

• 17 * 73 + 2

• 7 % 2

• 7 / 2 (vaut 3 : division entière)

• 7. / 2. (vaut 3.5 : division réelle)

• (0.3 * 168.2)+((4. + 0.11)/5.) • 31 >> 4 (vaut 1 : on supprime les 4 derniers bits) Attention aux mélanges de types dans les expressions (certains langages comme le Caml les ont tout simplement interdits), et aux parenthèses. On peut mélanger les types, il existe des règles de conversion par défaut. On peut aussi ne pas mettre de parenthèses et dans ce cas des règles de priorité sont appliquées. Dans le doute, il vaut mieux mettre toutes les parenthèses nécessaires et convertir toutes les valeurs dans le même type.

Expressions Booléennes

• 3<2 vaut FAUX. • (3<2) ET (6=5). Comme (3<2) vaut FAUX, l'expression (FAUX ET quelque chose) donnera forcément

FAUX. • (3>2) OU (6=5). Comme (3>2) vaut VRAI, l'expression (VRAI OU quelque chose) donnera forcément

VRAI.

Expressions Booléennes en C : ordre d'évaluation En C, l'évaluation d'une expression n'est effectuée que si elle est nécessaire à l'obtention du résultat. • 3<2. L'expression est fausse et vaut 0.

• (3<2)&&(6==5). Comme (3<2) est FAUX, l'expression (FAUX ET quelque chose) donnera forcément FAUX. L'expression (6==5) n'est pas évaluée.

• (3>2)||(6==5). Comme (3>2) est VRAI, l'expression (VRAI OU quelque chose) donnera forcément VRAI. L'expression (6==5) n'est pas évaluée.

Expressions arithmético-Booléennes en langage C Ceci est propre au langage C, qui ne possède pas de véritable type Booléen. Une expression Booléenne valant VRAI prend la valeur 1 dans une expression arithmétique, et 0 si elle vaut FAUX. Une expression arithmétique qui vaut 0 est considérée comme valant FAUX dans une expression Booléenne, alors qu'elle est considérée à VRAI pour toute autre valeur. • (3<4)*(1+((2+3)==5)) vaut (1)*(1+(1)) soit 2.

• (3||(4<3)4)) vaut 3||0 donc VRAI OU FAUX , donc VRAI soit 1.

Page 6: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 6 Pierre Tellier

Les types en C

• Entiers On peut décrire les types entiers du langage C à l'aide de la règle suivante : [signed | unsigned] [short | long] [int] En pratique, les types entiers couramment utilisés sont : int : entiers signés sur 4 octets (de -231 à 231-1). unsigned int : entiers positifs sur 4 octets (de 0 à 232-1). short : entiers signés sur 2 octets (de -215 à 215-1). unsigned short : entiers positifs sur 2 octets (de 0 à 216-1).

On utilise aussi parfois le type char pour coder les petits entiers : char : entiers signés sur 1 octet (de -27 à 27-1). unsigned char : entiers positifs sur 1 octet (de 0 à 28-1).

• Booléen Le type Booléen n'existe pas en C, en tout cas pas explicitement, mais peut être obtenu à l'aide des entiers : en effet, une expression Booléenne vaut 0 si elle vaut FAUX, 1 si elle vaut VRAI, et le langage C interprète la valeur 0 comme valant FAUX, et toute autre valeur comme valant VRAI. Ainsi l'expression (3<4) aura la valeur entière 1 car cette inégalité vaut VRAI.

• Réels float : nombres réels sur 4 octets. double (double précision) : nombres réels sur 8 octets.

• Caractère char • Chaîne de caractères char[longueur + 1] où longueur est la longueur des plus grands mots

qu'on veut pouvoir représenter.

Identificateurs Ils permettent de donner des noms à des objets : constantes, variables ou fonctions pour les désigner plus facilement. Ce sont des noms, i.e. une suite "presque" quelconque de caractères, comme fact, pgcd, produit, somme, etc. Les identificateurs ne doivent pas commencer par un chiffre et doivent être choisis de la façon la plus parlante possible. Dans tous les langages, certains identificateurs sont réservés. Ainsi en C il existe quelques identificateurs qui désignent des fonctions prédéfinies, par exemple : printf (affichage), sqrt (racine carrée) etc.

Constantes Ce sont des objets auxquels on donne des noms et dont on ne voudra modifier la valeur sous aucun prétexte. Par exemple, il est courant d'utiliser une constante qu'on peut nommer Pi pour désigner la valeur de π, ce qui évite une écriture fastidieuse de sa valeur décimale à chaque utilisation : constantes

réel Pi vaut 3.1415927; réel coursEuro vaut 6.55957; entier nbCôtésDuCarré vaut 4;

L'utilisation des constantes est primordiale, elle permet de modifier la dimension d'un problème et de son traitement avec un minimum de manipulations. En principe, la simple mise à jour des constantes suffit.

Constantes en langage C En langage C il existe 2 manières de procéder. • Avec const. On peut nommer une constante à l'aide du mot-clé const du langage C que l'on fait suivre

du type de la constante puis de son nom et de la valeur associée, comme le montrent les exemples suivants : const float Pi=3.14159; const float coursEuro=6.55957; const int NbCotesDuCarre=4; Pour les apprentis hackers, on peut faire remarquer qu'il existe des moyens de modifier la valeur des constantes déclarées de cette façon (voir le chapitre sur les pointeurs ...).

• Avec #define. L'autre façon de procéder est différente, puisqu'il ne s'agit pas à proprement parler d'une déclaration de constante, mais d'une définition. Il s'agit d'un mécanisme de réécriture (géré par le pré-processeur C) qui permet de remplacer directement le nom d'une constante par le texte associé sans engendrer la création d'un objet. Par exemple, la définition de la constante π peut se faire ainsi : #define PI 3.14159. Chaque occurrence du mot PI dans le programme sera remplacée par le texte associé 3.14159. Nous verrons par la suite que ce mécanisme de définition peut servir à de nombreuses autres choses, mais que son utilisation est assez délicate pour la mise au point des

Page 7: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 7 Pierre Tellier

programmes. D'autre part, nous recommandons l'usage de majuscules pour toutes les définitions (et uniquement pour les définitions).

Variables

Déclaration Ce sont des objets auxquels on donne des noms (identificateurs). Leur usage est fréquent dans la résolution de problèmes, en particulier pour poser des résultats intermédiaires. Ces objets sont des objets informatiques, chacun d'eux nécessite un emplacement dans la mémoire de l'ordinateur. La déclaration d'une variable permet de réserver dans cette mémoire un emplacement dont la taille est fonction du type. Ce type permet aussi de savoir comment il faudra utiliser l'information contenue dans cet emplacement. C'est pourquoi il est nécessaire de déclarer toute variable avant son utilisation. Une déclaration se termine par le symbole ";". Il est possible de déclarer plusieurs variables du même type en même temps, il suffit de les séparer par une virgule ",". variables

entier nombreEtudiants; entier age, nombreFrèresEtSoeurs, tailleFamille; réel rayon; réel a, b, c;

Les mêmes déclarations en langage C : int nombreEtudiants; int age, nombreFreresEtSoeurs, tailleFamille; float rayon; float a, b, c;

Initialisation Il est possible de donner une valeur (que l'on pourra éventuellement modifier par la suite) à une variable dès sa déclaration. On parle alors d'initialisation, que l'on pratique comme suit : variables

entier nombreEtudiants init 20; entier age, nombreFrèresEtSoeurs init 0, tailleFamille init 3; réel rayon init -1.0, piSur2 init pi/2.0;

Les mêmes déclarations en langage C : int nombreEtudiants = 20; int age, nombreFreresEtSoeurs=0, tailleFamille=3; float rayon=-1.0, piSur2=PI/2.0;

Expressions avec des variables et des constantes Elles s'écrivent exactement de la même manière que les expressions faisant intervenir des valeurs, mais font intervenir un ou plusieurs identificateurs : nombreFrèresEtSoeurs+2+1 2.0 * Pi * rayon Pi * rayon * rayon ou en C nombreFreresEtSoeurs+2+1 2.0 * Pi * rayon PI * rayon * rayon

Exécution : la séquentialité Séquentialité Par la suite, nous étudierons les stratégies qui permettent de résoudre des problèmes à l'aide de programmes. Sans anticiper, nous pouvons dévoiler que la solution passera par le découpage du problème en sous-problèmes suffisamment simples (non décomposables) pour être résolus à l'aide de traitements élémentaires : les instructions, qui seront exécutées en séquence, les unes après les autres. Ces instructions sont séparées par un délimiteur syntaxique, le point-virgule ";" qui indique la fin d'une instruction et donc qu'il est possible de passer à la suivante. Il s'agit de la séquentialité.

Page 8: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 8 Pierre Tellier

Cette syntaxe est retenue par de nombreux langages, en particulier le C. Certains se contentent de la fin de ligne, d'autres au contraire exigent deux points-virgules consécutifs. Il existe en C un autre séparateur d'instructions qui est la virgule (","). Il s'agit de l'opérateur de colatéralité : deux instructions séparées par une virgule peuvent être exécutées dans un ordre quelconque et pourquoi pas en même temps si cela est possible.

Affectation Pour utiliser une variable dans une expression, il est nécessaire de lui donner une valeur. Cette opération s'appelle l'affectation, qui consiste à ranger une valeur (le résultat d'une expression) dans l'emplacement mémoire occupé par une variable. Nous utiliserons, comme en langage Pascal, le symbole := pour décrire cette opération. rayon := 5.0; nombreFrèresEtSoeurs := 4; tailleFamille:=nombreFrèresEtSoeurs+1+2; Vous aurez compris que l'initialisation n'est qu'un raccourci qui permet de regrouper en une seule ligne déclaration et première affectation.

Affectation en C En C, c'est le symbole "=" qui permet cette opération. Attention à ne pas se mélanger avec l'opérateur de comparaison "==" ! rayon = 5.0; nombreFreresEtSoeurs=4; tailleFamille=nombreFreresEtSoeurs+1+2; a=b=1.0; En C, toute affectation a une valeur, la valeur affectée. Ainsi l'expression b=1.0; vaut 1.0, et peut être affectée à a.

Notion de variable informatique Comme nous l'avons dit, une variable informatique est conditionnée par l'emplacement mémoire qu'elle occupe. L'affectation a pour effet de modifier le contenu de cette mémoire. En informatique, il est possible d'écrire des expressions comme x := x*2; qui signifie : ranger dans la variable x son ancien contenu multiplié par 2. En fait une affectation se passe en 2 temps. D'abord l'expression en partie droite est évaluée, ce qui suppose que toutes les variables ont une valeur connue, et seulement ensuite le résultat de cette expression est affecté à la variable en partie gauche. Il n'est donc pas choquant ni faux de trouver en partie droite la variable qui se trouve aussi en partie gauche. La différence est qu'en partie droite on utilise l'ancienne valeur, le résultat de l'expression devient alors la nouvelle valeur. En C on peut aussi écrire de telles expressions, des opérateurs spéciaux ont même été prévus pour les accumulations : on peut remplacer toute expression x = x op v; où op est +, -, * ou / par x op= v;. Par exemple, x = 2*x; peut s'écrire x *= 2;. Le langage C est même allé plus loin, en offrant des opérateurs d'incrémentation et de décrémentation : i=i+1; peut s'écrire i+=1; ou bien i++; ou encore ++i;. Si l'incrémentation (ou la décrémentation) apparaît dans une expression, utiliser i++; provoque l'incrémentation après que la valeur de i ait été utilisée, alors qu'utiliser ++i; la provoque avant, comme le montre l'exemple suivant : int i,j,k,l; i = 3; j = 2*i++; k = 2*i; l = 2*++i; Après exécution, j vaudra 6 (la valeur de i utilisée est 3, puis i passe à 4), k vaut 8 (i vaut 4 puisqu'il a été incrémenté). Enfin, l vaut 10 car i est d'abord incrémenté avant d'être multiplié par 2. L'opérateur de décrémentation (--) fonctionne de la même façon. La raison de l'existence de ces opérateurs est historique : les anciens compilateurs, peu performants, n'étaient pas capables de détecter une accumulation, et donc de la traduire en utilisant au mieux le jeu d'instructions du processeur cible. Ce n'est bien sûr plus le cas aujourd'hui, néanmoins on trouve encore abondamment ce genre d'écriture, même dans les programmes écrits de nos jours. Échanger le contenu de 2 variables montre bien la connotation physique de la notion de variable informatique. Pour bien comprendre, pensez à 2 verres d'eau dont on veut échanger les contenus. Il faut passer par un troisième verre. Il en est de même pour les variables :

Page 9: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 9 Pierre Tellier

variables entier a init 1, b init 2; entier tmp;

début tmp := a; a := b; b := tmp;

fin A la fin de cette exécution, a vaudra 2, et b vaudra 1. Le même programme, cette fois en langage C : int a = 1, b = 2, tmp; tmp = a; a = b; b = tmp;

Entrées/sorties : écriture et lecture Les variables sont destinées à contenir des valeurs elles mêmes réutilisables dans d'autres expressions. Mais ceci n'a d'intérêt que si à un moment ou un autre il est possible de connaître ces valeurs, ou de donner de manière interactive une valeur à une variable, car l'intérêt des programmes est de pouvoir exécuter un même traitement sur des données différentes. Cela est rendu possible grâce à l'usage des variables. Néanmoins, il est fastidieux et peu interactif de devoir corriger un programme à chaque fois que l'on désire modifier la valeur d'une variable. On dispose pour remédier à cela de mécanismes permettant la modification des variables grâce à la lecture de valeurs au clavier, et permettant l'affichage de leur valeur à l'écran. Ceci est réalisé par les fonctions d'entrée sortie : variable:=lire() et afficher(expression). La fonction lire attend la saisie (au clavier) d'une valeur, et une fois que celle-ci est tapée, puis suivie de la touche ENTER, la valeur est transmise à la variable. Au contraire, la fonction affiche provoque un affichage à l'écran de la valeur de la variable. On pourra utiliser cette dernière avec plusieurs données en même temps, l'affichage des différentes données sera alors fait les unes à la suite des autres. Comme exemple, on cherche à réaliser un programme qui fait la somme de 2 nombres entiers (ce que fait très bien une calculatrice). Pour cela on devra entrer 2 valeurs, et on souhaite, après la saisie de la deuxième, que le résultat s'affiche. variables entier x, y, z; x := lire(); y := lire(); z := x+y; afficher(z); Remarque. On aurait pu se passer de la variable z, et écrire à la place : variables entier x, y; x := lire(); y := lire(); afficher(x+y); Ce programme peut fonctionner, mais il est peu bavard et ne renseigne pas l'utilisateur sur sa conduite à tenir. Pour cela, il est nécessaire d'agrémenter le déroulement du programme de messages qui aident à son utilisation. Voici par exemple comment on pourrait procéder : variables entier x, y, z; afficher("Somme de 2 valeurs "); afficher("Entrez la 1ere valeur "); x := lire(); afficher("Entrez la 2eme valeur "); y := lire(); z := x+y; afficher("La somme vaut : ", z); Nous n'insisterons pas trop sur ce type d'entrées et sorties, car de nos jours, cette façon de procéder est de plus en plus remplacée par l'utilisation d'interfaces graphiques avec zones de saisies et boites de dialogue, dont la programmation dépend de l'environnement de développement utilisé. En langage C, l'affichage à l'écran de la valeur d'une variable est réalisé grâce à la fonction printf dont le fonctionnement dans un premier temps peut se résumer ainsi : • Affichage d'un message : printf("message");

Page 10: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 10 Pierre Tellier

• Affichage d'un message puis passage à la ligne : printf("message\n");

• Affichage de la valeur d'une variable maVarEnt entière (int) : printf("%d",maVarEnt);

• Affichage de la valeur d'une variable maVarReel réelle (float) : printf("%f",maVarReel);

• Affichage de la valeur d'une variable maVarEnt entière (int) dans un texte : printf("la variable maVarEnt vaut %d",maVarEnt);

En fait, il faut retenir pour l'instant que la fonction printf affiche un texte à l'écran. Ce texte peut être un texte simple, ou un texte construit (on dit formaté) à partir de variables (et de texte). Les règles %d et %f sont là pour indiquer comment le contenu de la variable doit être interprété : %d = interpréter l'information binaire contenue par la variable comme une valeur entière et la convertir comme telle, %f = interpréter l'information binaire contenue par la variable comme une valeur réelle et la convertir comme telle. Il est aussi possible d'afficher plusieurs variables en un seul ordre, comme le montre cet exemple : printf("maVarEnt vaut %d et maVarReel vaut %f\n",maVarEnt,maVarReel);. Nous verrons en cours comment intervenir sur le format des nombres affichés (taille de l'affichage, nombre de chiffres après la virgule etc.). La fonction qui réalise les lectures est scanf. Elle s'utilise de manière un peu particulière sur les variables entières et réelles (ne parlons pas des caractères !) : • lecture au clavier de la valeur d'une variable maVarEnt entière (int) : scanf("%d",&maVarEnt);

• lecture au clavier de la valeur d'une variable maVarReel réelle (float) : scanf("%f",&maVarReel);

La grande différence avec printf est la présence du symbole & devant chaque variable. Il signifie adresse de. Nous verrons plus tard les raisons de sa présence, mais pour l'instant contentons nous de considérer ça comme une facétie syntaxique (ce qui est loin d'être le cas en réalité). Il est aussi possible d'ordonner la lecture de la valeur de plusieurs variables en un seul ordre, comme sur cet exemple : scanf("%d %f",&maVarEnt,&maVarReel); Il suffira alors d'entrer 2 valeurs, séparées par au moins un espace, tabulation ou retour-chariot. Il s'agit d'entrée formatée, les valeurs entrées sous forme décimale seront converties selon les règles de conversion précisées et rangées aux emplacements correspondant à chaque variable. Voici donc la traduction en C de notre petit programme d'addition : int x, y, z; printf("Somme de 2 valeurs\n"); printf("Entrez la 1ere valeur "); scanf("%d",&x); printf("Entrez la 2eme valeur "); scanf("%d",&y); z = x+y; printf("La somme vaut : %d\n",z);

Premiers programmes

Structure des programmes élémentaires Un programme élémentaire est constitué uniquement d'un programme principal, délimité par les mots-clé début (indique que l'exécution du programme débute ici) et fin (provoque la terminaison du programme). On y trouve d'abord les déclarations nécessaires (constantes, variables), puis la séquence d'instruction (affectations, entrées et sorties) qui constitue l'algorithme. Ainsi, notre exemple sur l'addition ne devient un programme qu'encadré de début et fin. variables

entier x, y, z; début

afficher("Somme de 2 valeurs "); afficher("Entrez la 1ere valeur "); x := lire(); afficher("Entrez la 2eme valeur "); y := lire(); z := x+y; afficher("La somme vaut : "); afficher(z);

fin

Programmes élémentaires en C En langage C, un programme principal (sous sa forme la plus rudimentaire) se présente sous la forme d'une suite de déclarations, suivie d'une suite d'instructions, le tout entre accolades et précédé de main() :

Page 11: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 11 Pierre Tellier

main() { /* déclarations */ /* instructions */ } Remarque : Le texte compris entre /* et */ est un commentaire. À ce propos, il n'est pas possible d'imbriquer les commentaires. En pratique, il est courant d'avoir recours à une première partie consacrée aux inclusions de types et prototypes (tous ces termes seront expliqués par la suite), ainsi qu'aux définitions de constantes. En particulier, si on utilise printf ou scanf dans un programme, il faut le préciser, en général en tout début, grâce à la directive #include <stdio.h> qui indique qu'il faut inclure au programme le fichier entête stdio.h (.h = header) qui décrit le comportement des fonctions d'entrées/sorties standard (io=input/output, std=standard). En C, notre programme d'addition donne donc : #include <stdio.h> main() { int x, y, z; printf("Somme de 2 valeurs\n"); printf("Entrez la 1ere valeur "); scanf("%d",&x); printf("Entrez la 2eme valeur "); scanf("%d",&y); z = x+y; printf("La somme vaut : %d\n",z); }

Première compilation et exécution Il est de coutume de donner un nom qui finit par ".c" aux fichiers qui contiennent des programmes en langage C. Nous allons donc sauvegarder les instructions ci-dessus dans un fichier appelé addition.c, qu'il nous faut d'abord compiler : gcc -o addition addition.c puis exécuter, une fois le fichier exécutable addition généré : addition ou ./addition

Autres exemples Voici un exemple de petit programme qui calcule le périmètre d'un cercle, obtenu par la formule : périmètre = 2.π.rayon. constantes réel pi vaut 3.14159; variables réel rayon, périmètre; début afficher("entrez la valeur du rayon du cercle : "); rayon := lire(); périmètre := 2.0 * pi * rayon; afficher("le périmètre vaut : " , périmètre); fin Puis voici seulement le programme C, que nous appelons pericercle.c #include <stdio.h> #define PI 3.14159 main() { float rayon, perimetre; printf("entrez la valeur du rayon du cercle : "); scanf("%f", &rayon); perimetre = 2.0 * PI * rayon; /* 2.0 et non 2 car il s'agit de réels */ printf("le périmètre vaut %f : ",perimetre); } Pour exécuter ce programme il est d'abord nécessaire de le compiler :

Page 12: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 12 Pierre Tellier

gcc -o pericercle pericercle.c Une fois le fichier exécutable pericercle généré, on l'exécute ainsi : pericercle ou ./pericercle Maintenant écrivons un petit programme permettant le calcul de la surface du disque, grâce à la formule surface=π.rayon2. Cet exemple montre qu'il n'est pas toujours nécessaire d'utiliser de variables intermédiaires. Nous avons aussi rendu globale la déclaration de constante. constantes

réel pi vaut 3.14159; variables

réel rayon; début afficher("entrez la valeur du rayon du cercle : "); rayon := lire(); afficher("la surface vaut : ", PI * rayon * rayon); fin La traduction de cet algorithme en C fait l'objet du programme surfadisque.c : #include <stdio.h> #define PI 3.14159 main() { float rayon; printf("entrez la valeur du rayon du cercle : "); scanf("%f", &rayon); printf("la surface vaut : %f ", PI * rayon * rayon); } compilation : gcc -o surfadisque surfadisque.c exécution : surfadisque En algorithmique, on peut supposer pour résoudre nos problèmes l'existence de fonctions prédéfinies, en particulier pour les traitements mathématiques et les traitements sur les chaînes. Si jamais ces fonctions n'existaient pas, il serait toujours temps d'y remédier. Nous verrons plus tard que cette façon de procéder est à la base du raisonnement algorithmique. En C, il est aussi possible d'utiliser certaines fonctions arithmétiques prédéfinies, comme sqrt qui calcule la racine carrée d'un nombre réel. Comme pour les fonctions d'entrée/sortie, il faut inclure l'entête concernant ces fonctions. De plus, l'usage de la librairie mathématique doit être indiqué lors de la compilation. Ainsi le programme qui calcule la diagonale d'un rectangle de longueur L et de largeur l se présente ainsi : variables réel largeur, longueur, diagonale; début afficher("entrez la longueur et la largeur du rectangle : "); longueur := lire(); largeur := lire(); diagonale := racineCarrée(longueur * longueur + largeur * largeur); afficher("la diagonale vaut : ", diagonale); fin Sa traduction en C est saisie dans le fichier diagonale.c : #include <stdio.h> #include <math.h> main() { float largeur, longueur, diagonale; printf("entrez la longueur et la largeur du rectangle : "); scanf("%f %f",&longueur,&largeur); diagonale = sqrt(longueur*longueur+largeur*largeur); printf("la diagonale vaut : %f ", diagonale); } compilation : gcc -o diagonale diagonale.c -lm exécution : diagonale

Page 13: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 13 Pierre Tellier

Remarques Le programme suivant est faux d'un point de vue sémantique, car une variable est utilisée (en partie droite d'une affectation), sans jamais avoir été à gauche ni fait l'objet d'un lire(). Son contenu est donc indéterminé et cette pratique formellement interdite, car il n'existe pas de valeur par défaut pour les variables. variables entier x, y; début y := x+1; afficher("y vaut : ", y); fin Ce genre d'erreur non syntaxique est difficile à détecter en C, à moins d'utiliser toutes les possibilités des compilateurs. Voici la traduction en C de ce programme faux (pasbon.c). #include <stdio.h> main() { int x, y; y = x+1; /* ce programme est faux, x n'a pas été initialisé */ /* cette erreur n'est pas détectée par la plupart des compilateurs */ printf("y vaut : %d \n", y); } compilation : gcc pasbon.c -Wall -o pasbon Cette option -Wall permet de détecter l'existence de variables possiblement non initialisées (et plein d'autres choses encore). exécution : pasbon

Analyse descendante Dans cette section, nous illustrons par un exemple comment résoudre un problème encore extrêmement simple, toutefois un peu plus compliqué que ceux que nous avons pu évoquer jusqu'à présent. Il s'agit du volume d'un écrou à 8 pans de longueur l, évidé par un cylindre de rayon r et dont la hauteur est h.

Nous sommes amenés à décomposer ce problème en sous-problèmes pour nous approcher de la solution. L'arbre qui suit montre la démarche descendante qui permet d'aboutir à des problèmes élémentaires faciles à résoudre.

Page 14: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 14 Pierre Tellier

La réalisation du programme correspondant se fait exactement dans l'ordre inverse : on réalise d'abord les opérations élémentaires, que l'on enchaîne pour aboutir au résultat : constantes

réel pi vaut 3.14159; variables

réel l, h, r, hauteurT, surfaceT, surfaceO, surfaceC, surfaceE, volumeE; début

afficher("Entrez la taille du côté, le rayon du trou et la hauteur de l'écrou :"); l := lire(); r := lire(); h := lire(); hauteurT := (l/2.0)/tangente(pi/8.0); /* tangente : fonction mathématique prédéfinie */ surfaceT := l*hauteurT/2.0; surfaceO := 8.0*surfaceT; surfaceC := pi*r*r; surfaceE := surfaceO-surfaceC; volumeE := surfaceE*h; afficher("son volume est : ",volumeE);

fin En voici une traduction en langage C : #include <stdio.h> #include <math.h> main() {

float l, h, r, hauteurT, surfaceT, surfaceO, surfaceC, surfaceE, volumeE; printf("Entrez taille du côté, rayon du trou et hauteur de l'écrou :"); scanf("%f %f %f",&l,&r,&h); hauteurT=(l/2.0)/tan(M_PI/8.0); /* tan et M_PI : fonction et constante mathématiques prédéfinies */ surfaceT=l*hauteurT/2.0; surfaceO=8.0*surfaceT; surfaceC=M_PI*r*r; surfaceE=surfaceO-surfaceC; volumeE=surfaceE*h; printf("son volume est : %f\n",volumeE);

}

Page 15: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 15 Pierre Tellier

Fonctions Nous avons déjà vu quelques fonctions dans le chapitre précédent : afficher, lire, tangente, racineCarrée. En C nous avons aussi rencontré ces fonctions : printf, scanf, tan, sqrt (et main qui est une fonction particulière, celle par laquelle l'exécution de tout programme commence). Nous allons maintenant étudier ce concept de fonction plus en détail. En mathématique, une fonction est une relation qui associe à une variable (ou plusieurs) une et une seule valeur. Nous allons procéder de la même façon en programmation, quoique certaines différences pourront dans certains cas apparaître. La possibilité de réaliser des fonctions informatiques présente de nombreux intérêts. En nous basant sur le concept des fonctions mathématiques, cela permet une meilleure abstraction grâce à une meilleure description (spécification) de ce que doit réaliser le programme et sur la façon dont cela doit être fait. En outre, cela concourt grandement à une meilleure structuration des programmes. En effet, l'analyse descendante préconisée précédemment sera réalisée au moyen d'un découpage fonctionnel. Enfin, la mise au point des programmes s'en trouve facilitée, grâce à une meilleure lisibilité et à une meilleure localisation des traitements. Chaque fonction sera responsable d'un traitement bien défini, et sera suffisamment courte pour tenir d'un seul tenant à l'écran. Elle pourra donc être mise au point facilement et indépendamment du reste du programme.

Fonctions avec paramètres et résultat Il s'agit du cas le plus classique de fonction, qui s'apparente aux fonctions mathématiques. Prenons l'exemple de la fonction à une variable f(x)=2x+3, définie de R dans R. On a coutume de spécifier un telle fonction de cette façon :

32:

+→

xxRRf

a

Le passage à une fonction informatique est immédiat et presque automatique : fonction f(réel x) : réel début

f := 2.0*x+3.0; fin dont la traduction en C est float f(float x) {

return 2.0*x+3.0; } Ceci mérite quelques explications supplémentaires : :réel indique que la valeur de la fonction est de type réel (le domaine d'application). Après le nom de la fonction, (réel x) indique que la fonction f est une fonction à une variable réelle. En informatique, on dit que f admet un paramètre x, et qu'il est de type réel. On parle de paramètre formel, car cette fonction délivre un résultat fonction de x, peu importe sa valeur. Enfin, l'affectation f := expression permet de donner la valeur à la fonction, calculée à partir de celle du paramètre. Les règles de bonne programmation préconisent de n'affecter qu'une fois la fonction et de placer cet ordre exclusivement comme dernière instruction de la fonction. Détaillons maintenant la syntaxe de la même fonction en langage C. float f indique que la fonction s'appelle f est qu'elle est de type float, i.e. qu'elle délivre des résultats de type float. (float x) indique que la fonction f est une fonction à une variable réelle (un paramètre x de type float). L'instruction return permet de délivrer le résultat. Là aussi, Les règles de bonne programmation préconisent de placer cet ordre exclusivement à la fin de la fonction. Une fois la fonction déclarée (avant toute utilisation), on peut l'utiliser autant de fois qu'on le désire, dans diverses formes d’expressions, ou en pratiquant la composition de fonctions, comme le montrent les exemples d'appels de fonction suivants : variables réel x init 4.0, y, z init 1.0; y := f(x); y := f(5.5); y := f(z+f(3.7)); afficher(f(x)); qui sont presque les mêmes en C :

Page 16: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 16 Pierre Tellier

float x = 4.0, y, z = 1.0; y = f(x); y = f(5.5); y = f(z+f(3.7)); printf("%f",f(x)); Dans ces exemples, les valeurs passées à la fonction f : x, 5.5 et z+f(3.7) sont appelées paramètres effectifs : les valeurs effectivement passées à la fonction. Il s'agit d'un passage de paramètre par valeur, c'est-à-dire que l'expression en paramètre est d'abord évaluée, et qu'ensuite sa valeur est transmise à la fonction. Le paramètre formel prend alors cette valeur pour l'exécution de la fonction. Prenons un autre exemple, celui de la moyenne de 2 nombres réels. La spécification de la fonction moyenne est :

2/)(),(:

babaRRRmoyenne

+→×a

La fonction qui réalise ce traitement est fonction moyenne(réel a; réel b) : réel variables réel res; début res := (a+b)/2.0; moyenne := res; fin afficher(moyenne(12.5, 14)); z := moyenne(x, y); Lorsqu'on a plusieurs paramètres, la syntaxe ressemble à celle de la déclaration de variables. On peut regrouper les variables par type, ou les séparer à l'aide d'un ";" : fonction moyenne(réel a, b) : réel est équivalent à fonction moyenne(réel a; réel b) : réel. En voici une traduction possible en C : float moyenne(float a, float b) { float res; res = (a+b)/2.0; return res; } printf("%f",moyenne(12.5, 14)); z = moyenne(x, y);} Encore une nouveauté dans cette fonction : la variable res déclarée à l'intérieur de la fonction est une variable locale. Sa portée (durée de vie) est limitée à la fonction, il n'y a donc pas d'interférences avec d'autres variables du même nom déclarées dans d'autres fonctions ou comme variables globales. Nous reparlerons des différentes portées des variables avant la fin de ce chapitre.

Fonctions sans résultat : procédures On parle de procédures (ou de sous-programmes) pour de telles fonctions car on s'éloigne ici de la signification mathématique des fonctions, qui possèdent en général au moins une variable et une valeur. L'intérêt de fonctions qui n'ont pas d'effet sur les variables est assez limité en algorithmique, à part peut être pour faire l'affichage de variables : procédure afficheEntier(entier x) début afficher ("valeur : ", x); fin En C, on ne fait pas de distinction entre fonctions et procédures. L'absence de résultat est simplement signifiée par le type de retour void. La dernière instruction de la fonction est un simple return suivi de rien, qui est de plus facultatif. void afficheEntier(int x) /* affichage d'un entier */ { printf("valeur : %d",x); return; /* facultatif */ } /* appel */ afficheEntier(age);

Page 17: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 17 Pierre Tellier

Fonctions et procédures sans paramètres Là encore, on s'éloigne ici de la signification mathématique des fonctions. Leur usage est moins fréquent que les fonctions classiques, et la plupart du temps correspond à des traitements de type "système" ou génération de valeur (lecture, production de nombre aléatoire etc.). Nous avons déjà utilisé la fonction lire() qui répond à ce schéma. Pour déclarer une telle fonction, il suffit de ne rien écrire à l'intérieur des parenthèses normalement censées contenir les paramètres. En C, on écrira simplement void à l'intérieur de ces parenthèses. L'appel se fait toujours avec des parenthèses vides. fonction lireEntier() : entier variables entier x; début x := lire(); lireEntier := x; fin int lireEntier(void) /* lecture d'un nombre entier au clavier (pour cacher le &) */ { int x; scanf("%d",&x); return x; } /* appel */ age = lireEntier(); Remarques : Il n'y a pas d'interférence entre le nom des paramètres et le nom des variables déclarées en dehors des fonctions. Ainsi dans les exemples précédents, la variable x dans la fonction lireEntier et le paramètre x de la procédure afficheEntier ne désignent pas le même objet. Enfin, un exemple de procédure qui n'a pas de paramètre, et se contente d'afficher un message constant : procédure afficheMessageBienvenue() début afficheln("----------------------------------"); afficheln(" Bienvenue cher utilisateur "); afficheln("----------------------------------"); fin void afficheMessageBienvenue(void) /* affichage d'un message */ { printf("----------------------------\n"); printf(" Bienvenue cher utilisateur \n"); printf("----------------------------\n"); return; /* facultatif */ } /* appel */ afficheMessageBienvenue(); Les paramètres ne sont jamais modifiés par une fonction, comme le montre l'exemple suivant : int a = 2; void lireEntierPasBon(int x) /* lecture d'un nombre entier au clavier (pour cacher le &) : ne marche pas */ { scanf("%d",&x); } printf("%d",a); /* affiche 2 à l'écran */ lireEntierPasBon(a); /* entrez 3 par exemple */ printf("%d",a); /* affiche encore 2 à l'écran ! */

Page 18: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 18 Pierre Tellier

Fonctions avec plusieurs résultats Résultat n-uplet Dans de nombreux cas, les fonctions informatiques ne peuvent se contenter de délivrer un seul résultat. Cela est facilement contourné en mathématique où une fonction peut délivrer un résultat qui peut être un n-uplet. En algorithmique nous allons procéder exactement de cette façon. Imaginons une fonction qui décompose un nombre réel en 2 parties : partie entière et partie décimale. Deux résultats doivent être délivrés. En fait, nous n'allons rendre qu'une seule valeur, le couple formé des deux nombres, un entier et un réel. La spécification de la fonction est :

et avec ),(:

x-[x] d : [x] e :dexRRRsépare

==×→

a

et son algorithme : fonction sépare(réel x) : (entier, réel) variables entier ent; réel dec; début ent := entier(x); dec := x - réel(ent); sépare := (ent, dec); fin L'utilisation de fonctions à résultat n-uplet se fait de la manière suivante : variables entier e; réel r, nb; nb := lire(); (e,r) := sépare(nb); afficher(e); afficher(r); La traduction de cet exemple n'est pas immédiate en C, car le type n-uplet n'est pas prédéfini. Nous allons pour cela avoir recours à la construction de types qui permettent la déclaration de variables contenant plusieurs informations. L'usage de telles structures (appelées aussi parfois enregistrements) sera vu plus en détail dans un chapitre ultérieur.

Les structures en C Le type n-uplet n'existe pas naturellement en C, mais le langage permet de créer des nouveaux types de données constitués de plusieurs informations. Ainsi, pour définir un couple constitué d'un nombre entier, et d'un nombre réel, nous utiliserons la syntaxe suivante : typedef struct { int e; float d; } EntPlusDec; typedef est un mot-clé du langage C qui permet la définition de nouveaux types (pas uniquement des structures). EntPlusDec est alors le nom d'un nouveau type, qui va permettre de déclarer des variables de ce type. L'accès aux informations contenues dans de telles variables (on parle des champs d'une structure) se fait grâce au symbole ".". Chaque champ s'utilise exactement comme une variable traditionnelle. Voici donc la traduction de notre précédente fonction en langage C : EntPlusDec separe(float x) { EntPlusDec res; res.e = (int)x; /* ou int(x) */ res.d = x - (int)res.e; /* ou float(res.e) */ return res; } dont l'utilisation est : EntPlusDec ed; float x; scanf("%f",&x);

Page 19: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 19 Pierre Tellier

ed = separe(x); printf("%d ",ed.e); printf("%f ",ed.d); Nous reviendrons bien plus en détail sur les structures, entre autres nous verrons qu'elles peuvent être imbriquées et contenir des informations pas forcément élémentaires. En attendant, nous donnons une autre syntaxe, dont l'utilité sera dévoilée plus tard : typedef struct t_entplusdec { int e; float d; } EntPlusDec; dans laquelle struct t_entplusdec est une autre façon de désigner le nouveau type.

Paramètres modifiables (passage par adresse) en C Une autre façon de délivrer plusieurs résultats consiste à utiliser des paramètres modifiables. Ceci est possible dans d'autres langages que le C, mais dans le contexte d'une initiation à la programmation, je déconseille cette pratique car elle est franchement éloignée du formalisme de spécification proche des mathématiques que nous avons adopté. Notons qu'il ne s'agit toutefois pas d'un pis-aller, de nombreux programmeurs ont l'habitude de passer les résultats sous forme de paramètres, et réservent la valeur délivrée par la fonction comme code d'échec ou de succès de la fonction. Toutefois ces paramètres modifiables n'existent pas non plus réellement en langage C. Il faut avoir recours à une astuce qui consiste à utiliser l'adresse des variables (rappelez vous de scanf). Nous reviendrons sur le mécanisme des pointeurs dans un chapitre dédié à cela. Pour l'instant encore, considérons l'introduction des paramètres modifiables comme une histoire de syntaxe. Soit la fonction sommeFois, qui calcule la somme et le produit de 2 nombres entiers. Sa spécification est :

)*,(),(:

bababaRRRRsommeFois

+×→×

a et peut s'écrire ainsi : void sommeFois(int a, int b, int *s, int *p) { *s = a+b; *p = a*b; } Dans cette fonction, a et b sont des entiers, ainsi que *s et *p. En fait les paramètres ne sont pas *s et *p, mais s et p, chacun du type (int *), c'est-à-dire que s et p sont des adresses de variables de type int. On dit que s et p sont des pointeurs sur des entiers. Le symbole * permet d'obtenir (dé-référencer) la variable à partir de son adresse. L'appel de la fonction se fait en utilisant les adresses des paramètres modifiables, obtenues en précédant les variables du symbole & : int nb1 = 1,nb2 = 3,somme,produit; sommeFois(nb1,nb2,&somme,&produit); printf("%d+%d = %d, %d*%d = %d\n",nb1,nb2,somme,nb1,nb2,produit); Important : lorsqu'on travaille déjà sur l'adresse d'une variable et qu'on doit à nouveau l'utiliser comme paramètre modifiable, il ne faut pas remettre le symbole & : void lireEntier(int *x) { scanf("%d",x); /* et non pas scanf("%d",&x) car x est déjà une adresse */ /* équivalent à scanf(&*x); !!! */ } int a; lireEntier(&a); /* n'apporte pas grand chose de plus que scanf ! */

Variables et portée des variables

Variables locales, variables globales Pour l'instant nous avons vu qu'il est possible de déclarer des variables à l'intérieur de chaque fonction et du programme principal. On appelle ces variables des variables locales, car elles sont inaccessibles en dehors des fonctions où elles ont été déclarées. Les fonctions communiquent entre elles (s'échangent des valeurs) grâce au passage des paramètres. Il existe d'autres façons de déclarer des variables, parmi lesquelles la

Page 20: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 20 Pierre Tellier

déclaration de variables globales : il s'agit de variables déclarées en dehors des fonctions, au début du programme En C les déclarations de variables globales sont en général après les directives d'inclusion. Ces variables appelées globales peuvent être désignées dans n'importe quelle fonction, elles sont accessibles partout. Si un paramètre ou une variable locale porte le même nom qu'une variable globale, il masque la variable globale, c'est-à-dire qu'il n'y a pas d'ambiguïté, on utilise le paramètre ou la variable locale, sans aucun risque d'interférence avec la variable globale. En revanche, il est interdit de déclarer dans une fonction une variable locale qui porte le nom d'un paramètre. L'usage des variables globales est extrêmement dangereux, car il est difficile de localiser où leur valeur est modifiée. Il est donc très important de limiter leur usage, de concevoir des fonctions qui ne manipulent pas de telles variables. Si cela s'avère impossible (on n'a pas toujours le droit de modifier le prototype des fonctions), on doit signaler à l'aide de commentaires les variables globales utilisées et surtout modifiées dans les fonctions. Ci suit un exemple de commentaires de fonctions, mentionnant l'accès à des variables globales. Comme cela est fastidieux, et gourmand en place, ce genre de commentaire sera le plus souvent omis dans ce polycopié. /* ---------------------------------------------------------------- fonction f : float f(float) Évaluation de la fonction mathématique f(x) = 2x+3 Fixes : float x = abscisse Modifiables : néant Résultat : réel = valeur de la fonction au point d'abscisse x Globales utilisées : néant Globales modifiées : néant ---------------------------------------------------------------- */ float f(float x) { return 2.0*x+3.0; } Normalement les variables locales sont créées au début de l'exécution des fonctions, puis détruites à la fin. Donc entre 2 appels successifs la valeur de ces variables est perdue, il faut les réinitialiser à chaque fois.

Variables statiques et automatiques en C Il est possible d'imposer une rémanence à des variables (elles conservent leur valeur) grâce au mot-clé static : int compteur(void) { static int i = 0; i = i+1; return i; } printf("%d ",compteur()); /* affiche 1 */ printf("%d ",compteur()); /* affiche 2 */ Le mot-clé static, devant une variable globale, indique qu'elle ne peut pas être partagée avec d'autres modules. En effet, pour réaliser de gros programmes, on utilise plusieurs modules (un module = un fichier contenant des fonctions). Une variable globale static est accessible dans tout le module, mais pas dans les autres. De manière symétrique, pour utiliser une variable déclarée globale dans un autre module, il suffit de la re-déclarer mais précédée du mot-clé extern (mais sans initialisation) qui signifie que la variable a déjà été déclarée à l'extérieur du module. Enfin citons les variables automatiques} du langage C, qui sont des variables encore plus locales que les variables locales que nous avons vues. En effet, déclarées au début d'un bloc d'instructions, leur portée est restreinte à ce bloc. Un bloc d'instructions est une séquence délimitée par { et }. Pour l'instant nous n'avons créé qu'un seul bloc par fonction, mais il est possible de les imbriquer, bien que cela soit rare de procéder ainsi, s'il n'y a pas de bonne raison : conditionnelle ou itération. Le petit exemple suivant montre quelques unes des variables dont nous venons de parler : /* variables globales */ int a = 2, b = 1, c; int sommeFoisDeux(int x, int y) {

Page 21: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 21 Pierre Tellier

int a,c; /* a et c sont des variables locales */ a = x+y; { int b; /* b est une variable automatique */ b = 2*a; c = b; } return c; } c = sommeFoisDeux(a,b); printf("%d %d",a,b); /* affiche 2 1 */

Fonctions en paramètre de fonctions Une fonction peut être passée en paramètre d'une fonction, comme n'importe quel autre objet. La syntaxe est juste un peu plus compliquée, comme le montre la fonction dérivée, qui prend en paramètre une fonction f (fonction réelle à une variable réelle) et une variable x, et qui calcule f'(x) en utilisant la formule de la dérivée en un point d’abscisse x, quand h tend vers 0 :

hxfhxfxf h)()(lim)(' 0

−+= →

fonction f1(réel x) : réel début f1 := 3.0*x + 2.0; fin fonction f2(réel x) : réel début f2 := 2.0*x*x + 5.0*x; fin fonction dérivée((réel):(réel) f; réel x; réel h) : réel début dérivée:= (f(x+h)-f(x))/h; fin variables réel yprime; début yprime := dérivée (f1, 5.0, 0.001); /* 3.051758 (dérivée(3x+2)=3) */ yprime := dérivée (f2, 3.0, 0.00001); /* 17.166138 (dérivée(2x^2+5x)=4x+5)*/ fin Une fois traduit en C : float f1(float x) { return 3.0*x + 2.0; } float f2(float x) { return 2.0*x*x + 5.0*x; } float derivee(float (*f)(float), float x, float h) { return (f(x+h)-f(x))/h; } main() {

Page 22: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 22 Pierre Tellier

float yprime; yprime=derivee(f1, 5.0, 0.001); /* 3.051758 (dérivée(3x+2)=3) */ yprime=derivee(f2, 3.0, 0.00001); /* 17.166138 (dérivée(2x^2+5x)=4x+5)*/ }

Exemples de programmes avec des fonctions Volume de l'écrou Nous reprenons ici l'exemple du calcul du volume de l'écrou, écrit cette fois ci à l'aide de fonctions. Chacun des traitements élémentaires décrits dans l'analyse descendante fait l'objet d'une fonction :

2/*),(:

hauteurbasesurfacehauteurbaseRRRanglesurfaceTri

=→×a

))8/tan(/)2/(cot,(cot*8cot:

πééanglesurfaceTrisurfaceéRRogonesurfaceOct

=→a

*:

2rayonsurfacerayonRRclesurfaceCer

π=→a

hauteurrayonclesurfaceCeréogonesurfaceOctvolumehauteurrayonéRRRRuvolumeEcro

*))()(cot(),,(cot:

−=→××a

Le programme devient : constantes pi vaut 3.14159; fonction surfaceTriangle(réel base, hauteur) : réel début surfaceTriangle := base * hauteur / 2; fin fonction surfaceOctogone(réel cote) : réel début réel demiCôté init cote*0.5, hauteur; hauteur := demiCôté/tangente(pi/8.0); surfaceOctogone := 8.0 * surfaceTriangle(cote, hauteur); fin fonction surfaceCercle(réel rayon) : réel début surfaceCercle := pi*rayon*rayon; fin fonction volumeEcrou(réel rayon, cote, épaisseur) : réel début volumeEcrou := (surfaceOctogone(cote)-surfaceCercle(rayon))*épaisseur; fin variables réel l, h, r volumeE; début afficher("Entrez la taille du côté, le rayon du trou et la hauteur de l'écrou :"); l := lire(); r := lire(); h := lire(); volumeE := volumeEcrou(r, l, h); afficher("le volume est : ", volumeE); fin et sa traduction en C #include <stdio.h> #include <math.h> /* Surface d'un triangle en fonction de sa base et de sa hauteur */ float surfaceTriangle(float base, float hauteur) {

Page 23: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 23 Pierre Tellier

return 0.5 * base * hauteur; } /* Surface d'un octogone régulier en fonction de la longueur de ses côtés */ float surfaceOctogone(float cote) { float demiCote = cote*0.5, hauteur, surfaceT; /* on coupe chacun des 8 triangles en 2 triangles rectangles */ hauteur = demiCote/tan(M_PI/8.0); surfaceT = surfaceTriangle(cote, hauteur); return 8.0 * surfaceT; } /* Surface d'un cercle en fonction de son rayon */ float surfaceCercle(float rayon) { return M_PI*rayon*rayon; } float volumeEcrou(float rayon, float cote, float epaisseur) { return (surfaceOctogone(cote)-surfaceCercle(rayon))*epaisseur; } main() { float l, h, r, volumeE; printf("Taille du côté, rayon du trou et hauteur de l'écrou :"); scanf("%f %f %f",&l,&r,&h); volumeE = volumeEcrou(r, l, h); printf("le volume est : %f\n",volumeE); }

Calcul de la durée Autre exemple, le calcul de la durée entre 2 instants, exprimés en en heures, minutes et secondes. La stratégie de calcul est la suivante : on convertit chacun des instants en secondes, on fait la différence, qu'on reconvertit en heures, minutes et secondes.

smhsmhNNNNhms

++→××

60*3600*),,(:sec_

a

60modsec603600modsec3600divsec avec),,((sec):sec_

, s)div (, m hsmhNNNNhms

===××→

a

)),s,m(h)-hms_,s,m(h_hms(hms_),s,m,h,s,m(hNNNNNNNNNdurée

111sec222secsec222111:

a××→×××××

On en déduit l'algorithme : fonction hms_sec(entier h,m,s) : entier début hms_sec := h*3600+m*60+s; fin fonction sec_hms(entier sec) : (entier, entier, entier) variables entier h,m,s; début h := sec div 3600; m := (sec mod 3600)div 60; s := sec mod 60; sec_hms := (h,m,s); fin fonction durée(entier h1, m1, s1, h2, m2, s2) : (entier, entier, entier) début

Page 24: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 24 Pierre Tellier

durée := sec_hms(hms_sec(h2, m2, s2)-hms_sec(h1, m1, s1)); fin Deux de ces fonctions délivrent plusieurs résultats. On aura donc recours à la création d'un nouveau type, le triplet heures, minutes et secondes. On profitera de ce type non seulement comme résultat de la fonction mais aussi pour le passage des paramètres, horaire de début et horaire de fin. typedef struct t_horaire { int h,m,s; } Horaire; int hms_sec(Horaire h1) { return h1.h*3600+h1.m*60+h1.s ; } Horaire sec_hms(int sec) { Horaire res; res.h = sec/3600 ; res.m = (sec%3600)/60 ; res.s = sec%60 ; return res; } Horaire duree(Horaire h1, Horaire h2) { Horaire res; res = sec_hms(hms_sec(h2)-hms_sec(h1) ; return res; } Rappelons que les structures peuvent être affectées entre elles, et qu'on utilise leur champs comme des variables simples. Un exemple d'usage de la fonction duree suit : main() { Horaire h1, h2, diff; printf("Entrez le premier horaire : "); scanf("%d %d %d",&h1.h, &h1.m, &h1.s); printf("Entrez le deuxième horaire : "); scanf("%d %d %d",&h2.h, &h2.m, &h2.s); diff = duree(h1, h2); printf("durée: %d heures %d minutes %d secondes", diff.h, diff.m, diff.s); } Pour comparer, nous donnons aussi la traduction en C avec paramètres modifiables, bien qu'elle soit beaucoup moins élégante. int conversion_hms_sec(int h, int m, int s) { return h*3600+m*60+s ; } void conversion_sec_hms(int sec, int *h, int *m, int *s) { *h = sec/3600 ; *m = (sec%3600)/60 ; *s = sec%60 ; } void duree(int h1,int m1,int s1,int h2,int m2,int s2,int *h3,int *m3,int *s3) { int sec1, sec2 ; sec1 = conversion_hms_sec(h1, m1, s1) ; sec2 = conversion_hms_sec(h2, m2, s2) ; conversion_sec_hms(sec2 - sec1, h3, m3, s3) ; }

Page 25: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 25 Pierre Tellier

Remarque : on ne met pas &h3 parce que h3 est déjà un pointeur (*h3 en paramètre, h3 est équivalent à &*h3). Une autre méthode (sans les variables intermédiaires) : void duree(int h1, int m1, int s1, int h2, int m2, int s2, int *h3, int *m3, int *s3) { sec_hms(hms_sec(h2,m2,s2)-hms_sec(h1,m1,s1),h3,m3,s3); } Voici un exemple d'utilisation de la fonction duree, entre les 2 horaires 9h 45min 52sec et 10h 13min 43sec : main() { int h, m, s; duree(9, 45, 52, 10, 13, 43, &h, &m, &s); printf("la durée est : %d heures, %d minutes et %d secondes\n", h, m, s); }

Langage C : arguments de main

Langage C : nombre quelconque de paramètres

Page 26: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 26 Pierre Tellier

Conditionnelle La programmation des traitements vus jusqu'alors est constituée d'une suite d'instructions qui s'enchaînent les unes après les autres. Or, il est de nombreuses circonstances où l'on voudrait pouvoir différencier les traitements selon les calculs à effectuer. Dans les exemples précédents, nous avons omis les plus élémentaires contrôles : pour le calcul du volume de l'écrou, les valeurs fournies par l'utilisateur sont-elles positives, le rayon du trou de l'écrou est il plausible par rapport à la taille du côté, ... ? Si oui pas de problème, notre algorithme s'applique. Sinon que doit-on faire ? Doit-on délivrer un résultat incohérent ou effectuer un traitement spécifique ? A quelles conditions un formulaire est il correctement rempli ? Autre exemple, le calcul des racines d'une équation du second degré nécessite une étude de cas en fonction du signe du discriminant. La conditionnelle est une structure de contrôle qui permet de n'effectuer les calculs que si certaines conditions sont vérifiées : si condition alors traitement finsi ou si condition alors traitement sinon traitement finsi Cette possibilité d'exprimer un traitement en fonction de la valeur d'expressions Booléennes est offerte par tous les langages de programmation et donc par le langage C. La syntaxe est : if (condition) {traitement} ou bien if (condition) {traitement} else {traitement} où condition est une expression Booléenne, et traitement est la ou les instructions à effectuer si la condition vaut VRAI. Si le traitement est réduit à une seule instruction, on peut omettre les accolades { et }. Il existe une autre façon d’exprimer la conditionnelle : (condition) ? expression : expression

Quelques exemples La deuxième pizza à moitié prix Une célèbre enseigne de livraison de pizzas à domicile propose la formule suivante : si on commande deux pizzas, la moins chère des deux est à moitié prix, ce qu'on peut exprimer à l'aide de si ... alors ... sinon : si la première pizza est la moins chère, c'est sur elle que porte la réduction, sinon c'est sur l'autre. Bien souvent, le traitement du problème peut être abordé en passant en revue l'ensemble des cas qui peuvent se produire. Dans la mesure du possible, on recommande d'utiliser à chaque fois que c'est possible la structure si ... alors ... sinon. On aura alors une meilleure efficacité, à condition de pouvoir exprimer le problème en cas mutuellement exclusifs. Sur notre exemple, il est plus judicieux d'exprimer si prix1<prix2 alors res := prix2 + prix1/2.0; sinon res := prix1 + prix2/2.0; finsi que si prix1<prix2 alors res := prix2 + prix1/2.0; finsi si prix1<=prix2 alors res := prix1 + prix2/2.0; finsi Cela évite l'évaluation de la deuxième condition, nécessairement fausse, si la première est vraie. On calculera donc le montant de la commande à partir des tarifs des deux pizzas à l'aide de la fonction suivante.

<=

→×

sinon 221 si 1)2,1(

:2

prixprixprixprixprixprixprix

RRRpizzasprixa

Ceci nous amène à l'algorithme fonction prix2pizzas(réel prix1, prix2) : réel variables réel res; début si prix1<prix2 alors res := prix2 + prix1/2.0; sinon res := prix1 + prix2/2.0; finsi prix2pizzas := res; fin qu'on peut traduire en C ainsi :

Page 27: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 27 Pierre Tellier

float prix2pizzas(float prix1, float prix2) { float res; if (prix1<prix2) res = prix2 + prix1/2.0; else res = prix1 + prix2/2.0; return res; } ou bien float prix2pizzas(float prix1, float prix2) { float res; res = (prix1<prix2) ? prix2 + prix1/2.0 : prix1 + prix2/2.0; return res; }

Équation du second degré Soit une équation du second degré de la forme ax2+bx+c=0. Le nombre de solutions dépend de la valeur du discriminant ∆ où ∆=b2-4ac. Si ∆ est strictement positif, il y a 2 solutions (-b-∆1/2)/2a et (-b+∆1/2)/2a, sinon, si ∆ est nul, il y a une solution double (-b/2a) et enfin, si ∆ est négatif, il n'y a pas de solution réelle. Une difficulté supplémentaire est présente dans cet exemple : on n'a pas toujours le même nombre de résultats : parfois un, parfois deux, parfois aucun. Or cela est inconcevable pour une fonction mathématique, de même que pour une fonction informatique qui ne délivre qu'un seul type de résultat, toujours le même. La stratégie à adopter face à ce problème doit être la suivante : il faut prévoir le cas avec le maximum de résultats, plus des informations supplémentaires pour indiquer lesquelles sont réellement pertinentes. Dans le cas de notre exemple, on prévoit de pouvoir délivrer deux solutions, plus une valeur qui indique combien il y en a réellement : aucune, une ou deux. Voici la fonction de calcul des racines des équations du second degré.

solutions les :2,1solutions de nombre

)2,1,(),,(:2

solsolnbsol

solsolnbsolcbaRRNRRRndDegracEq

=

××→××a

fonction racEq2ndDeg(réel a, b, c) : (entier, réel, réel) variables réel s1, s2, delta, racdelta; entier nbsol; début delta := b*b-4.0*a*c; si delta < 0.0 alors nbsol := 0; sinon si delta = 0.0 alors nbsol := 1; s1 := -b/(2.0*a); sinon nbsol := 2; racdelta := racineCarrée(delta); s1 := (-b-racdelta)/(2.0*a); s2 := (-b+racdelta)/(2.0*a); finsi finsi racEq2ndDeg := (nbsol, s1, s2); fin Pour la traduction en C, on a recours à la création d'un nouveau type. typedef struct { int nbsol; float s1, s2; } SolEq2ndDeg; SolEq2ndDeg racEq2ndDeg(float a, float b, float c) { SolEq2ndDeg res; int nbsol;

Page 28: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 28 Pierre Tellier

float sol1, sol2, delta, racdelta; delta = b*b-4.0*a*c; if (delta < 0.0) nbsol=0; else if (delta==0.0) { nbsol=1; sol1 = sol2 = -b/(2.0*a); } else /* cas delta > 0.0 */ { nbsol=2; racdelta=sqrt(delta); sol1 = (-b-racdelta)/(2.0*a); sol2 = (-b+racdelta)/(2.0*a); } res.nbsol = nbsol; res.s1 = sol1; res.s2 = sol2; return res; } /* un exemple d'utilisation */ main() { SolEq2ndDeg solu; float a, b, c; printf("Entrez les coefficients a, b et c de l'équation :"); scanf("%f %f %f", &a, &b, &c); solu = racEq2ndDeg(a, b, c); if (solu.nbsol == 0) printf("pas de solution\n"); else if (solu.nbsol == 1) printf("1 solution : %f\n", solu.s1); else printf("2 solutions : %f et %f\n", solu.s1, solu.s2); } Remarques • En langage C, toute instruction à une valeur, même une affectation. Ainsi l'instruction sol2 = -

b/(2.0*a) vaut -b/(2.0*a) que l'on peut sans difficulté affecter à sol1.

• Dans les fonctions, on évite de faire l'affichage des valeurs calculées, pour être le plus général possible. En effet, la finalité d'une fonction, par exemple celle qui calcule les racines d'une équation, n'est pas en général d'afficher les résultats à l'écran, mais d'être utilisée par d'autres fonctions. Son rôle est de calculer des résultats intermédiaires. C'est ainsi que la fonction racineCarrée qui calcule la racine carrée n'affiche pas à chaque fois, et heureusement, la valeur qu'elle calcule.

• Pour faciliter la lisibilité des programmes, veillez à indenter en fonction du niveau d'imbrication des instructions.

Autres exemples simples Premier autre exemple, le calcul de la facturation. Le prix unitaire en Francs est P, la quantité Q. Le montant de la facture est P*Q plus le port. Pour une commande de plus de 500 FRF, le port est gratuit, sinon il vaut 10% du montant de la commande, avec un minimum de 10 FRF. Il faut alors différencier 2 cas : la commande dépasse 500 FRF et la commande n'atteint pas 500 FRF. Ce deuxième cas se décompose aussi en 2 sous-cas : 10% de la commande dépasse 10 FRF et 10% de la commande ne dépasse pas 10 FRF : tarif brut = P * Q tarif brut > 500 : tarif à payer = tarif brut, pas de frais de port. tarif brut <= 500. On calcule les frais de port = tarif brut * 10%. si les frais de port sont inférieurs à 10 FRF, alors on fixe les frais de port à 10 FRF dans le cas contraire, on ne touche pas aux frais de port. La fonction qui met en œuvre cet algorithme est alors : fonction facture(réel prixUnitaire; entier quantité) : réel variables réel tarifBrut, fraisPort; début tarifBrut := prixUnitaire * entierVersRéel(quantité); si tarifBrut > 500. alors fraisPort=0.0;

Page 29: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 29 Pierre Tellier

sinon fraisPort := tarifBrut * 0.1; si fraisPort < 10.0 alors fraisPort=10.0; finsi facture := tarifBrut+fraisPort; fin et sa traduction en C : float facture(float prixUnitaire, int quantite) { float tarifBrut, fraisPort; tarifBrut = prixUnitaire * (float)quantite; if (tarifBrut > 500.) fraisPort=0.0; else { fraisPort = tarifBrut * 0.1; if (fraisPort < 10.0) fraisPort=10.0; } return tarifBrut+fraisPort; } Notez que l'on convertit explicitement la quantité (supposée entière) en nombre réel. Ci-après une variante plus compacte: float facture(float prixUnitaire, int quantite) { float tarifBrut = prixUnitaire * (float)quantite, fraisPort=0.0; if (tarifBrut <= 500.) { fraisPort = tarifBrut * 0.1; if (fraisPort < 10.0) fraisPort=10.0; } return tarifBrut+fraisPort; }

Macros en C Encore un exemple, pour nous familiariser avec la conditionnelle, et introduire un outil du C, les macros. Nous nous intéressons au minimum de 2 nombres entiers a et b : si a est inférieur à b, le plus petit est a, sinon c'est b. fonction miniEnt(entier a, b) : entier variables entier min; début si a<b alors min := a; sinon min := b; finsi miniEnt := min; fin int miniEnt(int a, int b) { int min; if (a<b) min=a; else min=b; return min; } Il est techniquement possible d'utiliser plusieurs fois return, mais cette façon de faire est à proscrire : int miniEnt(int a, int b) { if (a<b) return a; else return b; } Une autre écriture de cette fonction est rendue possible en langage C : int miniEnt(int a, int b)

Page 30: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 30 Pierre Tellier

{ int min; min = (a<b) ? a : b; return min; } Quelle que soit la façon dont on l'écrit, on l'utilise ainsi : int x=2, y=3, z, t; z = miniEnt(x, y); /* vaut 2 */ t = miniEnt(6, 4); /* vaut 4 */ Remarque : écrire une fonction minimum qui marche aussi avec des réels n'est pas possible en C. Pour contourner cela, on peut avoir recours à une macro : #define MIN(a,b,c) if (a<b) c=a; else c=b; Pour se servir de cette macro, il suffit d'écrire MIN(a,b,min). On ne met pas le symbole & devant min, car il ne s'agit pas d'une fonction, mais d'une définition. En fait c'est le texte MIN(a,b,min) qui est remplacé par son texte associé (c'est un raccourci, une facilité d'écriture). En pratique on choisit une écriture qui permet de récupérer un résultat à la façon d'une fonction : #define MIN(x,y) ((x)<(y)) ? (x) : (y) #define MAX(x,y) ((x)>(y)) ? (x) : (y) Remarque : dans toutes les macros, il faut mettre des parenthèses autour des pseudo-variables et autour de l'expression, car il ne s'agit pas de fonctions (pas de passage par valeur), donc si on les utilise avec des expressions, ces expressions ne sont pas évaluées avant l'exécution de la macro. Un exemple à ne pas prendre à la légère, les macros sont à l'origine de bugs souvent difficiles à isoler : #include <stdio.h> #define FOIS(a,b) a*b #define FOIS_OK(a,b) ((a)*(b)) int fois(int a, int b) { return a*b; } main() { printf("%d",fois(2+3,4)); /* affiche 5*4 = 20 */ printf("%d",FOIS(2+3,4)); /* affiche 2+3*4 = 2+12 = 14 */ printf("%d",FOIS_OK(2+3,4)); /* affiche ((2+3)*(4)) = (5*4) = 20 */ }

Condition et opérations Booléennes Le ET Booléen est tel que pour donner un résultat VRAI, les deux opérandes doivent être vrais. Au contraire, le OU donne un résultat VRAI dès qu'au moins un des deux opérandes vaut VRAI. L'opérateur NON délivre quant à lui le contraire de l'opérande. Ces trois opérations sont décrites à l'aide des tables suivantes :

a ET b b a OU b b a NON a VRAI FAUX VRAI FAUX VRAI FAUX a VRAI VRAI FAUX a VRAI VRAI VRAI FAUX VRAI FAUX FAUX FAUX FAUX VRAI FAUX

Si on ne dispose pas de ces fonctions, on peut les obtenir en effectuant une analyse de tous les cas un par un. Ainsi, le résultat de a ET b est donné par : fonction ET (Booléen a, b) : Booléen variables Booléen res; début si a = VRAI alors si b = VRAI alors res := VRAI; sinon res := FAUX; finsi sinon res := FAUX

Page 31: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 31 Pierre Tellier

finsi ET := res; fin Étant donné que le type Booléen n'existe pas en C, il est élégant de le définir de la manière suivante : typedef enum{FAUX, VRAI} Booleen; qui signifie que les objets peuvent prendre toutes les valeurs énumérées dans le type Booléen. Néanmoins on a coutume de le représenter à l'aide du type entier avec la convention que la valeur 0 vaut faux, toutes les autres valant vrai. Ainsi, la condition (a est vrai) peut être codée par (a != 0) mais est plus souvent exprimée par (a) au lieu de (a == VRAI) avec le type Booleen. De la même façon, la condition (a est faux) peut se traduire par (a == 0), mais on trouve plus souvent (!a) au lieu de (a == FAUX) avec le type Booleen. Le programme C correspondant au ET est : int ET(int a, int b) {

int res; if (a)

if (b) res = 1; /* vrai */ else res = 0; /* faux */

else res = 0; /* faux */ return res; } En remarquant que, dans le cas où a vaut VRAI, on rend VRAI si b est égal à VRAI et FAUX si b est FAUX, donc on retourne b dans les 2 cas, on peut donc simplifier le programme : fonction ET (Booléen a, b) : Booléen variables Booléen res; début si a = VRAI alors res := b; sinon res := FAUX; finsi ET := res; fin int ET(int a, int b) { int res; if (a) res = b; else res = 0; /* ou a */ return res; } le résultat de a OU b est donné par : fonction OU (Booléen a, b) : Booléen variables Booléen res; début si a = VRAI alors res := VRAI; sinon si b = VRAI alors res := VRAI; sinon res := FAUX; finsi finsi OU := res; fin Constatant que dans le cas où a vaut FAUX, le résultat est b, le programme peut se simplifier. Voici une traduction en C de cette version simplifiée . int OU(int a, int b) { int res; if (a) res = 1; else res = b; return res; } Enfin, la fonction NON permet de délivrer le contraire du Booléen passé en paramètre. Si a est VRAI, on rend FAUX, sinon on rend VRAI :

Page 32: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 32 Pierre Tellier

fonction NON (Booléen a) : Booléen variables Booléen res; début si a = VRAI alors res := FAUX; sinon res := VRAI; finsi NON := res; fin int NON(int a) { int res; if (a) res = 0; else res = 1; return res; }

Quelques exemples portant sur la conditionnelle Calcul du temps de parcours À partir de l'heure de départ et de l'heure d'arrivée, on cherche à déterminer le temps écoulé entre ces deux instants, sans passer par une conversion en secondes. Il ne s'agit ni plus ni moins que d'une soustraction en base 60, comme le montre l'exemple suivant : heure de départ : 10h 40m 30s, heure d'arrivée : 12h 30m 20s. Le temps écoulé est : 12h 30m 20s - 10h 40m 30s 1h 49m 50s Voyons comment ce résultat a été obtenu. On commence d'abord par les secondes. Si le nombre de secondes du second temps est plus important que celui du premier, alors on peut directement effectuer la soustraction. Dans le cas contraire, on retire une minute du second temps pour la convertir en 60 secondes qu'on ajoute ainsi : 12h 30-1=29m 20+60=80s - 10h 40m 30s On obtient alors le nombre résultant de secondes : 80-30=50. On recommence ensuite de manière identique avec les minutes : 29 étant inférieur à 40, on retire 60 minutes sur les 12h pour les ajouter aux minutes : 12-1=11h 29+60=89m 20+60=80s - 10h 40m 30s Le nombre de minutes résultant est alors égal à 89-40, soit 49 minutes, et le nombre d'heures est égal à 11-10, soit 1 heure : 11h 89m 80s - 10h 40m 30s 1h 49m 50s L'algorithme, solution du problème, est la simple généralisation de cette recette de cuisine à des nombres quelconques. Un temps étant constitué de 3 informations, nous écrivons un algorithme qui manipule des triplets : fonction durée(entier h1, m1, s1, h2, m2, s2) : (entier, entier, entier) variables entier h, m, s; début si s2 < s1 alors s2 := s2+60; m2 := m2-1; finsi /* retenue */ s = s2 - s1; si m2 < m1 alors m2 := m2+60; h2 := h2-1; finsi /* retenue */ m := m2 - m1; h := h2 - h1; durée := (h, m, s); fin On peut reprocher à cet algorithme les affectations qui portent sur des paramètres. Pour être parfaitement rigoureux, on devrait employer des variables intermédiaires :

Page 33: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 33 Pierre Tellier

fonction durée(entier h1, m1, s1, h2, m2, s2) : (entier, entier, entier) variables entier h, m, s, hh2, mm2, ss2; début hh2 := h2; mm2 := m2; ss2 := s2; si ss2 < s1 alors ss2 := ss2+60; mm2 := mm2-1; finsi /* retenue */ s = ss2 - s1; si mm2 < m1 alors mm2 := mm2+60; hh2 := hh2-1; finsi /* retenue */ m := mm2 - m1; h := hh2 - h1; durée := (h, m, s); fin La traduction en C passe par la création d'un type Horaire : typedef struct { int h, m, s; } Horaire; Horaire duree(Horaire h1, Horaire h2) { Horaire res, tmp; tmp = h2; if (tmp.s < h1.s) { tmp.s += 60; tmp.m--;} /* retenue */ res.s = tmp.s – h1.s; if (tmp.m < h1.m) { tmp.m += 60; tmp.h--;} /* retenue */ res.m = tmp.m - m1; res.h = tmp.h - h1; return res; } Soient deux temps t1=(h1,m1,s1) et t2=(h2,m2,s2), la différence t=t2-t1 en utilisant des paramètres modifiables s'obtient par : void duree(int h1,int m1,int s1,int h2,int m2,int s2,int *h,int *m,int *s) { if (s2 < s1) { s2 += 60; m2--;} /* retenue */ *s = s2 - s1; if (m2 < m1) { m2 += 60; h2--;} /* retenue */ *m = m2 - m1; *h = h2 - h1; } et un exemple d'utilisation de cette fonction est : void tempsEcoule(void) { int h1, m1, s1, h2, m2, s2, h, m, s; printf("Entrez temps 1 (h m s) : "); scanf("%d %d %d", &h1,&m1,&s1); printf("Entrez temps 2 (h m s) : "); scanf("%d %d %d", &h2,&m2,&s2); duree(h1, m1, s1, h2, m2, s2, &h, &m, &s); printf("temps écoulé : %d h, %d min et %d sec\n", h, m, s); }

Calcul simplifié de l'impôt sur le revenu Le calcul de l'impôt sur le revenu dépend du revenu et du nombre de parts. Dans cette approche, nous nous limitons au cas des salariés qui bénéficient d'un premier abattement de 20%. Toujours par souci de simplicité, nous optons pour la formule de la déduction forfaitaire de 10% des frais professionnels. L'étape suivante consiste à calculer le quotient familial, qui permet de déterminer la tranche d'imposition. Le quotient familial est en quelque sorte le revenu par personne. En fait c'est le revenu après abattement divisé par le nombre de parts fiscales. A partir de ce quotient qf, on obtient la formule qui permet de calculer l'impôt en fonction du revenu R, arrondi à la dizaine de Francs inférieure, et du nombre de parts N :

Quotient familial qf Formule

Page 34: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 34 Pierre Tellier

qf <= 25610 0 25610 < qf <= 50380 0.105 * R - 2689.05 * N 50380 < qf <= 88670 0.240 * R - 9490.35 * N 88670 < qf <= 143580 0.33 * R – 17470.65 * N 143580 < qf <= 233620 0.43 * R – 31828.65 * N 233620 < qf <= 288100 0.48 * R – 43509.65 * N 288100 < qf 0.54 * R - 60795.65 * N

Pour les revenus les plus modestes, on applique une décote supplémentaire : si l'impôt précédemment calculé est inférieur à 3350 FRF, la décote est égale la différence entre 3350 et ce montant calculé. Cette décote est ensuite retranchée de l'impôt. Nouveauté, on retranche à cette nouvelle valeur de l'impôt la valeur avant décote divisée par 2 pour avoir la décote définitive. Enfin, si l'impôt à payer est inférieur à 400 FRF, il n'est pas recouvrable. L’algorithme de la version basique est : fonction calculeTranche(réel qf) : (réel, réel) variables réel c, v ; début si qf <= 25610. alors c := 0.; v := 0.; sinon si qf <= 50380. alors c := 0.105; v := 2689.05; sinon si qf <= 88670. alors c := 0.24; v := 9490.35; sinon si qf <= 143580. alors c := 0.33; v := 17470.65; sinon si qf <= 233620. alors c := 0.43; v := 31828.65; sinon si qf <= 288100. alors c := 0.48; v := 43509.65; sinon c = 0.54; v = 60795.65; finsi(6) calculeTranche := (c ,v) ; fin fonction impôt(réel revenus, nbparts) : entier variables entier i, ib ; reel ri, R, n, qf, décote, c, v; début ri := revenus*0.72; R := arrondi_à_10_Francs_Près(ri); qf := R/nbparts; (c,v) := calculeTranche(qf); ib := réelVersEntier(R * c - nbparts * v); si ib<3260 alors décote := 3260-ib; i := ib-décote; décote := i – ib/2; i := ib – décote; sinon i := ib; finsi si i < 400 alors i=0; finsi impôt := i; fin Un programme C réalisant ce calcul peut être : int impot(float revenus, float nbparts) { int i, ib; float ri, R, n, qf, decote, c, v; ri = revenus*0.72; /* arrondi a la dizaine inférieure */ R = (float)((int)(ri/10.0)*10); qf = R/nbparts; if (qf <= 25610.) { c = 0.; v = 0.;} else if (qf <= 50380.) { c = 0.105; v = 2689.05;} else if (qf <= 88670.) { c = 0.24; v = 9490.35;}

Page 35: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 35 Pierre Tellier

else if (qf <= 143580.) { c = 0.33; v = 17470.65;} else if (qf <= 233620.) { c = 0.43; v = 31828.65;} else if (qf <= 288100.) { c = 0.48; v = 43509.65;} else { c = 0.54; v = 60795.65;} ib = (int)(R * c - nbparts * v); if (ib<3260) { decote = 3260-ib; i = ib-decote; if (i < 400) i=0; } else i = ib; return i; } main() { float tune, parts; printf("Entrez votre revenu (en Francs) : "); scanf("%f", &tune); printf("Entrez votre nombre de parts : "); scanf("%f", &parts); printf("Impôt à payer : %d Francs", impot(tune, parts)); } L’approche que nous avons présentée présente de nombreuses simplifications par rapport à la réalité. En effet, la plupart des abattements, réductions, déductions et autres avantages possibles sont plafonnés. Ainsi l’abattement de 10% est limité à 78950 FRF, celui de 20% à 157.900 F. Notez que si l’on opte pour les frais réels, il n’y a pas de plafond dans ce cas. D’autre part, l’avantage dû à l’application du quotient familial est lui aussi limité : il ne peut en aucun cas dépasser 12440 FRF multiplié par le nombre de demi-parts qui s’ajoutent à 1 pour un célibataire, ou à 2 pour un couple. Il faut donc calculer le quotient familial dans les 2 cas, calculer et appliquer le barème en conséquence. La différence entre ces valeurs constitue l'avantage, qu'on limite au plafonnement, avant de le retirer à la valeur la plus défavorable. Enfin, dans la version présentée, l’utilisateur doit lui même préciser le nombre de parts auquel il à droit. Dans la réalité, il est calculé, en fonction de la situation familiale, du nombre de personnes à charge, et de certaines circonstances particulières.

Énumération des cas Syntaxe Lorsque les conditions expriment une égalité stricte, on peut utiliser une autre syntaxe qui permet de décrire les cas un par un. Cette structure de contrôle est en fait une sorte d'aiguillage qui assure l'exclusivité des cas, permet de grouper plusieurs cas entre eux, et dispose d'un cas (optionnel) regroupant tous ceux qui ne sont pas décrits explicitement. Voici l'exemple d'une fonction et de ses variantes pour obtenir le nombre de jours d'un mois donné (vous noterez que l'année est aussi une donnée du problème, puisqu'elle influe sur la durée du mois de février). fonction nbJoursDuMois(entier m, a) : entier variables entier nbj; début cas selon m 1: nbj := 31; 2: si bissextile(a) alors nbj := 29; sinon nbj := 28; finsi 3: nbj := 31; 4: nbj := 30; 5: nbj := 31; 6: nbj := 30; 7: nbj := 31; 8: nbj := 31; 9: nbj := 30; 10: nbj := 31; 11: nbj := 30;

Page 36: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 36 Pierre Tellier

12: nbj := 31; autres : sortirSurErreur("nbJoursDuMois"); fcas nbJoursDuMois := nbj; fin En C chaque traitement doit se terminer par l'instruction break, sans quoi les traitements suivants (correspondant à d'autres cas) sont aussi exécutés : int nbJoursDuMois(int m, int a) { int nbj; switch(m) { case 1: nbj=31; break; case 2: if (bissextile(a)) nbj=29; else nbj=28; break; case 3: nbj=31; break; case 4: nbj=30; break; case 5: nbj=31; break; case 6: nbj=30; break; case 7: nbj=31; break; case 8: nbj=31; break; case 9: nbj=30; break; case 10: nbj=31; break; case 11: nbj=30; break; case 12: nbj=31; break; /* facultatif */ default : sortirSurErreur("nbJoursDuMois"); } return nbj; } Lorsque plusieurs cas conduisent au même traitement, ils peuvent être regroupés : fonction nbJoursDuMois(entier m, a) : entier variables entier nbj; début cas selon m 1, 3, 5, 7, 8, 10, 12: nbj := 31; 2: si bissextile(a) alors nbj := 29; sinon nbj := 28; finsi 4, 6, 9, 11: nbj := 30; fcas nbJoursDuMois := nbj; fin On peut aussi en C regrouper les cas, en omettant judicieusement certaines instructions break. int nbJoursDuMois(int m,int a) { int nbj; switch(m) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: nbj=31; break; case 2: if (bissextile(a)) nbj=29; else nbj=28; break; case 4: case 6: case 9: case 11: nbj=30; break; default: sortirSurErreur("nbJoursDuMois"); }

Page 37: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 37 Pierre Tellier

return nbj; }

Calcul de la position d'un jour dans l'année : quantième De nombreuses applications ne manipulent pas directement les dates, mais des numéros de jours, comptés à partir d'une date de référence. L'intérêt de procéder ainsi permet de s'affranchir des problèmes de gestion de dates, en particulier celui bien connu sous le nom de bug de l'an 2000. Pour calculer la position d'un jour dans l'année, il faut compter le nombre de jours qui le séparent du premier jour de l'année, c'est-à-dire faire la somme des durées des mois entièrement écoulés et des jours écoulés depuis le début du mois en cours. On a donc besoin d'une fonction qui délivre le nombre de jours de chaque mois, et comme ce nombre varie selon les années bissextiles, on a aussi besoin d'une fonction permettant de déterminer si une année est bissextile. Une année est bissextile si elle est multiple de 4, mais ceci n'est pas vrai pour les années multiples de 100 sauf pour celles multiples de 400 : fonction bissextile(entier a) : Booléen début bissextile := ((a mod 400 = 0) ou ((a mod 4 = 0) et (a mod 100 <> 0))); fin int bissextile(int a) { return ((a%400==0)||((a%4==0)&&(a%100!=0))); } La fonction qui délivre le nombre de jours d'un mois donné teste tous les mois un par un et retourne la durée correspondante utilise cette fonction. On peut l'écrire à l'aide de la structure de contrôle si ... alors ... sinon ... finsi ou de celle du traitement par cas (voir plus haut), qui est bien plus adaptée à cet exemple. fonction nbJoursDuMois(entier m, a) : entier variables entier nbj; début si m=1 alors nbj:=31; sinon si m=2 alors si (bissextile(a)) alors nbj:=29; sinon nbj:=28; finsi sinon si m=3 alors nbj:=31; sinon si m=4 alors nbj:=30; sinon si m=5 alors nbj:=31; sinon si m=6 alors nbj:=30; sinon si m=7 alors nbj:=31; sinon si m=8 alors nbj:=31; sinon si m=9 alors nbj:=30; sinon si m=10 alors nbj:=31; sinon si m=11 alors nbj:=30; sinon si m=12 alors nbj:=31; finsi(12) /* équivalent à 12 finsi consécutifs */ nbJoursDuMois := nbj; fin int nbJoursDuMois(int m,int a) { int nbj; if (m==1) nbj=31; else if (m==2) if (bissextile(a)) nbj=29; else nbj=28; else if (m==3) nbj=31; else if (m==4) nbj=30; else if (m==5) nbj=31; else if (m==6) nbj=30; else if (m==7) nbj=31; else if (m==8) nbj=31; else if (m==9) nbj=30; else if (m==10) nbj=31; else if (m==11) nbj=30;

Page 38: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 38 Pierre Tellier

else if (m==12) nbj=31; return nbj; } On peut bien sûr aussi regrouper les cas qui donnent des résultats identiques : fonction nbJoursDuMois(entier m, a) : entier variables entier nbj; début si ((m=1)ou(m=3)ou(m=5)ou(m=7)ou(m=8)ou(m=10)ou(m=12)) alors nbj := 31; sinon si (m=2) alors si (bissextile(a)) alors nbj := 29; sinon nbj := 28; finsi sinon si ((m=4)ou(m=6)ou(m=9)ou(m=11)) alors nbj := 30; finsi(3) nbJoursDuMois := nbj; fin int nbJoursDuMois(int m,int a) { int nbj; if ((m==1)||(m==3)||(m==5)||(m==7)||(m==8)||(m==10)||(m==12)) nbj=31; else if (m==2) if (bissextile(a)) nbj=29; else nbj=28; else if ((m==4)||(m==6)||(m==9)||(m==11)) nbj=30; return nbj; } Pour obtenir rapidement la somme des durées des mois complètement écoulés, on n'a pas d'autre choix que d'utiliser des cas non exclusifs. En C on peut s'appuyer sur la syntaxe du switch : fonction nbJoursDuMois(entier m, a) : entier variables entier nbj init 0; début si m>=11 alors nbj := nbj + 30; finsi si m>=10 alors nbj := nbj + 31; finsi si m>= 9 alors nbj := nbj + 30; finsi si m>= 8 alors nbj := nbj + 31; finsi si m>= 7 alors nbj := nbj + 31; finsi si m>= 6 alors nbj := nbj + 30; finsi si m>= 5 alors nbj := nbj + 31; finsi si m>= 4 alors nbj := nbj + 30; finsi si m>= 3 alors nbj := nbj + 31; finsi si m>= 2 alors si bissextile(a) nbj:=nbj+29; sinon nbj:=nbj+28; finsi finsi si m>= 1 alors nbj := nbj + 31; finsi nbJoursDuMois := nbj; fin int nbJoursMoisEcoules(int m,int a) { int nbj=0; switch(m) { case 11: nbj+=30; case 10: nbj+=31; case 9: nbj+=30; case 8: nbj+=31; case 7: nbj+=31; case 6: nbj+=30; case 5: nbj+=31; case 4: nbj+=30; case 3: nbj+=31; case 2: if (bissextile(a)) nbj+=29; else nbj+=28; case 1: nbj+=31;

Page 39: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 39 Pierre Tellier

} return nbj; } Enfin, la position du jour dans l'année est obtenue par : int jourDansAnnee(int j, int m, int a) { return j+nbJoursMoisEcoules(m-1, a); } qu'on peut exécuter de la manière suivante : main() { /* 29 septembre 1999 */ printf("%d\n",jourDansAnnee(29, 9, 1999)); /* affiche 272 */ }

Relevé de notes

Intersection de 2 segments de droite

Interpolation linéaire, extrapolation, splines

Page 40: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 40 Pierre Tellier

Itération et récursivité

Itérations et conditions d'arrêt Pour exécuter plusieurs fois de suite les programmes que nous avons réalisés, nous n'avions que la possibilité de relancer le programme. On imagine facilement que procéder de la sorte, pour éditer l'ensemble des factures des abonnés au téléphone, ou pour traiter les salaires de tous les collaborateurs d'une entreprise de grande taille, peut devenir vite fastidieux. Il existe un outil algorithmique, disponible dans tous les langages évolués, qui permet de ré-exécuter certains traitements, soit un nombre de fois déterminé, soit tant qu'une condition est vérifiée. Cette action permettant de ré-exécuter une partie du programme s'appelle l'itération. Elle permet d'ordonner la ré-exécution d'un calcul, grâce à des structures de contrôle de l'exécution du style : tant que (condition) faire traitement fait répéter traitement jusque (condition) faire pour compteur depuis début jusque fin traitement fait qui correspondent aux schémas d'exécution illustrés par la figure suivante :

La dernière forme de l’itération est la plus employée lorsqu’on connaît à l’avance le nombre de traitements à effectuer, et est la plus adaptée pour exprimer : faire N fois traitement fait. Le langage C distingue aussi ces 3 types d'itérations, mais cette distinction n'est pas très rigoureuse, car il est presque toujours possible d'employer indifféremment l'une des 3 : while (condition) {traitement} do {traitement} while (condition); for (initialisation; condition; post-traitement) {traitement} La première de ces structures est généralement employée lorsque éventuellement aucun traitement ne doit être effectué, la deuxième en revanche s'adresse aux cas où au moins un traitement doit être effectué, tandis que la troisième est plus spécialement utilisée lorsque le nombre de traitements à effectuer est connu. Les parenthèses autour de traitement peuvent être omises dans le cas où il est réduit à une seule instruction.

Page 41: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 41 Pierre Tellier

faire N fois

faire un trait le longueur N avec des "-" Pour éviter de compter les N tirets et d'écrire des instructions comme afficher("---------------");, ce qui est impossible si on ne connaît pas à l'avance la longueur du trait à dessiner, on a intérêt à répéter autant de fois que nécessaire l'action d'afficher un tiret : afficher("-");. procédure trait(entier N) variables entier i; début faire pour i depuis 1 jusque N // (pas 1) afficher("-"); finpour fin On verra par la suite qu'on aura besoin de la même fonction, mais affichant d'autres caractères. On écrit alors tout de suite une fonction, qui a comme paramètre le caractère à afficher. procédure trait(caractère c, entier lg) variables entier i; début faire pour i depuis 1 jusque lg afficher(c); finpour fin

faire M traits de longueur N, toujours avec des "-" Pour afficher M traits, on utilise la fonction écrite ci dessus, ce qui permet d'éviter d'avoir des boucles imbriquées. L'expérience montre que les boucles imbriquées sont génératrices d'erreurs. Cela est souvent dû à de malencontreux copier/coller, entraînant des conflits entre les variables de contrôle des boucles. Dans l'exemple suivant, il n'y a pas d'interférence entre les 2 variables i. procédure lignesDeTirets(entier M, N) variables entier i; début faire pour i depuis 1 jusque M trait('-',N); afficherln(); finpour fin faire un tableau d'1 ligne et de N colonnes en HTML On rappelle la syntaxe des tableaux en HTML :

• <table> … </table> : début et fin de table • <tr> … </tr> : début et fin de rangée (ligne) • <tr> … </td> : début et fin de donnée (case)

A la main, un tableau de 3 cases est obtenu par : <table> <tr> <td> </td> <td> </td> </tr> </table> On pourrait obtenir ce code avec un algorithme très "manuel" : afficher("<table> "); afficher(" <tr> "); afficher(" <td> </td> "); afficher(" <td> </td> "); afficher(" </tr> "); afficher("</table> "); L'algorithme qui crée ce même tableau, et tous ceux quelle que soit leur nombre de cases est :

Page 42: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 42 Pierre Tellier

procédure table1dHTML(entier nbCases) variables

entier i; début

afficher ("<table> <tr> "); faire pour i depuis 1 jusque nbCases

afficher ("<td> </td>"); finpour afficher ("</tr> </table>");

fin

faire un triangle de hauteur N avec des "*" * ** *** **** ***** ****** ******* A chaque ligne, on ajoute 1 étoile, en partant de 1 à la première ligne. Autre façon de voir les choses, le nombre d'étoiles par ligne correspond au numéro de lignes. procédure triangle(entier N) variables entier i; début faire pour i depuis 1 jusque N trait('*', i); afficherln(); finpour fin faire une pyramide de hauteur N avec des "*" * *** ***** ******* ********* *********** ************* Cela est plus compliqué que l'exemple précédent. D'une part le nombre d'étoiles augmente de 2 à chaque ligne, tout en étant parti de 1. On peut en déduire qu'au bout de N lignes, le nombre d'étoile, donc la taille de la base de la pyramide est 1 + (N-1)*2. On a alors facilement le nombre de blancs de part et d'autre de la ième ligne de la pyramide : c'est la largeur totale (1 + (N-1)*2), moins le nombre d'étoiles de la ligne courante (1 + 2(i-1)), le tout divisé par 2, pour centrer les étoiles. procédure pyramide(entier N) variables entier i; début faire pour i depuis 1 jusque N trait(' ',N-i);trait('*',2*i-1);trait(' ',N-i); finpour fin

Dessiner un carré avec des '#' ###### # # # # # # # # ######

Page 43: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 43 Pierre Tellier

Un carré de côté N (N>=2) est constitué de 2 lignes de N symboles #, entre lesquelles il y a N-2 lignes. Chacune de ces lignes débute par un #, se poursuit par bN-2 espaces et se termine par un autre #. procédure carré(entier N) variables entier i; début si N >= 2 alors trait('#', N); faire pour i depuis 1 jusque N-2 afficher('#'); trait(' ',N-2); afficher('#'); finpour trait('#', N); finsi fin

Dessiner un tableau avec des "-" et des "|" |---------|---------|---------| | | | | |---------|---------|---------| | | | | |---------|---------|---------| Paramètres : nombres de lignes NL, nombres de colonnes NC, hauteur des lignes TL et largeur des colonnes TC. Il y a quelques difficultés supplémentaires par rapport aux exemples précédents, ce qui impose de bien décomposer le problème en sous-problèmes assez faciles à résoudre avec les procédures que nous venons d'écrire. Il faut aussi penser au fait qu'il y a une ligne horizontale de plus que le nombre de colonnes du tableau, ainsi qu'une colonne de "|" en plus du nombre de colonnes du tableau. On s'attaque d'abord à la fonction de dessin de lignes horizontales. On doit dessiner NC fois un "|" suivi d'un trait, puis conclure par un dernier "|". Les lignes du tableau entre les bordures sont construites sur le modèle des bordures horizontales, il suffit de remplacer les tirets par des espaces. On paramètre donc la fonction de dessin de ligne en conséquence. procédure dessineLigne(entier NC, TC ; car c) variables entier i; début si NC > 0 alors afficher("|"); finsi faire pour i depuis 1 jusque NC trait(c,TC); afficher("|"); finpour fin Chaque ligne du tableau est constituée de plusieurs lignes horizontales autres que les séparateurs. procédure dessineLignesTableau(entier TL,NC,TC) variables entier i; début faire pour i depuis 1 jusque TL dessineLigne(NC, TC, ' '); finpour fin Enfin, on peut dessiner le tableau en entier : procédure affTab(entier NL, NC, TL, TC) variables entier i; début si NL > 0 alors dessineLigne(NC,TC,'-'); finsi faire pour i depuis 1 jusque NL dessineLignesTableau(TL,NC,TC); dessineLigne(NC,TC,'-'); finpour fin

Page 44: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 44 Pierre Tellier

Dessiner un tableau de M lignes et N colonnes en HTML En HTML, cela est beaucoup plus simple, puisqu'on n'a pas à dessiner soi-même les traits horizontaux et verticaux. Il suffit d'ordonner les cases et les lignes horizontales. procédure uneLigneTabHTML(entier NC) variables entier i; début

afficher ("<tr> "); faire pour i depuis 1 jusque NC

afficher ("<td> </td>"); finpour afficher ("</tr>");

fin procédure affTabHTML(entier NL, NC) variables entier i; début

afficher ("<table>"); faire pour i depuis 1 jusque NL

uneLigneTabHTML(NC); finpour afficher ("</table>");

fin

Répétition d'un calcul Après avoir réalisé un programme qui effectue une addition de deux nombres, nous nous intéressons à une application qui permet d'enchaîner plusieurs additions, sans avoir à relancer le programme. Il faut toujours prévoir un critère d’arrêt. Dans notre premier exemple, nous allons interroger l’utilisateur, pour savoir s’il souhaite effectuer un nouveau calcul, ou quitter le programme. variables entier a, b ; chaîne(3) réponse ; début répéter afficher("entrez 2 valeurs : "); a := lire(); b := lire(); afficher("la somme vaut", a+b); afficher("voulez vous faire un autre calcul (oui ou non) ?"); réponse := lire(); jusque (réponse = "non"); fin En C la manipulation des chaînes de caractères est un peu complexe, c'est pourquoi dans cet exemple nous nous contenterons d'une réponse de type entier, 1 voulant dire "oui", et 0 "non". Attention, en C, on exprime la condition pour ré-exécuter le calcul, et non la condition d'arrêt. main() { int a, b, rep ; do { printf("entrez 2 valeurs : "); scanf("%d %d", &a, &b); printf("la somme vaut %d", a+b); printf("voulez vous faire un autre calcul (oui ou non) ?"); scanf("%d", &rep); } while (rep == 1); } On peut améliorer l'algorithme précédent en forçant l'utilisateur à répondre par "oui" ou par "non", toute autre réponse étant considérée comme incorrecte et provoquant la demande d'une nouvelle réponse.

Page 45: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 45 Pierre Tellier

variables entier a, b ; chaîne(3) réponse ; début répéter afficher("entrez 2 valeurs : "); a := lire(); b := lire(); afficher("la somme vaut", a+b); répéter afficher("voulez vous faire un autre calcul (oui ou non) ?"); réponse := lire(); jusque (réponse = "oui" OU réponse = "non"); jusque (réponse = "non"); fin Notez la façon dont les instructions sont indentées pour faciliter la lecture en fonction des imbrications des structures de contrôle. Il est néanmoins vivement recommandé d'éviter les itérations imbriquées, car la lecture des algorithmes devient difficile, et leur mise au point compliquée, c'est pourquoi on préférera cette dernière écriture, à base de fonctions, qui ne laisse apparaître qu'une itération à la fois. Pour réduire encore les risques d’erreur, on évite les ET et les OU dans les conditions de boucle. fonction continuer():chaîne(3) variables chaîne(3) réponse; Booléen arrêt ; début répéter afficher("voulez vous continuer (oui ou non) ?"); réponse := lire();` arrêt := réponse = "oui" OU réponse = "non" ; jusque arrêt; continuer := réponse; fin variables entier a, b ; chaîne(3) réponse ; début répéter afficher("entrez 2 valeurs : "); a := lire(); b := lire(); afficher("la somme vaut", a+b); réponse := continuer(); jusque (réponse = "non"); fin dont voici la traduction en C, en gardant la même convention que précédemment : int continuer(void) {

int rep, arret; do {

printf("voulez vous continuer (oui ou non) ?"); scanf("%d", &rep); arret = (rep==1) || (rep==0); }

while (!arret) ; // attention jusque arret while (!arret) return rep; }

Page 46: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 46 Pierre Tellier

main() { int a, b, rep ; do { printf("entrez 2 valeurs : "); scanf("%d %d", &a, &b); printf("la somme vaut %d", a+b); rep = continuer(); } while (rep == 1); }

Itérations et conditions d'arrêt Prenons comme exemple celui du calcul de la moyenne d'une suite de nombres entrés au clavier. Il faut choisir une convention pour déterminer comment se termine la suite de valeurs. Nous en adopterons plusieurs, pour bien montrer qu'il n'y a souvent plusieurs stratégies pour arriver à ses fins. Dans le cas général, l'algorithme se présente ainsi : 1. initialiser la somme à 0, ainsi que le nombre de valeurs lues ; 2. attendre la saisie d'un nombre ; 3. si on détecte la fin, calculer la moyenne et délivrer le résultat, sinon passer à la suite ; 4. ajouter ce nombre à la somme et augmenter le nombre de valeurs lues ; 5. attendre un nouveau nombre, c'est-à-dire refaire depuis 2 (calculer la somme des nombres suivants). Ce traitement s'exprime plus facilement à l'aide de structures de contrôle de l'itération : 1. initialiser la somme à 0, ainsi que le nombre de valeurs lues ; 2. attendre un nombre ; 3. tant que on n'a pas détecté la fin de fichier : 4. ajouter ce nombre à la somme, et augmenter le nombre de valeurs lues ; 5. attendre un nouveau nombre ; 6. refaire = retourner en 3 : tant que 7. calculer la moyenne et délivrer le résultat. Dans les différentes variantes, il faudra s'assurer que le calcul de la moyenne est toujours correct, et gérer le fait qu'éventuellement la série de nombres est vide, ce qui peut entraîner une division par 0.

Moyenne d'une série de nombres positifs Une façon de terminer une série de nombres positifs est d'introduire un nombre négatif en guise de marqueur de fin. Une moyenne de nombres positifs étant toujours positive, nous délivrons une valeur négative en cas d'erreur. L'algorithme général se spécialise en : fonction moyenneSerie1():réel variables réel somme init 0.0, moyenne, nbLu; entier nbValeurs init 0; début afficher("entrez un nombre (négatif pour arrêter) "); nbLu := lire(); tant que nbLu >= 0.0 faire somme := somme + nbLu; nbValeurs := nbValeurs + 1;

afficher("entrez un nombre (négatif pour arrêter) "); nbLu := lire(); fintantque si nbValeurs > 0 alors moyenne := somme / entierVersRéel(nbValeurs); sinon moyenne := -1.0; finsi moyenneSerie1 := moyenne; fin et se traduit en C par :

Page 47: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 47 Pierre Tellier

float moyenneSerie1(void) { float somme = 0.0, moyenne, nbLu; int nbValeurs = 0; printf("entrez un nombre (négatif pour arrêter) "); scanf("%f", &nbLu); while (nbLu >= 0.0) { somme += nbLu; nbValeurs++; printf("entrez un nombre (négatif pour arrêter) "); scanf("%f", &nbLu); } if (nbValeurs > 0) moyenne = somme / (float)nbValeurs; else moyenne = -1.0; return moyenne; }

Moyenne d'une série de nombres quelconques Il devient difficile d'introduire un critère d'arrêt qui porte sur la valeur des nombres saisis. C'est pourquoi nous montrons 2 façons possibles de procéder. Dans un premier cas, l'utilisateur est invité à préciser après chaque saisie de nombre son intention de continuer (nous donnons 2 variantes, dont une avec saisie obligatoire d'au moins un nombre). Dans le deuxième cas, nous considérerons que la saisie de la suite de nombres est terminée lorsque l'utilisateur entre le caractère fin de fichier CTRL+d, (voir chapitre sur les fichiers) qui signifie qu'il n'y a plus de données en entrée. Le résultat est obtenu en effectuant la somme de tous les nombres, puis en divisant cette somme par le nombre de valeurs saisies. Avec demande de confirmation, la méthode ressemble beaucoup à notre premier exemple, celui des additions successives : fonction moyenneSerie2():réel variables réel somme init 0.0, nbLu; entier nbValeurs init 0; chaîne(3) réponse; début répéter

afficher("entrez un nombre "); nbLu := lire(); somme := somme + nbLu; nbValeurs := nbValeurs + 1; réponse := continuer(); jusque (réponse = non); moyenneSerie2 := somme / entierVersRéel(nbValeurs); fin float moyenneSerie2(void) { float somme = 0.0, nbLu; int nbValeurs = 0, rep; do { printf("entrez un nombre "); scanf("%f", &nbLu); somme += nbLu; nbValeurs++; rep = continuer(); } while (rep == 1);

return somme / (float)nbValeurs; } Dans la plupart des langages, la fin de la saisie peut être déterminée par un caractère spécial (fin de fichier), ou lorsque une saisie non conforme est effectuée (du texte à la place d'un nombre par exemple). C'est le moyen le plus efficace de procéder, car il évite de confirmer son intention de poursuivre après chaque saisie, ce qui est pénible lorsque le nombre de valeurs à saisir est important. En C, la fin de la saisie est signifiée par le caractère de fin de fichier CTRL+d.

Page 48: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 48 Pierre Tellier

La version qui suit n'impose pas qu'au moins une valeur ait été saisie, c'est pourquoi il faudrait prévoir de délivrer un deuxième résultat permettant de savoir si la moyenne calculée est significative ou non. fonction moyenneSerie3():réel variables réel somme init 0.0, moyenne, nbLu; entier nbValeurs init 0; début

afficher("entrez un nombre ou CTRL+d"); nbLu := lire(); tant que estUnNombre(nbLu) faire somme := somme + nbLu; nbValeurs := nbValeurs + 1; afficher("entrez un autre nombre ou CTRL+d");

nbLu := lire(); fintantque si nbValeurs > 0 alors moyenne := somme / entierVersRéel(nbValeurs); sinon moyenne := 0.0; finsi moyenneSerie1 := moyenne; fin Le programme C correspondant est quasiment identique et s'écrit : float moyenneSerie3(void) { float somme = 0.0, moyenne, nbLu; int nbValeurs = 0; printf("entrez un nombre (ou CTRL+d) "); scanf("%f",&nbLu); while (!feof(stdin)) /* tant que non fin de fichier */ { somme += nbLu; nbValeursLues++ ; printf("entrez un autre nombre (ou CTRL+d) "); scanf("%f",&nbLu); } if (nbValeursLues > 0) moyenne = somme / (float)nbValeursLues; else moyenne = 0.0; return moyenne; }

Itérations et compteurs De nombreux problèmes puisent leur solution dans la mise en place d'un compteur, c'est-à-dire une variable qui va prendre des valeurs entières consécutives (souvent en commençant par 0 ou 1) au cours du calcul.

Génération de nombres entiers : compteurs Pour nous familiariser avec ce concept de compteur, nous allons voir comment générer (afficher) une série de nombres entre 1 et une valeur fixée, d'abord dans le sens croissant, puis dans le sens inverse. Il s'agit apparemment d'un exercice extrêmement simple, mais l'expérience montre qu'il n'est pas si facile que ça d'obtenir un algorithme qui s'arrête au moment voulu. Pensez à faire tourner "à la main" vos algorithmes, avant de passer au codage. procédure produireEntiers(entier n) variables entier compteur init 1; début tant que (compteur <= n) faire afficher(compteur); compteur := compteur + 1; fintantque fin

Page 49: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 49 Pierre Tellier

Il est facile de passer à la version qui produit les entiers dans l'autre ordre : procédure produireEntiers(entier n) variables entier compteur init n; début tant que (compteur >= 1) faire afficher(compteur); compteur := compteur - 1; fintantque fin Toutefois il est beaucoup plus naturel d'utiliser l'itération de type faire pour, bien plus adaptée à cela : procédure produireEntiers(entier n) variables entier compteur init n; début faire pour compteur depuis 1 jusque n faire afficher(compteur); finpour fin Le pas par défaut vaut 1. Dans le cas de l'affichage décroissant, il est nécessaire de préciser qu'il vaut –1 : procédure produireEntiers(entier n) variables entier compteur init n; début faire pour compteur depuis n jusque 1 pas -1 faire afficher(compteur); finpour fin En C, la tradition veut qu'on appelle les compteurs i, j, k etc. On obtient les traductions des algorithmes précédents : void produireEntiers(int n) { int i=1; while(i <= n) { printf("%d ",i); i++; /* i=i+1 ou encore i+=1 */ } } void produireEntiers(int n) { int i=n; while(i >= 1) { printf("%d ",i); i--; /* i=i-1 ou encore i-=1 */ } } void produireEntiers(int n) { int i; for(i=1;i<=n;i++) printf("%d ",i); } Explication : i=1 est l'initialisation, effectuée avant la boucle d'itération. i<=n est la condition qui si elle est vraie, permet de faire une nouvelle itération. Enfin i++ augmente de 1 la valeur de i avant de refaire une nouvelle itération (si possible). Note : il n'y a qu'une seule instruction dans cette itération, c'est pourquoi nous avons omis les accolades. Ainsi, produireEntiers(6); provoque l'affichage de : 1 2 3 4 5 6.

Page 50: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 50 Pierre Tellier

void produireEntiers(int n) { int i; for(i=n;i>=1;i--) printf("%d ",i); }

Application : produit factoriel Le produit factoriel est défini ainsi : n! = n*(n-1)*(n-2)*...*2*1. 0!=1. Par exemple, 5! = 5*4*3*2*1 = 120. On voit que pour calculer la factorielle d'un nombre n, il est nécessaire de produire tous les entiers depuis 1 jusqu'à n, et de les multiplier entre eux. En programmation impérative, les multiplications sont effectuées au fur et à mesure de la production des nombres. Cela peut être réalisé à partir des structures déjà présentées. On obtient donc pour la factorielle l'algorithme suivant : fonction factorielle(entier n):entier variables entier f init 1, compteur; début faire pour compteur depuis 1 jusque n f := f * compteur; finpour fin int factorielle(int n) { int i, f=1; for(i=1;i<=n;i++) f=f*i; return f; }

Récursivité Il existe une autre façon de raisonner, jugée plus naturelle par les uns, plus élégante par d'autres ou encore moins efficace (à cause du passage de paramètres et des appels de fonctions) par une troisième catégorie de programmeurs. Il s'agit de la récursivité, qui s'appuie sur la définition par récurrence du problème. La particularité de la récursivité, c'est que pour résoudre le problème dans le cas général, on suppose qu'on est capable de le résoudre dans un cas un peu plus simple. Ceci est conforme à notre démarche descendante, qui consiste à décomposer un problème en sous-problèmes. Ici le sous-problème est le même que le problème initial, mais dans un cas un peu plus simple. Ce raisonnement entraîne l'écriture de fonctions qui contiennent des appels à la fonction qu'on est en train de définir. Cette approche s'impose bien sûr non seulement lorsque la nature du problème est intrinsèquement récursive, mais peut aussi être utilisée pour résoudre la plupart des problèmes et des calculs ayant un caractère répétitif. Comme nous le verrons par la suite, elle permet de résoudre certains problèmes horriblement complexes, voire impossible à résoudre uniquement à l'aide d'itérations. Outre cet avantage, l'écriture de fonctions récursives reste très proche de la spécification des problèmes, ce qui réduit de manière conséquente les risques d'erreur, en particulier au niveau de l'écriture des conditions d'arrêt des itérations. Pour bien réussir une fonction récursive, il faut : exhiber la formule de récurrence dans le cas général (ex : n! = n*(n-1)!) ; trouver les cas particuliers, pour lesquels la formule générale ne s'applique pas et qui sont les cas d'arrêt (ex 0! = 1) ; vérifier qu'il existe une fonction strictement décroissante associée aux paramètres de la fonction récursive. Cette fonction, appelée fonction de simplification, signifie que l'on est effectivement en train de résoudre le problème en le divisant en sous-problèmes plus simples que le problème initial. Dans le cas de la factorielle, cette fonction associe au paramètre n la valeur n-1, elle est strictement décroissante sur les entiers naturels.

Page 51: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 51 Pierre Tellier

Cas naturels Dans tout manuel de programmation, on trouve le calcul de la factorielle pour illustrer la récursivité. Le produit factoriel est défini de la manière suivante : n! = n*(n-1)*(n-2)*...*2*1 0! = 1 Pour exhiber la récurrence, on reformule la factorielle ainsi : n! = n * (n-1)*(n-2)*...*2*1 = n*(n-1)! 0! = 1. La factorielle de n est donc bien définie à l'aide la factorielle de n-1, et possède un cas d'arrêt. On a alors presque automatiquement l'algorithme récursif : fonction factorielle(entier n) : entier variables

entier f; début si n=0 alors f := 1; sinon f := n * factorielle(n-1); finsi factorielle := f; fin int factorielle(int n) { int f; if (n==0) f = 1; else f = n * factorielle(n-1); return f; }

Compteurs Nous avons déjà vu comment produire des nombres entiers en utilisant les structures de contrôle itératives, et les difficultés à mettre au point les critères d'arrêt. Avec la récursivité, cela est rendu beaucoup plus simple, à l'aide du raisonnement suivant. Pour produire les n entiers entre 1 et n, c'est facile si n est inférieur à 1, car il n'y a rien à faire. Si au contraire n est plus grand ou égal à 1 alors on produit n, et il faut encore produire tous les entiers entre n-1 et 1, ce qui nous ramène au problème initial, mais avec n-1 au lieu de n. L'algorithme est alors : procédure produireEntiers(entier n) début si n >= 1 alors afficher(n); produireEntiers(n-1); finsi fin void produireEntiers(int n) { if (n >= 1) { printf("%d ", n); produireEntiers(n-1); } } L'affichage des nombres obtenu par l'exécution de ce programme se produit dans l'ordre décroissant. Pour obtenir l'ordre inverse, il suffit d'inverser le traitement et l'appel récursif, ce qui signifie : pour afficher tous les entiers jusque n, on suppose qu'on a su les afficher tous jusque n-1, il ne reste plus alors qu'à afficher n : procédure produireEntiers(entier n) début si n >= 1 alors produireEntiers(n-1); afficher(n); finsi fin

Page 52: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 52 Pierre Tellier

Moyenne d'une série de nombres Pour calculer la moyenne d'une série de nombre saisie au clavier, la récursivité n'est pas la méthode qui s'impose de manière intuitive. Ce qui fait la difficulté dans ce cas, c'est que 2 informations sont utiles à ce calcul, à savoir la somme cumulée des valeurs saisies, ainsi que leur nombre. Le principe qui préside à ce calcul est de la sorte : si aucun nombre n'est saisi, la somme vaut 0, et le nombre de valeurs aussi. Au contraire, si une valeur est introduite, elle s'ajoute aux valeurs qui seront encore saisies, et augmente de 1 leur nombre. On n'a donc pas directement une fonction qui calcule la moyenne, mais une fonction qui délivre les éléments permettant cette évaluation : fonction sommeEtNombre() : (réel : entier) variables réel s, nbLu, resteS; entier n, resteN; début afficher ("entrez un nombre ou code de fin "); nbLu := lire(); si estUnNombre(nbLu) alors (resteS, resteN) := sommeEtNombre(); s := nbLu + resteS; n := 1 + resteN; sinon s := 0.0; n := 0; finsi sommeEtNombre := (s, n); fin fonction moyenneSerie() : réel variables réel s, m; entier n; début (s, n) := sommeEtNombre(); si n > 0 alors m := s / entierVersRéel(n); sinon m := 0.0; finsi moyenneSerie := m; fin typedef struct { float s; int n; } CoupleSN; CoupleSN sommeEtNombre(void) { CoupleSN c, resteC;

float nbLu; printf("entrez un nombre ou CTRL+d "); scanf("%d", &nbLu); if (!feof(stdin)) { resteC = sommeEtNombre(); c.s = nbLu + resteC.s; c.n = 1 + resteC.n; else { c.s = 0.0; c.n = 0; } return c; }

Page 53: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 53 Pierre Tellier

float moyenneSerie(void) { CoupleSN c; float m; c = sommeEtNombre(); if (c.n > 0) m = s / float(n); else m = 0.0; return m; }

Répétition d'un calcul Pour exécuter un calcul à répétition, avec demande de confirmation après chaque étape, le schéma naturel est plutôt itératif. On peut toutefois aussi s'en sortir en utilisant la récursivité, comme nous allons le montrer. On désire offrir à un utilisateur la possibilité d'exécuter autant de fois qu'il le souhaite un programme qui réalise l'addition entre 2 nombres. À chaque fois, l'utilisateur sera interrogé sur son désir de faire un calcul ou de quitter l'application. S'il désire continuer, son calcul est exécuté, et la fonction récursive est appelée à nouveau. Dans le cas contraire, rien n'est exécuté. On profite de cet exemple pour écrire la fonction continuer de façon récursive. procédure plusieursAdditions() variables entier a, b ; chaine(3) réponse ; début réponse := continuer(); si réponse = "oui" alors afficher("entrez 2 valeurs : "); a := lire(); b := lire(); afficher("la somme vaut", a+b); plusieursAdditions(); finsi fin void plusieursAdditions(void) { int a, b, rep; rep = continuer(); if (rep == 1) { printf("entrez 2 valeurs : "); scanf("%d %d", &a, &b); printf("la somme vaut %d", a+b); plusieursAdditions(); } } fonction continuer():chaine(3) variables chaine(3) réponse, res; début afficher("voulez vous continuer (oui ou non) ?"); réponse := lire();` si (réponse = "oui" OU réponse = "non") alors res := réponse; sinon res := continuer(); finsi

continuer := res; fin dont voici la traduction en C, en gardant la même convention que précédemment : int continuer(void) {

int rep;

Page 54: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 54 Pierre Tellier

printf("voulez vous continuer (oui ou non) ?"); scanf("%d", &rep);

if ((rep <> 1) && (rep <> 0)) rep = continuer(); return rep; }

Itération et récursivité Exemples simples

Somme des premiers entiers Pour illustrer ces nouveaux concepts prenons un autre exemple, celui de la somme des n premiers nombres entiers. L’approche itérative repose sur le raisonnement suivant : on va produire successivement tous les entiers, et chacun leur tour les additionner à une variable somme, initialement nulle, qui va donc successivement prendre toutes les valeurs intermédiaires avant de contenir le résultat. fonction sommePremiersEntiers(entier n) : entier variables entier i, s init 0; début faire pour i depuis 1 jusque n s := s+i; finpour sommePremiersEntiers := s ; fin int sommePremiersEntiers(int n) { int i, s=0; for(i=1;i<=n;i++) s=s+i; /* ou s += i */ return s; } Le raisonnement récursif est différent. Sachant que la somme des n premiers entiers, c'est n plus la somme des n-1 premiers entiers, on obtient : fonction sommePremiersEntiers(entier n) : entier variables entier s; début si i = 0 alors s := 0 ;

sinon s := n + sommePremiersEntiers(n-1) ; finsi

sommePremiersEntiers := s ; fin int sommePremiersEntiers(int n) { int s; if (n==0) s = 0; else s = n+sommePremiersEntiers(n-1); return s; } Vérifions que cette fonction est correcte : formule de récurrence dans le cas général : somme des n premiers entiers = n + somme des (n-1) premiers entiers ; cas particulier : somme des 0 premiers entiers = 0 ; la fonction de simplification s(n)=n-1 qui associe (n-1) à n est strictement décroissante. Dans ce cas bien précis, il existe une formule remarquable : int sommePremiersEntiers(int n)

Page 55: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 55 Pierre Tellier

{ return (n*(n+1))/2; }

Modulo Nous introduisons un autre problème arithmétique, celui du modulo. C'est le reste de la division entière. Il nous faut reprendre nos vieux souvenirs du temps de l'école sur la division entière de a par b. Si a est plus petit que b, le résultat est 0 et le reste est a. Sinon, a est plus grand ou égal à b, et dans ce cas, le résultat est au moins 1. On peut même dire que le résultat est 1 plus le résultat de la division entière de (a-b) divisé par b. fonction modulo(entier a,b) : entier variables entier m; début si a<b alors m := a ; sinon m := modulo(a-b, b) ; finsi modulo := m ; fin int modulo(int a, int b) { int m; if (a<b) m = a; else m = modulo(a-b,b); return m; } En itératif, on trouve le reste lorsqu'à force de retirer b à a, on trouvera une valeur inférieure à b. fonction modulo(entier a,b) : entier variables entier aa int a, bb init b ; début

tant que (aa>=bb) faire aa := aa-bb ;

fintantque modulo := aa ; fin int modulo(int a, int b) { int aa=a,bb=b; /* rmq : on pourrait utiliser a et b */ while(aa>=bb) aa -= bb; /* aa = aa - bb */ return aa; }

Division entière Étant donnés 2 nombres entiers a et b, la division entière de a par b peut se résoudre en isolant les différents cas : cas où a est inférieur à b : le résultat de la division est nul ; cas où a est supérieur ou égal à b : dans ce cas le résultat est au moins 1, c'est même exactement 1 plus le résultat de la division entière de a-b par b. fonction division(entier a, b) : entier variables entier res ; début si a<b alors res:=0; sinon res:=1+division(a-b, b); finsi division := res;

Page 56: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 56 Pierre Tellier

fin int division(int a, int b) { int res; if (a<b) res=0; else res=1+division(a-b, b); return res; } Sous forme itérative : tant que cela est possible, on ajoute 1 au résultat et on retranche b à a. fonction division(entier a, b) : entier variables entier res init 0, aa init a; début tant que aa>=b faire res := res+1 ; aa := aa-b ; fintantque division := res; fin int division(int a, int b) { int res=0, aa=a; while(aa>=b) { res = res + 1; aa = aa - b; } return res; } ou, sous une forme presque illisible, mais qui plait tant à certains … int division(int a, int b) { int res=0, aa=a+b; while((aa-=b)>b) res++; return res; }

Nombres premiers Un nombre premier p est un nombre qui n'admet que 2 diviseurs : 1 et lui-même. Autrement dit, si il existe un entier entre 2 et p-1 qui divise p, p n'est pas premier. Un nombre n est diviseur d'un nombre p si p modulo n vaut 0 (modulo = % en C). Pour résoudre ce problème on va supposer que le nombre p premier et faire varier n entre 2 et p-1. fonction estUnNombrePremier(entier p) : Booléen variables entier n ; Booléen premier init VRAI; début faire pour n depuis 2 jusque p-1 si p mod n = 0 alors premier := FAUX ; finsi finpour estUnNombrePremier := premier ; fin int estUnNombrePremier(int p) { int n, premier=1; /* 1 veut dire vrai */ for(n=2;n<=p-1;n++) if (p % n == 0) premier = 0; /* 0 veut dire faux */ return premier; }

Page 57: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 57 Pierre Tellier

On peut faire un certain nombre de remarques : dès qu'un nombre est trouvé non premier, ce n'est pas la peine de continuer, il n'a aucune chance de redevenir premier. D'autre part, ce n'est pas la peine de tester tous les entiers jusqu'à p-1, on peut s'arrêter plus tôt (par exemple, ce n'est pas la peine d'aller plus loin que l'entier n tel que n2>p) et de plus, on pourrait se contenter de ne tester que les diviseurs eux-mêmes premiers. Il y a plusieurs façons de terminer prématurément la boucle. La première consiste à utiliser une structure du type tant que, avec une condition qui intègre le fait que le nombre est encore premier : tant que n2 est inférieur à p et que p est premier ... fonction estUnNombrePremier(entier p) : Booléen variables entier n init 2 ; Booléen premier init VRAI; début tant que n*n<=p ET premier faire si p mod n = 0 alors premier := FAUX ; finsi n := n+1 ; fintantque estUnNombrePremier := premier ; fin Il est vivement conseillé d’utiliser des conditions de sortie le plus simple possible, et qui sont mises à jour dans la boucle. Pour cela on peut utiliser une variable unique de contrôle de l’itération. Personnellement, je préfère nettement la version qui suit, par rapport à la précédente : fonction estUnNombrePremier(entier p) : Booléen variables entier n init 2 ; Booléen premier init VRAI, finBoucle init FAUX ; début si n*n > p alors finBoucle := VRAI ;finsi tant que NON finBoucle faire si p mod n = 0 alors finBoucle := VRAI ;premier := FAUX ; sinon n := n+1 ; si n*n > p alors finBoucle := VRAI ;finsi finsi fintantque estUnNombrePremier := premier ; fin int estUnNombrePremier(int p) { int n=2, finboucle=0, premier=1; /* 0 veut dire faux, 1 vrai */ while (!finboucle) { if (p % n == 0) {premier = 0; finboucle = 1 ; } else if (n*n>p) finboucle = 1 ; else n++; } return premier; } L'autre manière d’écrire la même chose en C est beaucoup plus brutale, et applicable aussi bien dans une boucle while que dans une boucle for. On utilise le type Booléen Booleen défini précédemment. Booleen estUnNombrePremier(int p) { int n; Booleen premier=VRAI; for(n=2;n*n<p;n++) if (p % n == 0) { premier = FAUX; break; /* on quitte la boucle ! */ } return premier; }

Page 58: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 58 Pierre Tellier

L'instruction break que nous venons d'introduire permet de rendre bien des services en provoquant une sortie prématurée de la boucle. Cette instruction est très pratique dans les cas où il y a de nombreuses conditions de sortie de boucle. Toutefois, il est recommandé tant qu'on n'est pas encore très familier de la programmation de ne pas en abuser. C'est encore plus vrai pour l'instruction continue qui provoque un retour au début de l'itération, sans exécuter les instructions qui suivent continue. Pour éviter toutes les difficultés à exprimer correctement et efficacement une condition d'arrêt, on adopte un raisonnement par récurrence : un nombre n est premier si n vaut 2; un nombre n n'est pas premier si un nombre d (initialement n-1) est un diviseur de n, ou si on est capable de trouver un diviseur de n entre 2 et d-1 un nombre n est premier si d vaut 1 On a alors l'algorithme, et le programme suivants : fonction premier(entier n, d) : Booléen variables Booléen res; début si n=2 alors res := VRAI; sinon si d=1 alors res := VRAI; sinon si n mod d = 0 alors res := FAUX sinon res := premier(n, d-1); finsi finsi finsi premier := res; fin variables Booléen preums; entier n; début n := lire();

preums := premier(n, n-1); fin int premier(int n, int d) { int res; if ((n==2)(d==1)) res = 1; else if (n%d == 0) res = 0; else res = premier(n, d-1); return res; }

Calcul de l'intégrale par la somme de Riemann Voici un autre exemple tiré des mathématiques, qui illustre le passage d'une fonction en paramètre. Il s'agit du calcul approché par la somme de Riemann de l'intégrale entre a et b d'une fonction à une variable. La formule de Riemann donne la valeur de l'intégrale, lorsque que n tend vers l'infini.

∑∑=

=→∞→∞

−+−=−+−=n

i

n

inn n

abiafnab

nabiafn

abR1

1

0))((lim))((lim

Approche itérative On l'obtient par simple transcription de la formule de Riemann : en effet, il suffit de produire toutes les valeurs de i, de calculer la valeur du terme pour chaque valeur de i, et d’en faire la somme. fonction intègre((réel) :réel f ; réel a, b ; entier n) variables réel res init 0, delta ; entier i ; début faire pour i depuis 0 jusque n-1

Page 59: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 59 Pierre Tellier

res := res + f(a + entierVersRéel(i) * delta) ; finpour intègre := res * delta ; fin float integre(float (*f)(float), float a, float b, int n) { float R=0.0, res, delta ; int i; delta = (b-a) / (float) n ; for (i = 0 ; i < n ; i ++) R += f(a + (float) i * delta) ; res = R * delta ; return res ; }

Approche récursive L'approche récursive, dans ce cas précis, est moins intuitive que l'approche itérative. Elle s'appuie sur le raisonnement suivant : l'intégrale de la fonction f entre a et b divisée en n intervalles, c'est 0 si n vaut 0, sinon c'est f(a) multiplié par la longueur d'un intervalle (b-a)/n plus l'intégrale de f entre a+(b-a)/n et b, divisée en n-1 intervalles. fonction intègre((réel) :réel f ; réel a, b ; entier n) variables réel res init 0, delta ; début si n=0 alors res := 0.0 ; sinon delta := (b-a)/entierVersRéel(n) ; res := delta * f(a) +intègre(f, a+delta, b, n-1) ; finsi intègre := res ; fin On remarque que dans cette version, on perd la factorisation du terme delta (qu’on recalcule à chaque appel récursif, alors qu’il vaut toujours la même chose). Il suffit de faire une fonction qui calcule la somme des termes, et de multiplier par delta une fois la somme évaluée. fonction intègre0((réel) :réel f ; réel a, b, delta ; entier n) variables réel res init 0 ; début si n=0 alors res := 0.0 ; sinon res := f(a) +intègre0(f, a+delta, b, delta, n-1) ; finsi intègre0 := res ; fin fonction intègre((réel) :réel f ; réel a, b ; entier n) variables réel res, delta ; début

si n=0 alors res := 0.0 ; sinon delta := (b-a)/entierVersRéel(n) ; res := delta * intègre0(f, a+delta, b, delta, n-1) ; finsi intègre := res ; fin float integre0(float (*f)(float), float a, float b, float delta, int n) {

Page 60: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 60 Pierre Tellier

float res; if (n == 0) res = 0.0; else res = f(a) + integre0(f, a+delta, b, delta, n-1); return res ; } float integre(float (*f)(float), float a, float b, int n) { float res, delta ; if (n == 0) res = 0.0; else { delta = (b-a) / (float) n ; res = delta * integre0(f, a+delta, b, delta, n-1); } return res ; }

Utilisation Voici un exemple d'utilisation de la fonction integre. Pour que le résultat soit précis, il faut utiliser une grande valeur de n. float f1(float x) { return 3.0*x*x-1.0 ; } main() { float S; S = integre(f1, 2.0, 4.0, 100000); }

Tableau d'amortissement du remboursement d'un emprunt Le montant des mensualités de remboursement d'un emprunt est calculé grâce à la formule suivante :

1)1()1(

** −++

= N

N

II

ICM

dans laquelle C est le capital emprunté, I le taux d'intérêt (mensuel) et N la durée de l'emprunt (en mois). Chaque mois, une part de cette mensualité est consacrée aux intérêts sur le capital emprunté, le reste servant à rembourser une partie du capital, faisant ainsi diminuer le capital restant dû. La part d'intérêt de chaque mensualité a donc tendance à décroître, le capital restant dû diminuant à chaque mensualité remboursée. Dans notre tableau, nous allons faire apparaître sur chaque ligne le numéro de mois, la mensualité (constante), la part d'intérêts, la part de capital, le capital restant dû ainsi que le cumul des intérêts versés depuis le début du remboursement de l'emprunt. La seule difficulté de ce programme consiste à bien comprendre le mécanisme du remboursement de l'emprunt : à chaque remboursement, on paie les intérêts sur le capital dont on a disposé pendant un mois, et en quelque sorte on refait un nouvel emprunt mais d'une somme inférieure. C'est d'ailleurs ce raisonnement qui permet de résoudre aussi ce problème par la récursivité. Dans l’approche récursive, on peut se permettre de passer en paramètre le montant de la mensualité, car étant toutes identiques, il serait dommage de la recalculer pour chaque mois. fonction mensualité(réel C, I ; entier N) variables réel tmp ; début tmp := (1.0 + i)**entierVersRéel(N) ; mensualité := C*I*tmp/(tmp-1.0) ; fin procédure amortissement0(réel C0, C, I; entier N, numéro; réel montant, cumul) variables réel intérêts ; début si N > 0 alors

Page 61: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 61 Pierre Tellier

intérêts := C*I ; afficher(numéro, montant, intérêts, C0, C-(montant-intérêts), cumul+intérêts) ; amortissement0(C0, C-(montant-intérêts), I, N-1, numéro+1, montant, cumul+intérêts) ; finsi fin procédure amortissement(réel C, I; entier N) variables réel montant ; début montant := mensualité(C,I,N) ; afficher("Mois Montant Intérêts Capital Capital dû Intérêts cumulés"); amortissement0(C,C,I,N,1,montant,0.0); fin Pour faciliter l'utilisation de la fonction, le taux d'intérêts est annule et exprimé en pourcentages. Nous donnons en C la traduction de l'approche itérative. #include <stdio.h> #include <math.h> float mensualite(float C, float I, int N) { float tmp; tmp = (float)pow((double)(1. + I), (double)N); return C * I * tmp / (tmp - 1.); } void amortissement(float C, float I, int N) { float interets, capitalRemb, m, capitalDu, cumul=0.0; int i; I = I/(12. * 100.); N = N * 12; m = mensualite(C, I, N); capitalDu = C; printf("Mois\tMensualité\tIntérêts\tCapital\t\tCapital dû\tIntérêts Cumulés\n"); for(i=1;i<=N;i++) { interets = capitalDu * I; cumul += interets; capitalRemb = m - interets; capitalDu = capitalDu - capitalRemb;

printf("%d\t%f\t%f\t%f\t%f\t%f\n",i,m,interets,capitalRemb,capitalDu,cumul); } } main() { amortissement(10000., 8., 1); } L'appel de procédure amortissement(10000.0, 8., 1); permet de calculer le tableau d'amortissement d'un emprunt de 10000 FRF sur 1 an à 8%, qui se présente ainsi : Mois Mensualité Intérêts Capital Capital dû Intérêts cumulés 1 869.884705 66.666668 803.218036 9196.781964 66.666672 2 869.884705 61.311881 808.572823 8388.209140 127.978554 3 869.884705 55.921396 813.963309 7574.245831 183.899948 4 869.884705 50.494973 819.389731 6754.856100 234.394928 5 869.884705 45.032375 824.852329 5930.003771 279.427307 6 869.884705 39.533359 830.351345 5099.652426 318.960663 7 869.884705 33.997684 835.887021 4263.765405 352.958344 8 869.884705 28.425103 841.459601 3422.305803 381.383453

Page 62: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 62 Pierre Tellier

9 869.884705 22.815373 847.069332 2575.236471 404.198822 10 869.884705 17.168244 852.716461 1722.520010 421.367065 11 869.884705 11.483467 858.401238 864.118773 432.850525 12 869.884705 5.760792 864.123913 -0.005140 438.611328 Si on remplace les instructions de formatage "%f" par "%10.2f", on force l'affichage des nombres sur 10 caractères, dont 2 chiffres après la virgule. On obtient alors l'affichage : Mois Mensualité Intérêts Capital Capital dû Intérêts cumulés 1 869.88 66.67 803.22 9196.78 66.67 2 869.88 61.31 808.57 8388.21 127.98 3 869.88 55.92 813.96 7574.25 183.90 4 869.88 50.49 819.39 6754.86 234.39 5 869.88 45.03 824.85 5930.00 279.43 6 869.88 39.53 830.35 5099.65 318.96 7 869.88 34.00 835.89 4263.77 352.96 8 869.88 28.43 841.46 3422.31 381.38 9 869.88 22.82 847.07 2575.24 404.20 10 869.88 17.17 852.72 1722.52 421.37 11 869.88 11.48 858.40 864.12 432.85 12 869.88 5.76 864.12 -0.01 438.61

Calcul du jour dans l'année : quantième Nous avons au chapitre précédent écrit la fonction nbJoursMois permettant d'obtenir le nombre de jour de chaque mois. Pour obtenir la position d'un jour dans l'année, il suffit d'additionner au numéro de jour la somme des durées de tous les mois entièrement écoulés entre le premier jour de l'année et la date en question. Ainsi, la position dans l'année du 3 mars est 3 plus la somme des durées de janvier et de février. Pour une date en décembre, cela est plus fastidieux car il faut additionner les durées des mois de janvier à novembre. Pour rendre ce traitement plus automatique, il est plus aisé de calculer la somme des durées de tous les mois, depuis le premier jusqu'à celui qui précède la date courante. On peut avoir les 2 approches, récursive ou itérative. Dans cette dernière, il suffit alors d'un compteur pour parcourir tous ces mois. fonction positionJourDansAnnée(entier j, m, a) : entier variables entier pos; début si m=1 alors res := j; sinon res := nbJoursMois(m-1,a) + positionJourDansAnnée(j, m-1, a) ; finsi positionJourDansAnnée := res; fin En C, et en itératif : int positionJourDansAnnee(int j, int m, int a) { int pos = j, mois; for(mois = 1; mois < m-1; mois++) pos += nbJoursMois(mois, a); return pos; }

Récursivité croisée

Page 63: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 63 Pierre Tellier

Tableaux statiques 1D Lorsque l'on doit manipuler de nombreuses variables, il devient vite pénible de devoir les déclarer toutes une à une. Par exemple si l'on doit utiliser 20 variables entières, plutôt que de déclarer individuellement 20 variables entières entier i1, i2, i3, ..., i20, il vaut mieux déclarer un tableau de 20 nombres entiers tableau entier i[20], accessibles par i[0], i[1], i[2], ... , i[19] où les nombres 0 à 19 sont les indices (valeurs entières). Vous pouvez aussi déclarer les tableaux ainsi : tableau réel tab[3..12]. Dans ce cas les éléments sont accessibles à travers les indices de 3 à 12. Manipuler les tableaux ne semble donc pas plus difficile que de manipuler des variables simples. C'est souvent le cas si la taille des tableaux coïncide avec le nombre d'informations utiles, et est connue à l'avance. Dans de nombreux problèmes, non seulement le nombre d'informations à manipuler est important, mais il n'est pas non plus connu avec précision au moment de l'écriture du programme, ou peut varier d'une exécution à l'autre, comme par exemple le nombre de réponses à une requête sur une base de données dépend des critères de recherche, qui sont variables. Deux stratégies sont alors envisageables. La première consiste à déclarer des tableaux, dits statiques, suffisamment grands pour répondre aux besoins des divers contextes d'exécution. Il faudra alors gérer le fait que le nombre d'informations utiles au problème peut être inférieure à la taille déclarée du tableau. Plusieurs approches sont possibles, comme gérer un compteur d'informations (approche la plus courante) ou un marqueur (comme les chaînes de caractère en C), ou encore adopter les tableaux à trous. La deuxième stratégie consiste à adapter au cours de l'exécution la taille des tableaux (dits alors dynamiques) à la dimension du problème. Nous avons déjà dit l'intérêt d'avoir des tableaux pour travailler sur un grand nombre de variables. Prenons l'exemple des tableaux de nombres entiers. Il existe plusieurs façons de les initialiser dès la déclaration. tableau entier tab0[0..19]; tableau entier tab1[20]; tableau entier tab2[] = [1,2,3,4,5]; tableau entier tab3[20] = [1,2,3,4,5]; type Tab c'est tableau entier[5]; Tab tab5; Tab tab6 = [1,2,3,4,5]; En C, on a aussi plusieurs façons de faire. int tab1[20]; /* tableau de 20 entiers non initialisé */ int *tab2 = {1,2,3,4,5}; /* tableau de 5 entiers */ int tab3[] = {1,2,3,4,5}; /* tableau de 5 entiers */ int tab4[20]= {1,2,3,4,5}; /* tableau de 20 entiers partiellement initialisé */ typedef int Tab[5]; Tab tab5; Tab tab6= {1,2,3,4,5}; Il existe 2 grands cas d'utilisation des tableaux : le cas où le nombre de variables à utiliser est grand et connu et l'autre cas plus fréquent que nous illustrerons, qui est lorsque le nombre de variables est grand mais non connu précisément. Il faut toutefois connaître le nombre maximum de données qu'on peut être amené à manipuler, et on gérera à part le nombre de variables effectivement utilisées. Pour manipuler des tableaux tassés (les informations pertinentes sont regroupées au début du tableau), on a coutume de procéder de la sorte : constantes entier TAILLEMAX vaut 100 variables entier nbEntiers init 0; tableau entier tabEntiers[TAILLEMAX]; /* taille physique du tableau */ #define TAILLEMAX 100 int tabEntiers[TAILLEMAX]; /* tableau de TAILLEMAX entiers */ int nbEntiers=0; /* initialement, le tableau est vide */

Page 64: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 64 Pierre Tellier

Opérations élémentaires sur les tableaux Nous allons maintenant manipuler ce genre de tableaux à l'aide de fonctions. Comme le tableau ne contient pas sa propre taille, notre stratégie consiste à toujours passer en plus en paramètre la taille effective, ainsi que sa taille physique, lorsque la fonction est susceptible d'augmenter le nombre d'informations. L'illustration de ces manipulations est faite sur des tableaux d'entiers : • Ajout d'un élément à un tableau ; • Initialisation à partir de valeurs lues au clavier ; • Affichage des éléments d'un tableau ; • Recopie d'un tableau dans un autre ; • Extraction d'un sous-tableau ; • Recherche de l'existence d'un élément dans un tableau ; • Recherche de la position d'un élément dans un tableau ; • Suppression d'un élément d'un tableau ; • Insertion d'un élément dans un tableau ; • Recherche du plus petit élément d'un tableau ; • Recherche de l'existence d'un élément dans un tableau trié ; • Recherche de l'encadrement d'un élément dans un tableau trié; • Inversion de l'ordre des éléments d'un tableau. En C, la syntaxe du passage d'un tableau d'entiers tab en paramètre se fait de 2 manières : int *tab ou bien int tab[].

Ajout d'un élément à un tableau L'ajout d'un élément se fait à la fin du tableau, et vient occuper le premier emplacement libre, s'il y en a un évidemment. Dans ce cas, la taille effective du tableau est augmentée de 1. On écrit donc une fonction qui prend en paramètre un tableau, sa taille (maximum et effective) ainsi que l'élément à ajouter, et délivre le tableau résultant (le tableau proprement dit, et sa taille effective). fonction ajoutElentTableauEntiers(tableau entier tab[taillemax],entier taille,élément):(tableau entier, entier) variables entier nouvelleTaille; début si taille < taillemax alors tab[taille]:=élément; nouvelleTaille := taille+1; sinon nouvelleTaille := taille; finsi ajoutElentTableauEntiers := (tab, nouvelleTaille); fin En C, une fonction ne peut délivrer un tableau en résultat. En revanche, un tableau est naturellement modifiable, il suffit de rendre comme résultat la taille effective mise à jour: int ajoutElementTableauEntiers(int tab[],int taille,int taillemax,int element) { int nouvtaille=taille; if (taille < taillemax) { tab[taille]=element; nouvtaille++; } return nouvtaille; } /* appel de la fonction */ nbEntiers=ajoutElementTableauEntiers(tabEntiers,nbEntiers,TAILLEMAX,5);

Initialisation d'un tableau Tout d'abord une fonction qui permet d'initialiser un tableau à partir de valeurs lues au clavier (ou dans un fichier) et délivre le nombre de valeurs effectivement lues. La lecture se termine sur le caractère fin de fichier ou lorsque le nombre maximal de données pouvant être contenues dans le tableau est atteint. int initTableauEntiers(int tab[], int taillemax)

Page 65: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 65 Pierre Tellier

{ int nbValeursLues=0, val, lectureFinie=0 ; while(!lectureFinie) { if (nbValeursLues>=taillemax) lectureFinie = 1 ; else { printf("Entrez une valeur (ou CTRL+D ou CTRL+Z) "); scanf("%d",&val); if (feof(stdin)) lectureFinie=1; else tab[nbValeursLues++]=val; } } return nbValeursLues; } /* exemple d’appel de la fonction */ nbEntiers=initTableauEntiers(tabEntiers,TAILLEMAX); En pratique, les tableaux sont souvent initialisés par lecture directe dans des fichiers, comme nous le verrons au chapitre qui leur est consacré. Ils sont aussi souvent le résultat de requêtes sur des bases de données. Pour être parfaitement rigoureuse, cette fonction devrait utiliser la fonction d’ajout vue juste avant.

Affichage des éléments d'un tableau Un tableau de taille n contient des éléments auxquels on peut accéder en faisant varier un indice de 0 à n-1. Il suffit donc d'être capable de faire prendre à une variable entière toutes les valeurs d'indice souhaitées. En raisonnant par récurrence, la démarche est autre. Si le tableau n'est pas vide, on affiche son premier élément, d'indice 0, il ne reste plus alors qu'à traiter le sous-tableau qui commence au deuxième élément. procédure afficheTableauEntiers(tableau entier tab[taillemax]; entier taille) début si taille>0 alors afficher(tab[0]); afficheTableauEntiers(tab[1..taille-1],taille-1); finsi fin procédure afficheTableauEntiers(tableau entier tab[taillemax]; entier taille) variables entier i; début faire pour i depuis 0 jusque n-1 afficher(tab[i]); finpour fin void afficheTableauEntiers(int tab[], int n) { int i; for(i=0;i<n;i++) printf("%d ",tab[i]); }

Recopie d'un tableau dans un autre On peut aussi parler d'affectation entre tableaux. En phase d'écriture d'algorithmes, nous sommes parfaitement en droit de supposer que cette fonctionnalité est disponible et nous écrivons tab2 := tab1 pour recopier le contenu d'un tableau tab1 dans un deuxième tableau tab2. Malheureusement, cette fonctionnalité est rarement disponible dans les langages de programmation, c'est pourquoi nous décrivons le principe de la recopie de tableau directement en langage C. Le principe est simple. Le premier élément (d'indice 0) du premier tableau est rangé en position 0 dans le second tableau, et ainsi de suite, d'où la formule générale qui consiste à ranger l'élément en i-ème position du premier tableau en i-ème position dans le second. On effectue cette opération pour tous les éléments du premier tableau, sans toutefois dépasser la taille physique du second, et on délivre le nombre de valeurs effectivement recopiées. Réfléchissez à ce qui se passe si les 2 tableaux se chevauchent (le début de tab2 est avant la fin de tab1). En fait, pour être sûr que cet algorithme fonctionne en toutes circonstances, on devrait théoriquement utiliser un tableau intermédiaire, et aussi utiliser la fonction d’ajout. int recopieTableauEntiers(int tab1[],int taille1,int tab2[],int taillemax2) { int i=0; while((i<taille1)&&(i<taillemax2)) {

Page 66: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 66 Pierre Tellier

tab2[i]=tab1[i]; i++; } return i; } #define TMAX2 50 int dest[TMAX2]; int nbEntiers2=0; /* appel de la fonction */ nbEntiers2=recopieTableauEntiers(tabEnt,nbEntiers,dest,TMAX2); /* en utilisant les fonctionnalités offertes par le langage C */ memcpy((char*)dest,(char*)tabEnt,MIN(nbEntiers,TMAX2)*sizeof(int)); /* memcpy : si les mémoires se chevauchent résultat non garanti ! */ /* À vous de contrôler que le tableau destination est suffisamment grand */

Extraction d'un sous-tableau Il peut parfois être intéressant de ne considérer qu'une partie des informations contenue dans un tableau. Dans tous nos algorithmes, nous supposerons que cette fonctionnalité est disponible. Ainsi, pour extraire le sous-tableau de tab compris entre les indices a et b, nous pouvons directement écrite tab[a..b]. En revanche, C, cette fonction n'est pas disponible. On procède alors un peu comme pour la copie de tableau, en veillant à ce que le tableau vers lequel on copie ces informations soit de taille suffisante. int recopieSousTableauEntiers(int tab1[],int a,int b,int tab2[],int taillemax2) { int i=a, j=0; while((i<=b)&&(j<taillemax2)) { tab2[j]=tab1[i]; i++; j++; } return j; } Vous remarquerez que le résultat de l’extraction n’est pas délivré par la fonction, mais seulement sa taille. Le tableau résultant, tab2, est lui passé en paramètre, car il est impossible, en C, d’écrire des fonctions avec des tableaux statiques en résultat. En réalité, ce n’est pas le tableau qui est passé en paramètre, mais l’adresse de son premier élément, c’est pourquoi il apparaît comme « modifiable ».

Recherche d'un élément dans un tableau Si le tableau est vide, aucun traitement n'est effectué: on peut conclure que l'élément n'est pas présent. Si l'élément recherché est le premier élément du tableau, on a gagné. Sinon, le résultat de la recherche est celui de la recherche sur le sous-tableau privé de son premier élément. Dans l'approche itérative, on compare chaque élément du tableau à l'élément de référence, sans dépasser le dernier élément. Dès que l'élément cherché est trouvé, il est inutile de continuer, on peut provoquer un arrêt prématuré de l'itération. fonction RechercheElémentTableauEntiers(tableau entier tab[taillemax]; entier taille, élément) : Booléen variables Booléen res; début si taille=0 alors res:=FAUX; sinon si tab[0]=élément alors res:=VRAI; sinon res:= RechercheElémentTableauEntiers(tab[1..taillemax-1], taille-1, élément); finsi finsi RechercheElémentTableauEntiers := res; fin int RechercheElementTableauEntiers(int tab[], int taille, int element) { int i=0,trouve=0,finBoucle=0; while (!finBoucle) { if (i>=taille)finBoucle=1; else if (tab[i]==element) { trouve=1; finBoucle=1;

Page 67: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 67 Pierre Tellier

} else i++; } return trouve; }

Recherche de la position d'un élément dans un tableau Avant la recherche, l'élément de référence est réputé absent. Le résultat de la recherche est mis par convention à -1, une valeur d'indice impossible. On le compare ensuite à tous les éléments du tableau, en faisant varier l'indice de 0 jusqu'à (taille-1). Si la comparaison révèle une égalité, il suffit de relever la valeur de l'indice, et d'indiquer que la recherche est terminée. Si un même élément est présent plusieurs fois, seule la position de sa première occurrence est délivrée. En récursif, le raisonnement est le même que celui pour la recherche simple. Si le premier appel à la fonction réussit, on doit délivrer 0. Si c'est le deuxième, le résultat est 1, et ainsi de suite. Pour mettre en œuvre cela, la fonction doit avoir un paramètre compteur, incrémenté de 1 à chaque appel. fonction positionElémentTableauEntiers(tableau entier tab[taillemax]; entier taille, élément, compteur) : entier variables entier res; début si taille=0 alors res:=-1; sinon si tab[0]=élément alors res:=compteur; sinon res:=positionElémentTableauEntiers(tab[1..taillemax], taille-1,élément,compteur+1); finsi finsi positionElémentTableauEntiers:=res; fin L'appel de cette fonction se fait avec la valeur 0 pour le paramètre compteur. La version itérative, en C donne : /* position de la première occurrence d'un élément dans un tableau */ /* convention : retourne -1 si l'élément est absent */ int positionElementTableauEntiers(int tab[], int taille, int element) { int i=0,position=-1,lafin=0; while(!lafin) { if (i>=taille) lafin=1; else { if (tab[i]==element) { position=i; lafin=1; } else i++; } } return position; }

Recherche de la position d'un élément dans un tableau trié Nous présentons ici une amélioration de la fonction de recherche pour des données triées. Une première approche pourrait consister à interrompre la recherche dès que l'élément courant a une valeur supérieure à la valeur recherchée. Mais mieux que cela, il y a l'approche dichotomique : on compare l'élément recherché à celui du milieu du tableau. S'il y a égalité, on a terminé, sinon on peut déterminer dans quel sous-tableau il est utile de chercher, et on n'a plus que la moitié des données à parcourir. On itère ce processus jusqu'à ce qu'on trouve la donnée ou jusqu'à ce qu'on échoue sur un tableau de 1 seul élément : fonction rechercheDicho(tableau entier tab[taillemax]; entier borne_inf, borne_sup, élément) : entier variables entier milieu, res; début si borne_inf > borne_sup alors res:= -1; sinon milieu := (borne_inf + borne_sup) / 2; si tab[milieu] = élément alors res := milieu;

Page 68: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 68 Pierre Tellier

sinonsi tab[milieu] > élément alors res := rechercheDicho(tab, borne_inf, milieu-1, élément); sinon res := rechercheDicho(tab, milieu+1, borne_sup, élément); finsi finsi rechercheDicho := res; fin int rechercheDicho(int tab[], int n, int element) { int debut=0, fin=n-1, pos=-1, milieu=n/2; while((debut<=fin) && (tab[milieu]!=element)) { if (tab[milieu]==element) pos = milieu; else { if (tab[milieu]>element) fin = milieu-1; else debut = milieu+1; milieu = (debut+fin)/2; } } return pos; }

Suppression d'un élément d'un tableau On cherche à retirer du tableau un élément désigné par son indice. Il faut bien sûr que cet élément fasse partie effectivement du tableau. La difficulté réside alors à combler le vide laissé par la disparition de l'élément. Pour cela on va décaler vers la gauche tous les éléments situés à des indices supérieurs à celui supprimé, c'est-à-dire qu'on va les ranger à un indice diminué de 1 par rapport à leur emplacement antérieur. On commence par traiter les éléments les plus proches de celui supprimé. On peut aussi utiliser la fonction d'extraction de sous-tableau pour réaliser le déplacement des éléments après celui qui est supprimé. fonction suppressionElémentTableauEntiers(tableau entier tab[taillemax]; entier taille, position) : (tableau entier, entier) variables entier nouvelleTaille; début si position < 0 OU position > taille-1 alors nouvelleTaille := taille; sinon nouvelleTaille := taille-1; tab[position..taille-2] := tab[position+1..taille-1]; finsi suppressionElémentTableauEntiers := (tab, nouvelleTaille); fin int suppressionElementTableauEntiers(int tab[], int taille, int position) { int i=position,nouvtaille=taille; if(position < taille) { while (i < taille-1) { tab[i]=tab[i+1]; i++; } nouvtaille--; } return nouvtaille; }

Insertion d'un élément dans un tableau Un élément ne peut être inséré que si l'indice qu'on a choisi pour cette insertion est inférieur ou égal à la taille effective du tableau. Il faut aussi que le tableau ne soit pas complètement plein pour pouvoir accueillir un nouvel élément. Une fois ces conditions vérifiées, une place doit être faite au nouvel élément. Pour cela on va décaler vers la droite tous les éléments situés à des indices supérieurs à celui supprimé, c'est-à-dire qu'on va les ranger à un indice augmenté de 1 par rapport à leur emplacement antérieur. On commence d'abord par le dernier élément du tableau.

Page 69: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 69 Pierre Tellier

fonction insertionElémentTableauEntiers(tableau entier tab[taillemax]; entier taille, position, élément) : (tableau entier, entier) variables entier nouvelleTaille; début si position < 0 OU position > taille OU taille >= taillemax alors nouvelleTaille := taille; sinon nouvelleTaille := taille+1; tab[position+1..taille] := tab[position..taille-1]; tab[position] := élément; finsi insertionElémentTableauEntiers:= (tab, nouvelleTaille); fin /* pour insérer un élément, il faut que sa position d'insertion */ /* soit inférieure ou égale à la taille effective */ int insertionElementTableauEntiers(int *tab,int taille,int position,int element) { int dernier = taille-1 ; int nouvtaille = taille; if ((taille < taillemax)&&(position<=taille)) { while (dernier >= position) { tab[dernier + 1] = tab[dernier] ; dernier--; } tab[position] = element ; nouvtaille++; } return nouvtaille ; }

Inversion de l'ordre des éléments d'un tableau L'algorithme d'inversion des éléments d'un tableau procède de la manière suivante : si le tableau contient plus d'un élément, le premier et le dernier élément sont permutés, puis on effectue l'inversion sur le sous-tableau privé de ses éléments extrêmes. Ceci est réalisé à l'aide de 2 variables qui désignent le premier et le dernier éléments et qui sont mises à jour à chaque itération, jusqu'à ce que qu'elles se croisent. fonction permuterEntiers(entier a, b) : (entier, entier) début permuterEntiers := (b, a); fin fonction inversionElémentsTableauEntiers(tableau entier tab[taillemax]; entier taille) : (tableau entier, entier) variables entier nouvelleTaille; début si taille >= 2 alors (tab[0], tab[taille-1]) := permuterEntiers(tab[0], tab[taille-1]); tab[1..taille-2] := inversionElementsTableauEntiers(tab[1..taille-2], taille-2); finsi inversionElémentsTableauEntiers := (tab, taille); fin void permuterEntiers(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } void inversionElementsTableauEntiers(int *tab, int taille) { int debut=0,dernier=taille-1 ;

Page 70: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 70 Pierre Tellier

while (dernier >= debut) permuterEntiers(&tab[debut++],&tab[dernier--]); }

Application : les ensembles Les ensembles sont une collection d'informations, sans redondance. Chaque information n'est donc présente qu'une fois au plus dans un ensemble. /* vérifie l'unicité de chaque élément */ int verifierEnsemble(int *tab, int n) { int i,j,res=1; for(i=0;i<n-1;i++) for(j=i+1;j<n;j++) if(tab[i]==tab[j]) res=0; return res; } Si un tableau contient des éléments présents plusieurs fois, on peut le réduire à un ensemble en ne gardant qu'un exemplaire de chaque valeur. Pour tous les éléments du tableau, on recherche dans la partie restante les éléments identiques. S'il y en a, l'élément courant n'est pas conservé. /* supprime les "doublons" */ int faireEnsemble(int *tab, int n) { int i,taille=0; for(i=0;i<n;i++) if (!RechercheElementTableauEntiers(tab+i+1, n-(i+1), tab[i])) taille=ajoutElementTableauEntiers(tab, taille, n, tab[i]); return taille; } L'union de 2 ensembles est définie comme l'ensemble contenant les éléments des 2 ensembles de départ. Pour conserver l'unicité, seuls les éléments du deuxième ensemble qui ne sont pas déjà dans le premier sont ajoutés. /* union d'ensembles : éléments du premier et du second pas dans le premier */ int unionEns(int *tab1, int n1, int *tab2, int n2, int *tab3, int max3) { int i,taille=0; for(i=0;i<n1;i++) taille=ajoutElementTableauEntiers(tab3, taille, max3, tab1[i]); for(i=0;i<n2;i++) if (!RechercheElementTableauEntiers(tab1, n1, tab2[i])) taille=ajoutElementTableauEntiers(tab3,taille,max3,tab2[i]); return taille; } L'intersection de deux ensembles réunit les éléments qui appartiennent à la fois au premier et au second ensemble. /* intersection d'ensembles : éléments du premier aussi dans le second */ int intersectionEns(int *tab1, int n1, int *tab2, int n2, int *tab3, int max3) { int i,taille=0; for(i=0;i<n1;i++) if (RechercheElementTableauEntiers(tab2, n2, tab1[i])) taille=ajoutElementTableauEntiers(tab3,taille,max3,tab1[i]); return taille; } Enfin, la différence consiste à retirer du premier ensemble les éléments qui appartiennent au second. En pratique, appartiennent à la différence tous les éléments du premier sauf ceux présents dans le second. /* différence d'ensembles : éléments du premier sauf ceux dans le second */ int differenceEns(int *tab1,int n1,int *tab2,int n2,int *tab3,int max3) { int i,taille=0; for(i=0;i<n1;i++)

Page 71: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 71 Pierre Tellier

if (!RechercheElementTableauEntiers(tab2, n2, tab1[i])) taille=ajoutElementTableauEntiers(tab3,taille,max3,tab1[i]); return taille; } Un exemple de programme utilisant ces fonctions : void afficheEnsemble(int *tab, int n) { int i; for(i=0;i<n;i++) printf("%d ",tab[i]); fprintf(stderr,"\n"); } main() { int tab1[N], tab2[N], tab3[N]; printf("entrez 10 valeurs dans tab1\n> "); for(i=0;i<N;i++) scanf("%d",&tab1[i]); printf("entrez 10 valeurs dans tab2\n> "); for(i=0;i<N;i++) scanf("%d",&tab2[i]); printf("tab1 est un ensemble %d\n",verifierEnsemble(tab1, N)); printf("tab2 est un ensemble %d\n",verifierEnsemble(tab2, N)); n1=faireEnsemble(tab1, N); n2=faireEnsemble(tab2, N); printf("Ensemble tab1\n "); afficheEnsemble(tab1, n1); printf("Ensemble tab2\n "); afficheEnsemble(tab2, n2); n3 = unionEns(tab1, n1, tab2, n2, tab3, N); printf("Ensemble tab1 union tab2\n"); afficheEnsemble(tab3, n3); n3 = intersectionEns(tab1, n1, tab2, n2, tab3, N); printf("Ensemble tab1 inter tab2\n"); afficheEnsemble(tab3, n3); n3 = differenceEns(tab1, n1, tab2, n2, tab3, N); printf("Ensemble tab1 moins tab2\n"); afficheEnsemble(tab3, n3); } dont voici un exemple d'exécution: entrez 10 valeurs dans tab1 > 1 2 3 4 5 6 1 2 7 8 entrez 10 valeurs dans tab2 > 1 1 1 2 5 6 7 8 9 0 tab1 est un ensemble 0 tab2 est un ensemble 0 Ensemble tab1 3 4 5 6 1 2 7 8 Ensemble tab2 1 2 5 6 7 8 9 0 Ensemble tab1 union tab2 3 4 5 6 1 2 7 8 9 0 Ensemble tab1 inter tab2 5 6 1 2 7 8 Ensemble tab1 moins tab2 3 4

Page 72: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 72 Pierre Tellier

Tri de tableaux Comme nous l'avons déjà vu, le fait de disposer de tableaux triés accélère de façon significative l'accès aux informations. Nous allons décrire plusieurs manières de trier des tableaux d'entier. Nous donnerons aussi les programmes correspondants en langage C qui permettent de manipuler ces tableaux d'entiers.

Tri naïf Une manière intuitive de trier par ordre croissant une collection d'informations peut être décrite comme suit. Si le tableau contenant ces informations est vide, on peut le considérer comme trié. Sinon, on cherche la position de son plus petit élément, afin de le permuter avec le premier élément. À l'issue de cette étape, le premier élément est le plus petit, il ne reste plus qu'à trier le sous-tableau qui commence à partir de l'élément suivant. fonction posPlusPetit(tableau entier tab[taillemax]; entier taille) : entier variables entier pos; début si taille=1 alors pos:=0; sinon pos := posPlusPetit(tab[1..taille-1], taille-1]; si tab[0] < tab[pos] alors pos := 0; finsi finsi posPlusPetit := pos; fin fonction triNaïf(tableau entier tab[taillemax]; entier taille) : (tableau entier) variables entier pos; début si taille >=2 alors pos := posPlusPetit(tab, taille); (tab[0], tab[pos]) := permuterEntiers(tab[0], tab[pos]); tab[1..taille-1] := triNaïf(tab[1..taille-1],taille-1); finsi triNaïf := tab; fin /* ----------------------------------------------------------- recherche de la position du plus petit element d'un tableau ----------------------------------------------------------- */ int posPlusPetit(int *tab, int n) { int i,pos=-1,min; if (n>0) { min = tab[0]; pos = 0; } for(i=1;i<n;i++) { if (min > tab[i]) { min = tab[i]; pos = i; } } return pos; } /* ----------------------------------------------------------- tri d'un tableau : version naïve 1 - recherche de la position du plus petit élément 2 - on échange cet élément avec celui au début du tableau 3 - on recommence, sans toucher au début qui est déjà trié ----------------------------------------------------------- */

Page 73: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 73 Pierre Tellier

void triNaif(int *tab, int n) { int i, p; for(i=0;i<n;i++) { p = posPlusPetit(tab+i,n-i); permuterEntiers(&tab[p+i], &tab[i]); } }

Tri par insertion Dans la plupart des applications, des données sont régulièrement ajoutées et retirées aux informations manipulées par les programmes. En particulier, à chaque ajout d'une donnée à des informations triées, il n'est pas utile de refaire intégralement le tri. On peut procéder par insertion triée. Cela consiste à repérer la position que doit occuper la nouvelle donnée, puis de l'insérer à l'aide de la fonction d'insertion déjà vue. La fonction de recherche de la position d'insertion balaye les éléments du tableau jusqu'à trouver le premier élément qui soit plus grand que l'élément à insérer. /* ----------------------------------------------------------- recherche de la position d'insertion d'un element ----------------------------------------------------------- */ int posInsertion(int *tab, int elem, int n) { int i, placeTrouvee=0; while ((i<n)&&(!placeTrouvee)) { if (elem > tab[i]) placeTrouvee = 1; i++; } return i; } /* ----------------------------------------------------------- insertion d'un élément en respectant le tri ----------------------------------------------------------- */ int insertionTriee(int *tab, int n, int elem) { int p, nouvtaille; p = posInsertion(tab, elem, n); nouvtaille = insertionElementTableauEntiers(tab, n, p, elem); } Pour effectuer un tri complet d'un tableau par cette méthode, il suffit de faire une insertion triée de tous les éléments du tableau.

Tri à bulle Le tri dit "à bulle" s'appelle ainsi à cause de la manière dont se déplacent les éléments dans le tableau, à la manière des bulles qui remontent à la surface : plus elles sont grosses, plus elles remontent rapidement. Le principe de cette méthode consiste à comparer les éléments 2 à 2 : chaque élément est comparé avec son voisin d'indice immédiatement supérieur, et s'il y a lieu, ils sont permutés. Ceci garantit que le plus grand élément est arrivé en dernière position du tableau, même si d'autres permutations ont aussi été effectuées pour rapprocher le tableau de son état trié (d'autres bulles ont aussi monté vers la surface, sans toutefois y parvenir). Il suffit alors de recommencer sur le tableau privé de son dernier élément. Si aucune permutation n'est effectuée, c'est que le tableau est déjà trié, on pourrait envisager de le détecter pour arrêter prématurément l'algorithme de tri. /* ----------------------------------------------------------- tri d'un tableau : version 2 (dite tri a bulle) on compare chaque element avec son voisin de droite, et si il y a lieu, on corrige l'ordre (attention a s'arrêter avant le dernier) a la fin de cette opération, le plus grand est a la fin. donc on recommence, mais sans aller jusqu'à la fin, et ceci tant qu'on a encore quelque chose a trier ----------------------------------------------------------- */

Page 74: ALGORITHMIQUE ET PROGRAMMATION EN C

Algorithmique et programmation en C Page 74 Pierre Tellier

void triBulle(int *tab, int n) { int fin, j; for(fin=n;fin>=0;fin--) for(j=0;j<fin-1;j++) if (tab[j]>tab[j+1]) permuterEntiers(&tab[j], &tab[j+1]); } Le problème de ces algorithmes est leur coût : on recherche le plus petit élément parmi les n éléments, puis parmi les n-1 restants après le premier rangement, puis parmi n-2 etc. Au total il faut effectuer n + (n-1) + .. + 1 recherches, donc le nombre d'opérations peut être considéré comme proportionnel au carré du nombre de données à trier : n + (n-1) + .. + 1 = n(n+1)/2. La stratégie pour réduire ce coût va consister à séparer le tableau en 2, d'effectuer un tri sur chacun de ces 2 tableaux, et de les fusionner ensuite. Notons que pour effectuer le tri de chacun de ces 2 sous-tableaux, on applique récursivement cette même stratégie. Une autre stratégie basée sur le même principe consiste à faire un découpage en sous-tableaux de telle sorte que dans le premier tableau tous les éléments soient inférieurs à ceux du second tableau...

Tableaux à trous Une alternative à la gestion de tableaux tassés, avec marqueur de fin ou un nombre effectif de données, est la gestion des trous. On peut utiliser des marqueurs pour indiquer quels sont les emplacements libres. Cela facilite la suppression des éléments, puisqu'il suffit d'écraser leur valeur par la valeur du marqueur. En revanche, l'ajout est plus compliqué qu'un ajout à la fin, comme nous l'avons déjà pratiqué, puisqu'il requiert la recherche préalable d'une place libre.