Prog Proc Chap 2
Prog Proc Chap 2
Prog Proc Chap 2
Conception de programmes
procéduraux
2.1 Motivation
Une fonction peut être vue comme un opérateur qui eectue un calcul et qui produit un
résultat. Une fonction réalise, donc, une simple opération dont le résultat peut être, par
la suite, utilisé dans d'autres opérations plus complexes.
Une fonction doit, tout d'abord, être dénie. La dénition d'une fonction est composée
de deux parties, une entête est un corps. En langage C, l'entête d'une fonction est
composée du type du résultat de la fonction, du nom de la fonction ainsi que d'une liste
de paramètres comprise entre des parenthèses (voir Programme 2 pour la syntaxe des
fonctions C). Le nom de la fonction est un identicateur choisi par le programmeur. La
liste des paramètres dépend des données dont la fonction a besoin pour bien fonctionner.
1
2 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX
.
.
. float SaisieReelNonNul()
do {
{ float x;
.
.
}
.
while (abs(x) < eps);
do return x;
{ }
printf("Donnez un réel non nul"); .
.
.
scan("%f",&x10);
} x1 = SasieReelNonNul();
return expression ;
Une fois dénie, une fonction peut être appelée autant de fois que nécessaire. L'appel
d'une fonction se fait en évoquant le nom de la fonction suivi d'une liste de paramètres.
Il est important de signaler que l'appel d'une fonction ne constitue pas une instruction
à part entière. En d'autres mots, on ne peut pas trouver dans un programme C, par
exemple, une ligne se limitant à fonc(p1 , p2 , . . .); , où fonc est le nom d'une fonction. La
raison est que, à lui tout seul, un tel appel n'a aucun eet (de bord), c'est-à-dire, qu'il ne
modie pas les valeurs des variables de la routine appelante. Donc, le résultat d'un appel
de fonction doit toujours être récupéré par la routine appelante et inséré soit dans une
aectation, soit dans une expression, soit dans un achage ou autres.
On présente souvent les procédures comme étant des fonctions qui ne retournent pas
de résultat. On peut alors se demander a quoi peut bien servir une procédure si elle
ne retourne pas de résultat. Comme première réponse, on peut dire que les résultats
des calculs eectués par une procédure peuvent bien être achés par la procédure elle
même, sans qu'ils soient communiqués à la routine appelante. C'est le cas de la procédure
4 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX
L'appel d'une procédure, (ou d'une fonction d'ailleurs), provoque une rupture avec le
déroulement linéaire des instructions selon l'ordre de leur apparition dans le programme.
Ainsi, le corps de la procédure appelée, qui peut se trouver bien loin de l'appel de la
procédure, s'exécute avant les instructions qui se trouvent immédiatement après l'appel.
Dans le programme de la Figure 2.2, par exemple, le deuxième et troisième appel à la
procédure VerifDate ne sont exécutés que lorsque le premier appel à cette procédure se
termine. Les appels de procédures peuvent donc être considérées comme des structures
de contrôle puisqu'elles permettent de couper avec l'ordre linéaire des exécutions.
Toute routine, (fonction ou procédure), peut avoir besoin de paramètres pour fonctionner
correctement. Ces paramètres sont déclarés lors de la dénition de la routine, comme
indiqué dans les deux sections précédentes. Au niveau de leur déclaration, c'est-à-dire
dans l'entête de la routine, les paramètres n'ont pas d'existence réelles, dans le sens que,
à ce niveau, ils n'ont pas de valeur précise. On parle alors de paramètres formels. Ces
paramètres sont initialisés, lors de l'appel de la routine, par les valeurs des expressions
utilisées dans l'appel. Ces derniers sont désignés par paramètres eectifs.
Règle 2. Les paramètres formels d'une routine et les paramètres eectifs correspondants
doivent coïncider en nombre et en type.
6 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX
19 // c h a i n e m e s s a g e à l ' a i d e d e s o n c o d e ASCII ( 1 3 0 )
20
21 s p r i n t f ( message , " j o u r %c r r o n%c " , 1 3 0 , 1 3 0 ) ;
22 V e r i f D a t e ( j o u r , 1 , 3 1 , message ) ;
23 s p r i n t f ( message , " mois %c r r o n%c " , 1 3 0 , 1 3 0 ) ;
24 V e r i f D a t e ( mois , 1 , 1 2 , message ) ;
25 s p r i n t f ( message , "ann%c e %c r r o n%c e " , 1 3 0 , 1 3 0 , 1 3 0 ) ;
26 V e r i f D a t e ( annee , 1 9 0 0 , 2 0 1 7 , message ) ;
27 return 0;
28 }
5 copie
5
7
param. e2 param. form2
7 copie
7
5
La liaison entre paramètre eectif et paramètre formel correspondant est déduite de la po-
sition du paramètre eectif, respectivement, formel, dans la liste des paramètres eectifs,
respectivement, formels.
Exercice 1. (Nombres amicaux) Deux entiers naturels sont dit amicaux s'il sont dis-
tincts et si chacun des deux entiers est égal à la somme des diviseurs stricts de l'autre.
Proposez un programme C qui détermine si deux entiers naturels, saisis par l'utilisateur,
sont amicaux ou pas. Indication: dénissez une fonction pour eectuer le calcul qui vous
semble le plus récurent.
8 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX
Figure 2.4: Programme C illustrant les deux modes de passage de paramètre. Il s'agit
de deux procédures, la première utilise le passage de paramètre par valeur et la seconde
utilise le passage par adresse ou référence.
no. lig. 1 2 3 5 6 7 8 4
a ? 5 5 5 5 7 7 5
b ? 7 7 7 7 7 5 7
tmp - - - ? 5 5 5 -
x &x *x
t t* ?
t* t** t
Table 2.1: Types des objets obtenues par l'utilisation des opérateurs & et *.
Exercice 2. (Le carré magique) Un carré magique 3×3 se compose de 9 cases, disposées
en 3 lignes et 3 colonnes, contenant chacune un entier de 1 à 9. Les cases contiennent
des valeurs distinctes telles que la somme des cases se trouvant sur la même ligne, la
même colonne, ou sur l'une des deux diagonales est la même. Écrire un programme
C qui saisie les valeurs des 9 cases d'un carré et qui détermine s'il est magique ou
pas. Indication: dénissez une fonction pour eectuer le calcul qui vous semble le plus
récurent.
Exemple 4. On se propose d'échanger les valeurs de deux variables saisies par l'utilisateur.
Pour ce faire, on utilise une procédure, Swap2, ayant deux paramètres passés par adresse,
(voir le programme de la Figure 2.4). Avec ce mode de passage de paramètres, les variables
variable var
15 return 0;
16 }
Figure 2.7: Schéma illustrant le passage de paramètres par adresse. Les changements
opérés par la procédure Swap2 sont directement eectués sur les paramètres eectifs.
a et b du programme principal ont bien été échangées, comme le montre le Tableau 2.8
qui résume la trace d'exécution de la procédure Swap2.
2.5. VARIABLES GLOBALES VS VARIABLES LOCALES 11
no. lig. 1 2 3 5 6 7 8 4
a ? 5 5 5 5 7 7 7
b ? 7 7 7 7 7 5 5
tmp - - - ? 5 5 5 -
n
n 0 1
B =
1 1
a b 0 1
c d 1 1
Avec le langage C, il est possible de déclarer des variables à l'extérieur de toute fonctions,
en général, au début du code juste après les directives. De telles variables sont alors
visibles par toutes les fonctions du programme et on les désigne par variables globales. Ceci
implique qu'il est théoriquement possible de les utiliser dans les diérentes fonctions du
programme. Néanmoins, il est rare de se trouver dans une situation où il est indispensable
d'utiliser des variables globales.
Règle 3. Il est préférable d'éviter, autant que possible, l'utilisation des variables globales.
Par ailleurs, dans toute routine, (fonction ou procédure), il est possible de déclarer des
variables dites locales à la routine. De telles variables ne peuvent être utilisées que dans
le corps de la routine où elles sont déclarées.
Si par hasard, des variables globales portent les mêmes noms que des variables locales
à une routine R alors les variables globales seront masquées pendant le déroulement de
la routine R et on ne pourra y accéder de nouveau qu'à la n du déroulement de cette
dernière. Dans le programme de la Figure 2.9, par exemple, la variable locale b de la
procédure Swap masque la variable globale de même nom. Ainsi, la valeur de la variable
globale b ne sera pas écrasée par l'aectation de la Ligne 11 et l'échange de valeur entre
les variables a et b peut avoir lieu correctement.
En fait, dès qu'une routine termine de se dérouler, les variables locales à cette rou-
tine n'ont plus d'existence, et ne pourront donc plus masquer d'autres variables, jusqu'au
12 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX
4
5 void Swap ( int ∗ adra , int ∗ adrb )
6 {
7 // Déclaeation d ' une variable locale b
9 int b;
10
11 b = ∗ adra ;
12 ∗ adra = ∗ adrb ;
13 ∗ adrb = b ;
14 }
15
16 int main ( )
17 {
18 p r i n t f ( " S a i s i s s e z deux e n t i e r s : " ) ;
19 s c a n f ( "%d %d" ,&a ,&b ) ;
20 Swap(&a ,&b ) ; // P a s s a g e p a r v a l e u r
21 p r i n t f ( "%d %d" , a , b ) ;
22
23 return 0;
24 }
Figure 2.9: Programme illustrant le masquage de variables globales par des variables
locales.
2.6. LA RÉCURSIVITÉ 13
prochain appel de la routine. Signalons que les variables globales peuvent aussi être
masquées par des paramètres de routine qui portent les mêmes noms. Néanmoins, il est
recommander de choisir les noms des variables globales et locales ainsi que les noms des
paramètres formels des diérents routines de façon à éviter les masquages. Remarquons
aussi que si l'on s'interdit d'utiliser des variables globales (Règle 3) alors la plupart des
situations de masquage sont évitées.
Soit une routine R d'un programme P, alors il est possible de partitionner l'ensemble
de toutes les variables intervenant dans le programme P en deux sous-ensembles en se
référant à la routine R comme suit:
2.6 La récursivité
Dénition 2. Une routine est dite récursive si elle s'appelle elle même.
L'appel d'une routine récursive est, en général, déclenché par une autre routine. Ce
premier appel déclenche une séquence d'appels récursifs. L'exécution d'une séquence
d'appels récursifs déclenché par un même appel initial suit le principe du dernier arrivé,
premier servi , c'est-à-dire que les appels récursifs sont terminés dans l'ordre inverse de
leur déclenchement. Ainsi l'appel récursif eectué en premier sera terminé en dernier et
2
inversement. Ceci implique que le contexte d'un appel récursif donné doit être sauveg-
ardé quelque part jusqu'à ce que tous les appels récursifs qui lui ont succédé terminent.
Pour sauvegarder ces contextes d'exécution, on utilise une structure de donnée particulière
qui s'appelle la pile d'exécution. La nécessité de la sauvegarde des contextes d'exécution
est derrière le principal inconvénient de la technique de la récursivité. C'est que cette
dernière fait intervenir un mécanisme d'exécution très consommateur d'espace mémoire,
en particulier, quand il s'agit d'exécuter de longues séquences d'appel récursifs, avec des
contextes d'exécution de grande taille.
Par ailleurs, toute fonction ou procédure récursive doit comporter une instruction (ou
un bloc d'instructions) nommée point terminal. Le point terminal permet d'arrêter la
2 Rappelons qu'un tel contexte contient des paramètres et des variables locales de cet appel
14 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX
Figure 2.11: Une fonction récursive dont l'exécution pourrait ne pas s'arrêter.
séquence d'appels récursifs. Donc, l'appel d'une routine récursive qui ne contient pas de
point terminal déclenche une suite d'appels récursifs qui ne termine jamais.
Exemple 5. Il s'agit d'une fonction récursive qui calcule les valeurs d'une suite mathé-
matique connue sous le nom de suite de Syracuse, en référence à la ville américaine qui
porte le même nom. Les termes de cette suite sont dénis par:
1 si n=1
un = un/2 si n ≡ 0 [2]
u3n+1 sinon
Exemple 6. L'une des énigmes classiques qui se résolvent aisément en utilisant une
récursion est celle connue sous le nom des tours de Hanoi. Il s'agit de faire déplacer
2.6. LA RÉCURSIVITÉ 15
Figure 2.12: Résolution du problème des tours de Hanoi pour n=3 disques.
un ensemble de n disques, tous de diamètres diérents, d'un pieu de départ vers un pieu
d'arrivée. La diculté de la tâche provient du fait qu'il est interdit d'empiler un disque
de diamètre plus grand sur un disque de diamètre plus petit. On dispose, néanmoins, d'un
pieu supplémentaire qui va permettre de contourner la diculté (voir la Figure 2.12 pour
une solution de l'énigme pour le cas n=3 disques).
Le programme C de la Figure 2.13 utilise une procédure récursive ( Hanoi), qui construit
et ache la suite des déplacements valides à eectuer pour faire passer la pile de disques
du pieu A au pieu C. Le nombre de disques est donné par le paramètre n. La concision
de cette procédure est remarquable, si on tient compte de la diculté de la résolution de
l'énigme.
Exemple 7. (Le tri par fusion) L'une des opérations les plus classiques en programmation
consiste à trier un tableau uni-dimensionnel dans le sens croissant ou décroissant. On
voudrait alors étudier un tri qui soit plus ecace que les tris de complexité quadratique
tel que le tri à bulles, le tri par insertion ou le tri par sélection. Pour ce faire, on choisit
d'étudier le tri par fusion. Ainsi, pour trier un tableau de n éléments, on procède en le
scindant en deux moitiés, de taille quasi-égale qu'on trie de manière indépendante. Une
fois, les deux moitiés du tableau triées, il est possible de les fusionner en un seul tableau
trié en un temps linéaire, O(n). Pour trier les deux moitiés de tableau, on peut appliquer
la même stratégie, de manière récursive, et ainsi de suite, jusqu'à ce que l'on atteigne des
tableaux de taille 1, qui sont triés d'oce.
En résumé, le tri par fusion peut être réalisé par une procédure récursive qui décompose
le problème en deux sous-problèmes de taille quasiment égales puis combine les solutions
des deux sous-problèmes en un temps linéaire, O(n), d'où une complexité en O(n log n).
Le programme C de la Figure 2.14, contient cinq procédures, dont deux qui mettent en
÷uvre le principe du tri par fusion. La première procédure, TriFusion qui est récursive,
procède en décomposant le problème en deux puis en combinant les solutions des deux
sous-problèmes en faisant appel à La deuxième procédure, Fusion. Cette dernière a pour
tâche de fusionner deux tableaux qui sont supposés être triés. Un exemple d'une telle
fusion est le suivant:
Avant fusion
16 CHAPTER 2. CONCEPTION DE PROGRAMMES PROCÉDURAUX
G= 0 3 4 5 7 9 D= 1 2 5 6 7 8
Après fusion
G= 0 1 2 3 4 5 D= 5 6 7 7 8 9
Exercice 4. (La fonction d'Ackermann) L'une des fonctions les plus étudiée en informa-
tique théorique est celle d'Ackermann. Cette fonction est dénie comme suit:
Ack(0, m) = m + 1
Ack(n + 1, 0) = Ack(n, 1)
Ack(n + 1, m + 1) = Ack(n, Ack(n + 1, m))
Proposez une fonction C récursive qui calcule la valeur de la fonction d'Ackermann pour
un n et un m donnée.
2.6. LA RÉCURSIVITÉ 17
46 }
47
48 void T r i F u s i o n ( int tab [ ] , int aux [ ] , int deb , int fin )
49 {
50 int m;
51
52 if ( deb < f i n )
53 {
54 m = ( deb + f i n ) / 2 ;
55 T r i F u s i o n ( tab , aux , deb ,m) ;
56 T r i F u s i o n ( tab , aux ,m+1, f i n ) ;
57 Fusion ( tab , aux , deb ,m,m+1, f i n ) ;
58 }
59 }
60
61
62 int main ( )
63 {
64 int n , tab [MAXTAIL] , aux [MAXTAIL ] ;
65
66 do{
67 p r i n t f ( "Donnez l e nombre d'% c l%cment %c t r i e r : " , 1 3 0 , 1 3 0 , 1 3 3 ) ;
68 s c a n f ( "%d" ,&n ) ;
69 } while ( n > MAXTAIL) ;
70
71 S a i s i e T a b l e a u ( tab , n ) ;
72 T r i F u s i o n ( tab , aux , 0 , n − 1) ;
73 A f f i c h e T a b l e a u ( tab , n ) ;
74 return 0;
75 }
Exercice 5. Le tri rapide (quicksort), comme le tri par fusion, s'appuie sur la stratégie
diviser pour régner. Ainsi, pour trier les éléments d'un tableau tab[deb..fin] dont
les indices sont comprit entre un indice de début et un indice de n, le tri rapide procède
comme suit: Le tableau T[deb..fin] est scindé en deux sous-tableaux tab[deb..m-1]
et tab[m..fin], où m est un entier comprit entre deb et fin. Les éléments des deux
sous-tableaux sont réarrangés de telle sorte que:
tous les éléments de tab[deb..m-1] sont inférieur ou égal à tab[m] qui, à son tour,
doit être inférieur ou égal à tous les éléments de tab[m+1..fin].
Le tri des deux sous-tableaux tab[deb..m-1] et tab[m+1..fin] sont, ensuite, assuré
par deux appels récursifs à la même routine de tri rapide. À la diérence du tri par
fusion, le tableau tab[deb..fin] se trouve trié du moment que les deux sous-tableaux
sont triés.
Un point crucial du tri rapide est le choix de l'indice m au niveau duquel le tableau
tab[deb..fin] est scindé en deux. Pour des raisons de simplicité, on suppose que cet
indice correspond à la position nale de l'élément tab[fin], qui est alors désigné par
le pivot, dans le tableau tab[deb..fin].
• Proposez une fonction C qui permet de réarranger un tableau tab[deb..fin] de
sorte que la condition du partitionnement décrite ci-dessus soit vériée, et qui
retourne la position nale de tab[fin] dans tab[deb..fin].
• Proposez une procédure récursive qui met en ÷uvre le principe du tri rapide.
• Complétez le programme C pour qu'il eectue le tri rapide d'un tableau d'entiers
données ayant une taille données.
Exercice 7. Codez, dans le langage C, le test de parité qui utilise la récursivité croisée
(voir le Chapitre 2 du cours d'algorithmique).
2.7 Conclusion
4. Écrire le programme principale qui aura pour tâche de récupérer les solutions des
sous-problèmes et de les combiner en une solution pour le problème initial.