INFORMATIQUE 1ère Année Algo
INFORMATIQUE 1ère Année Algo
INFORMATIQUE 1ère Année Algo
htm
Vous avez toute liberté pour télécharger, imprimer, photocopier ce cours et le diffuser gratuitement. Toute diffusion à
titre onéreux ou utilisation commerciale est interdite sans accord de l'auteur.
Si vous êtes le gestionnaire d'un site sur Internet, vous avez le droit de créer un lien de votre site vers mon site, à
condition que ce lien soit accessible librement et gratuitement. Vous ne pouvez pas télécharger les fichiers de mon site
pour les installer sur le vôtre.
PLAN
I : Algorithmes
1) Actions élémentaires
2) Affectations de variables
3) Instructions conditionnelles
4) Expressions booléennes
5) Instructions itératives
6) Exemples
II : Types de données
1) Le stockage de l'information
2) Les variables de type simple
3) Les structures de données
III : Questions diverses relatives aux algorithmes
1) L'équation du second degré et la résolution d'équation par dichotomie
2) Preuve d'un algorithme
3) Complexité d'un algorithme
4) Arrêt d'un algorithme
IV : Bases de données
1) Attributs et schémas relationnels
2) Données et relations
3) Opérations sur la base de données
4) Exemples
I : Algorithmes
1- Actions élémentaires
Le mot algorithme provient du nom du mathématicien arabe Al Kharezmi, inventeur de l'algèbre, né
durant le IXème siècle en Perse. Un algorithme est une suite finie d'instructions à appliquer dans un
ordre déterminé dans le but de résoudre un problème donné. Chacun de nous applique les
algorithmes appris dans l'enfance lorsqu'il calcule la somme de deux nombres, leur produit ou leur
quotient.
-1-
Les algorithmes, aussi complexes soient-ils, sont construits à partir d'actions élémentaires,
essentiellement au nombre de trois :
A cela, il faut ajouter les instructions de lecture des données et de sortie des résultats. Nous
utiliserons une notation symbolique, adaptable à n'importe quel langage de programmation. Nous
donnerons également des exemples de traduction syntaxique d'un algorithme en un programme
utilisable sous Python, langage de programmation, Scilab, logiciel dédié au calcul numérique de
données matricielles, tous deux utilisés en CPGE, Maple, logiciel de calcul formel, et Java, langage
de programmation assez répandu en université. Mais ceci n'est pas un cours d'apprentissage d'un de
ces langages ou logiciels, mais un cours généraliste sur les notions universelles rencontrées en
informatique. Le lecteur peut également transcrire les algorithmes utilisés les plus simples sur sa
calculatrice programmable ou en n'importe quel autre langage de programmation.
2- Affectations de variables
L'affectation de variable permet d'attribuer des valeurs à des variables ou de changer ces valeurs. Les
variables sont représentées par un nom, qui peut comporter plusieurs lettres. La plupart des langages
de programmation modernes font la distinction entre majuscules et minuscules. Nous symboliserons
l'affectation de variable de la façon suivante :
Y←2 Y prend la valeur 2
X ← 2*Y+1 puis X la valeur 5 (* désigne le produit)
X ← 3*X +2 puis X prend la valeur 17
Le membre de droite est une expression calculée par la machine, puis, une fois ce calcul effectué, le
résultat est stocké dans la variable figurant dans le membre de gauche. Si le même nom figure dans
les deux membres, cela signifie que la variable change de valeur au cours du calcul. Dans la dernière
instruction ci-dessus, l'ancienne valeur 5 de X est définitivement perdue au cours de l'exécution du
calcul et remplacée par la valeur 17.
Affectation de variables
En Python En Scilab
Y=2 Y=2
X = 2*Y + 1 X = 2*Y + 1
X = 3*X + 2 X = 3*X + 2
En Maple En Java
Y := 2; Y = 2;
X := 2*Y + 1; X = 2*Y + 1;
X := 3*X + 2; X = 3*X + 2;
Le choix du symbole "=" en Python, Scilab et Java n'est pas très heureux, car l'instruction "←" ne
désigne pas une égalité mathématique, mais une action visant à changer la valeur d'une variable. Le
choix de ":=" dans Maple est de ce point de vue plus clair.
-2-
Le changement simultané de deux variables demande une certaine attention. Supposons qu'on
dispose d'un couple (a, b) dont la valeur a été préalablement assignée et qu'on veuille changer la
valeur de ce couple en (b, a + b). La commande suivante est incorrecte :
a←b
b←a+b
car dans la deuxième instruction, la valeur de a figurant dans le membre de droite a été modifiée en
celle de b dans la première instruction, ce qui a contribué à effacer la précédente valeur de a. A la fin
du calcul, on a en fait remplacé le couple (a, b) par le couple (b, 2b). De même :
b←a+b
a←b
donne la valeur correcte de b, mais recopie ensuite cette valeur dans a, de sorte que le couple (a, b)
a été remplacé par le couple (a + b, a + b). Il convient d'utiliser une variable temporaire. Notons
(a0, b0) la valeur initiale du couple (a, b). On indique en commentaire les valeurs de chaque variable
au cours du calcul. Il est utile d'ajouter ce genre de commentaire dans un programme afin d'en
prouver la validité. Les commentaires sont précédés d'un # en Python et Maple, d'un // en Scilab ou
Java.
tmp ← b # après cette instruction, tmp = b0, a = a0, b = b0
b←a+b # après cette instruction, tmp = b0, a = a0, b = a0 + b0
a ← tmp # après cette instruction, tmp = b0, a = b0, b = a0 + b0
On a bien le résultat attendu. On aurait pu faire :
tmp ← a # après cette instruction, tmp = a0, a = a0, b = b0
a←b # après cette instruction, tmp = a0, a = b0, b = b0
b ← tmp + b # après cette instruction, tmp = a0, a = b0, b = a0 + b0
Dans les deux cas, c'est la variable dont on a stocké la valeur dans tmp qu'on modifie en premier.
De même, si on veut permuter les valeurs de deux variables, on procèdera comme suit :
tmp ← a # après cette instruction, tmp = a0, a = a0, b = b0
a←b # après cette instruction, tmp = a0, a = b0, b = b0
b ← tmp # après cette instruction, tmp = a0, a = b0, b = a0
Signalons que Python dispose d'une option d'affectation simultanée des variables évitant l'utilisation
de la variable tmp :
a,b = b,a + b
a,b = b,a
3- Instruction conditionnelle
Nous écrirons cette instruction :
si <Condition>
alors <Bloc d'Instructions 1>
sinon <Bloc d'Instructions 2>
finsi
<Bloc d'Instructions 1> désigne une ou plusieurs instructions à exécuter si <Condition> est vraie. <Bloc
d'Instructions 2> désigne une ou plusieurs instructions à exécuter si <Condition> est fausse. Par
exemple :
si X >0
alors X ← X -1
sinon X ← X + 1
finsi
Cette instructions retranche 1 à une variable X positive et ajoute 1 à une variable négative ou nulle.
-3-
Dans le cas où il n'y a pas de <Bloc d'Instructions 2> à exécuter, on mettra l'instruction conditionnelle
sous la forme :
si <Condition>
alors <Blocs d'Instructions 1>
finsi
<Condition> est une expression booléenne pouvant prendre la valeur vraie (True) ou fausse (False),
telle que X = 0, X > 0, X ≥ 0, X ≠ Y, X est un entier pair, etc... La façon de transcrire les
expressions booléennes est propre à chaque langage. Par exemple, Les quatre conditions ci-dessus se
traduisent par :
Expressions booléennes
En Python En Scilab
X == 0 X == 0
X>0 X>0
X >=0 X >=0
X != Y X <> Y
X%2 == 0 modulo(X,2) == 0
En Maple En Java
X=0 X == 0
X>0 X>0
X >=0 X >=0
X <> Y X != Y
X mod 2 == 0 X%2 == 0
On notera l'utilisation d'un double symbole = = pour tester l'égalité de deux variables dans les
langages Python, Scilab et Java puisque ces langages utilisent déjà le simple = pour l'affectation de
variable.
-4-
Instruction conditionnelle
En Python En Scilab
En Maple En Java
4- Expressions booléennes
Les expressions booléennes intervenant dans la condition d'une instruction conditionnelle peuvent
être combinées entre elles, comme en mathématiques, au moyen des opérateurs de conjonction (et),
de disjonction (ou) et de négation (non). La disjonction est prise au sens large, c'est-à-dire que "A ou
B" est vraie à partir du moment où une seule des deux propositions est vraie. Il existe également
deux expressions, l'une ayant la valeur "Vrai", l'autre ayant la valeur "Faux". La traduction de ces
opérateurs diffèrent d'un langage à l'autre.
Opérateurs booléens
En Python En Scilab
True %T
False %F
(X>0) or (Y==0) (X>0) | (Y==0)
B = (X<=0) and (Y!=0) B = (X<=0) & (Y<>0)
not B ∼B
En Maple En Java
true True
false False
(X>0) or (Y=0) (X>0) | (Y==0)
B := (X<=0) and (Y<>0); B = (X<=0) & (Y!=0);
not(B); !B
Si les connecteurs logiques "et", "ou" et "non" ont le même sens dans tous les langages, leur
évaluation effective peut différer. Considérons deux variables numériques X et Z. On initialise X à 1,
mais Z reste non initialisée. Considérons maintenant l'expression booléenne A suivante :
A ← (X > 0) ou (Z = 1)
Quelle valeur faut-il lui attribuer ? Les logiciels diffèrent sur ce point. Scilab remarquera que Z n'a
pas été initialisée, donc qu'il est impossible de définir la valeur de vérité de A. Le logiciel s'arrête
-5-
alors à cette instruction en indiquant une erreur due au défaut d'initialisation de Z. Python, pour des
raisons de rapidité, évaluera d'abord l'expression X > 0, notera qu'elle est vraie puis en déduira que A
est vraie quelle que soit la valeur de vérité de l'expression Z = 1 et donc ne cherchera pas à évaluer
cette dernière. La valeur de vérité True sera attribuée à A et l'exécution du programme se
poursuivra. Cependant, si X avait été initialisée à – 1, il aurait évalué la valeur de vérité de Z = 1 et
une erreur d'exécution se serait alors produite.
Pour des raisons de sûreté, il est prudent que le programmeur ne prévoit une évaluation booléenne
que s'il est certain que chaque expression booléenne possède effectivement au moment de son
exécution une valeur bien définie, faute de quoi un programme qui semble fonctionner dans certains
cas pourrait soudainement cesser de fonctionner dans d'autres.
5- Instruction itérative
Cette instruction consiste à répéter un certain nombre de fois les mêmes instructions. C'est ce qui fait
la puissance d'un programme informatique. On distingue deux types d'itérations :
i est une variable appelée compteur de boucles. Dans l'exemple précédent, <Bloc dInstructions> est
exécuté n fois, n étant une variable préalablement définie. Par exemple, la boucle suivante calcule la
somme des n premiers carrés d'entiers :
S←0
pour i ← 1 à n
S ← S + i*i
finfaire
Instruction itérative
En Python En Scilab
for i in range(n):
Bloc d'Instructions for i in deb:fin
Bloc d'Instructions
for i in range(deb, fin+1): end
Bloc d'Instructions
En Maple En Java
-6-
On prendra garde qu'en Python, dans le premier exemple, i varie en fait de 0 à n – 1. range(n) désigne
en effet une structure permettant de parcourt la suite des entiers naturels strictement inférieur à n. De
même, pour parcourir les entiers entre valeurDebut et valeurFin inclus, on utilisera range(valeurDebut,
valeurFin+1), la dernière valeur de range étant exclue. Comme pour l'instruction conditionnelle, c'est
l'indentation qui marque la limite du bloc.
Par exemple, on calcule la plus petite puissance de 2 supérieure ou égale à la variable n comme suit,
n étant un entier supérieur ou égal à 2 préalablement défini :
P←1
tant que P < n faire # au début de la boucle, P est une puissance de 2 et P < n
P ← P*2 # après cette instruction, P est une puissance de 2 et P/2 < n
finfaire # à la fin de la boucle, P est une puissance de 2 et P/2 < n ≤ P
Instruction itérative
En Python En Scilab
En Maple En Java
6- Exemples
❑ La suite de Collatz
an
Collatz a étudié la suite définie par a0 entier strictement positif et an+1 = si an est pair et 3an + 1 si
2
an est impair. Si on part de 27, on obtient :
-7-
27, 82, 41, 124, 62, 31, 94, 47, 142, 71, 214, 107, 322, 161, 484, 242, 121, 364, 182, 91, 274, 137,
412, 206, 103, 310, 155, 466, 233, 700, 350, 175, 526, 263, 790, 395, 1186, 593, 1780, 890, 445,
1336, 668, 334, 167, 502, 251, 754, 377, 1132, 566, 283, 850, 425, 1276, 638, 319, 958, 479, 1438,
719, 2158, 1079, 3238, 1619, 4858, 2429, 7288, 3644, 1822, 911, 2734, 1367, 4102, 2051, 6154,
3077, 9232, 4616, 2308, 1154, 577, 1732, 866, 433, 1300, 650, 325, 976, 488, 244, 122, 61, 184,
92, 46, 23, 70, 35, 106, 53, 160, 80, 40, 20, 10, 5, 16, 8, 4, 2, 1, 4, 2, 1, 4, 2, 1, etc...
On ignore aujourd'hui si, pour tout a0, la suite finit par boucler par le cycle 1, 4, 2, 1 ou non. En
2010, cette propriété a été vérifiée pour tout a0 ≤ 5 × 1018. On sait seulement que, s'il existe un cycle
non trivial, il possède une période au moins égale à 17087915. Voici ci-dessous un algorithme
affichant tous les termes de la suite jusqu'à ce qu'on arrive à 1, et calculant le nombre de termes
affichés. Cette dernière valeur est stockée dans une variable nommée nbrIter. Le paramètre a est la
valeur initiale de la suite, supposée connue au moment où l'on effectue ces instructions :
nbrIter ← 0
Tant que a ≠ 1 faire
Si a pair alors a ← a/2
sinon a ← 3*a + 1 finsi
Afficher a
nbrIter ← nbrIter + 1
finfaire
Dans la pratique, il est utile de regrouper ces instructions en une fonction de la valeur initiale a,
fonction à laquelle on peut donner un nom de son choix, Collatz par exemple, et dont le résultat est
par exemple la valeur finale de nbrIter. On peut ensuite utiliser cette fonction comme une nouvelle
commande en demandant Collatz(27) par exemple.
La suite de Collatz
En Python En Scilab
-8-
En Maple En Java
On remarquera qu'en Python, le quotient de la division euclidienne dans les entiers se note //. Par
ailleurs, on peut également noter la syntaxe abrégée pour ajouter un nombre à la variable nbrIter. Ceci
permet d'accéléler l'exécution du programme en ne cherchant qu'une fois l'adresse en mémoire où se
trouve la variable nbrIter.
En Maple, le paramètre a0 ne peut voir sa valeur modifiée dans la fonction. C'est pourquoi cette
valeur est copiée dans une variable a locale à la procédure. Il en est de même en Java. Par ailleurs,
dans ce dernier langage, le type de chaque variable utilisée doit être clairement défini par l'utilisateur
(type entier int dans le cas présent pour toutes les variables utilisées). En Python ou Maple, le type de
la variable est implicitement définie par sa première affectation. En Scilab, le type par défaut est le
type float des nombres réels. Les calculs sont a priori approchés, et dans certains cas, la parfaite
exactitude du résultat peut ne pas être garantie.
❑ La multiplication égyptienne
Afin de multiplier deux nombres entre eux, par exemple 43 et 34, les égyptiens faisaient comme suit.
Ils divisent l'un des nombres par 2, jusqu'à obtenir 1, l'autre étant multiplié par 2. On ajoute les
multiples du deuxième nombre correspondant à des quotients impairs du premier. Cet algorithme
n'est autre que l'algorithme usuel de multiplication, mais lorsque les nombres sont écrits en binaire :
43 34
21 68
10 136
5 272
2 544
1 1088
34 + 68 + 272 + 1088 = 1462 = 43 × 34
L'algorithme est le suivant. Nous avons besoin de trois variables, A, B et S. A et B prendront les
valeurs successives calculées dans chaque colonne, S sera la somme partielle des valeurs B lorsque A
sera impair. Nous commentons les instructions en notant entre parenthèses les relations vérifiées par
les variables A, B et S au fur et à mesure du calcul. Pour cela, ak, bk et sk désignent les valeurs de A,
B et S après k boucles. Nous montrons alors par récurrence que AB + S est constante, égale à ab,
produit des deux valeurs initiales. Cette relation constitue alors ce qu'on appelle un invariant de
boucle. // désigne le quotient entier de deux entiers, à adapter à la syntaxe propre au langage utilisé.
-9-
A←a # initialement A = a
B←b # et B = b
S←0 # S = 0 donc ab = S + AB
Tant que A ≠ 0 faire # Invariant de boucle : ab = sk + akbk
Si A impair alors S ← S+B finsi # sk+1 = sk si ak est pair
# et sk+1 = sk + bk si ak est impair
a
A ← A // 2 # ak+1 = k si ak est pair
2
a –1
# et ak+1 = k si ak est impair
2
B←B*2 # bk+1 = 2bk et dans tous les cas,
# on a : sk+1 + ak+1bk+1 = sk + akbk
finfaire # On a donc bien sk+1 + ak+1bk+1 = ab
# et donc à nouveau ab = S + AB
On termine la boucle quand A = 0 ; le résultat final ab se trouve donc dans S. On aurait pu également
apporter la preuve de la validité de l'algorithme en indiquant simplement après chaque instruction les
relations entre A, B et S, ce qui est plus concis, mais peut-être plus difficile à comprendre. Par
exemple, à la cinquième ligne ci-dessous, si A est impair, on augmente S de B, donc, si on avait la
relation ab = S + AB avant cette instruction, on a nécessairement, après avoir augmenté S de B, ab =
S – B + AB.
A←a #A=a
B←b #B=b
S←0 # S = 0 donc ab = S + AB
Tant que A ≠ 0 faire # Invariant de boucle : ab = S + AB
Si A impair alors S ← S+B finsi # si A est pair, ab = S + AB
# si A est impair, ab = S – B + AB = S + (A – 1)B
A ← A // 2 # Dans tous les cas, ab = S + 2AB
B←B*2 # Dans tous les cas, ab = S + AB
finfaire # On sort de la boucle quand A = 0 donc ab = S
La multiplication égyptienne
En Python En Scilab
- 10 -
En Maple En Java
Si on remplace la somme par le produit et 0 par 1, alors le résultat de la procédure donne la valeur de
P = ba, l'invariant de boucle étant alors ba = P*BA.
A←a #A=a
B←b #B=b
P←1 # P = 1 donc ab = P*BA
Tant que A ≠ 0 faire # Invariant de boucle : ab = P*BA
Si A impair alors P ← P*B finsi # si A est pair, ab = P*BA
# si A est impair, ab = P/B*BA = P*BA–1
A ← A // 2 # Dans tous les cas, ab = P*B2A
B←B*B # Dans tous les cas, ab = P*BA
finfaire # On sort de la boucle quand A = 0 donc ab = P
Cela permet de calculer la puissance ba en O(ln(a)) opérations au lieu de O(a). Cet algorithme est
connu sous le nom d'exponentiation rapide.
II : Types de données
1- Le stockage de l'information
La plus petite information utilisable dans un ordinateur est le chiffre binaire (binary digit ou bit) 0 ou
1. Ces chiffres binaires sont regroupés par 8 pour donner un octet. Il existe donc 28 = 256 octets,
depuis 0000 0000 jusqu'à 1111 1111. La moitié d'un octet est représenté par 4 chiffres binaires,
donnant 24 = 16 combinaisons possibles, depuis 0000 jusqu'à 1111. Ces combinaisons sont
représentées par les symboles 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F appelés chiffres
hexadécimaux. Un octet peut donc être également représenté par deux chiffres hexadécimaux. Par
exemple :
0101 1100 ou 5C
0101 1101 ou 5D
0101 1110 ou 5E
0101 1111 ou 5F
0110 0000 ou 60
Il est prudent de spécifier par un symbole particulier (par exemple un h en indice) les nombres
hexadécimaux. Ci-dessus, il ne faut pas confondre l'octet 60h et le nombre décimal 60. De même, si
une ambiguïté est possible, les nombres binaires seront indicés par un b.
- 11 -
Les octets servent au codage de toute l'information. Les variables sont codées par une suite d'octets,
de même que les instructions des programmes. Nous ne nous intéresserons qu'au codage des
variables. Il existe essentiellement deux types de variables :
• Celles qui sont codées par un nombre prédéfini d'octets. Le type de ces variables est dit simple. Il
s'agit principalement des entiers, des nombres flottants, des booléens et des caractères.
• Celles qui sont codées par un nombre variable d'octets. Le type de ces variables est dit composé.
Il s'agit des tableaux, des listes, des chaînes de caractères.
L'avantage de ce type de variable est que les calculs sont parfaitement exacts tant qu'on reste dans le
domaine de définition des entiers. Il n'y a pas d'erreurs d'arrondis. Les principales opérations qu'on
utilise sur ce type de variables sont l'addition, la multiplication, la soustraction, l'élévation à une
puissance entière et la division euclidienne (avec quotient entier et reste). Cette dernière est définie
de la façon suivante. Si a et b appartiennent à , b étant non nul, il existe un unique couple (q, r) de
2
tel que :
a = bq + r et 0≤r<b
q est le quotient, r est le reste. Dans le cas de Scilab, la fonction modulo s'applique en fait à des
variables de type réel.
En Maple En Java
L'inconvénient de ce type de variable est la limitation de sa capacité. Des calculs avec des nombres
trop grands peuvent dépasser le domaine de définition des entiers.
Le codage des entiers int32 se fait de la façon suivante. Si la suite de 32 bits est a31a30a29...a2a1a0,
31
l'entier représenté vaut ∑ ai 2i modulo 232, ce qui signifie que :
i=0
- 12 -
31
Si a31 = 0, alors 0a30a29...a1a0 représente l'entier ∑ ai 2i ∈ {0, ..., 231 – 1}
i=0
31 30
Si a31 = 1, alors 1a30a29...a1a0 représente l'entier ∑ ai 2i – 232 = ∑ ai 2i – 231 ∈ {– 231, ..., –1}
i=0 i=0
Les entiers forment alors un ensemble cyclique désigné en mathématiques par /232 . On constate
que le chiffre de gauche a31 désigne le signe de l'entier (+ si a31 = 0 et – si a31 = 1). On a ainsi :
On atteint ici le dernier entier positif ou nul (231 – 1) codé en binaire 0111 1111 ... 1111. L'entier
"suivant" est alors – 231, codé en binaire 1000 0000 ... 0000 :
80 00 00 00 –2147483648
80 00 00 01 –2147483647
80 00 00 02 –2147483646
... ...
FF FF FF FD –3
FF FF FF FE –2
FF FF FF FF –1
00 00 00 00 0
- 13 -
Le nombre d = 14839 est le plus grand diviseur commun de a et b. Le plus petit multiple commun est
alors m = 14839 × 4 × 3 = 178068. Il existe des algorithmes efficaces du calcul du PGCD, basé sur le
fait que PGCD(a, b) = PGCD(a modulo b, a), où a modulo b désigne le reste dans la division
ab
euclidienne de a par b. Une fois d calculé, on trouve le PPCM au moyen de la formule m = . Le
d
logiciel Scilab (version 5.4) permet de calculer le PPCM au moyen de la fonction lcm appliquée sur
les valeurs a et b converties au format int32 :
lcm(int32([59356 44517]))
On obtient alors le résultat aberrant – 111369. Que s'est-il passé ? Si on demande le PGCD des deux
valeurs (fonction gcd), on obtient bien d = 14839. Mais, au lieu de calculer le PPCM m en effectuant
(a/d) × b qui donne le résultat exact sans difficulté, le logiciel calcule selon toute vraisemblance
d'abord a × b entraînant un débordement de capacité. En effet, 59356 × 44517 vaut 2642351052 qui
est supérieur à 231. Le logiciel le considère donc comme égal à 2642351052 – 232 = – 1652616244
dont le quotient entier par d vaut – 111369. On remarque donc que les deux formules,
mathématiquement identiques, m = (a/d) × b et m = (a × b)/d, ne le sont pas informatiquement. Le
choix d'utiliser la deuxième formule est particulièrement regrettable. Il peut donner un résultat faux
même si le véritable PPCM est bien dans le domaine de définition des entiers, alors que la première
formule aurait donné un résultat correct. On pourra ainsi tester que Scilab donne pour PPCM de
50000 avec lui-même la valeur surréaliste de – 35899. Ces problèmes ont été résolus à partir de la
version 5.5 de Scilab.
En Python ou Maple, il n'y a pas de limite de capacité des entiers, ceux-ci étant codés par une suite
arbitrairement longue d'octets lorsque la taille de quatre octets est dépassé. Cela se manifeste sous
Python par l'affichage d'un L à l'écran apparaissant à la suite d'un tel entier long.
Dans la suite, pour simplifier, nous donnerons des exemples directement en décimal, sans oublier que
les nombres introduits sont en fait convertis en binaire au sein de la machine pour exécuter les
calculs, puis reconvertis en décimal pour affichage à l'écran. Voici des exemples de nombres flottants
donnés avec 15 chiffres significatifs.
a = 0.887987458369512 × 105
b = – 0.124445447874558 × 10–3
Les nombres flottants ont une précision limitée ce qui signifie que, par essence, les calculs effectués
avec eux sont approchés. Au cours des calculs, les erreurs peuvent d'ailleurs s'accumuler et entraîner
un résultat infondé. Voyons ce qu'il en est par exemple pour l'addition de a et b ci-dessus. On aligne
les chiffres des deux nombres sur le même exposant, mais il en résulte une perte de précision du
nombre le plus petit :
- 14 -
0.887987458369512
– 0.0000000012444547874558 tronqué à – 0.000000001244454
La somme c = a + b vaut 0.887987457125058 × 105. Si l'on retranche a, on ne retrouve pas le b
initial, mais le b tronqué.
c – a = – 0.000000001244454 × 105 = – 0.1244454 × 10–3
Ainsi (a + b) – a n'est pas numériquement égal à b.
Les algorithmes de calcul sur nombres flottants peuvent être l'objet de propagation d'erreurs
redoutables, difficiles à détecter quand l'algorithme est complexe. Il convient donc de ne jamais
prendre un résultat numérique fourni par une machine au pied de la lettre, et de garder un regard
critique sur ce résultat.
EXEMPLE 1 : On souhaite écrire une fonction de paramètre un entier n et donnant une valeur
1
⌠ xn
approchée de In =
11
dx. On a I0 = ln( ) et, en écrivant que xn = xn–1(10 + x) – 10xn–1, on
10 + x 10
⌡0
1
obtient la relation In = – 10In–1 d'où l'idée de calculer cette fonction par itération successive. Les
n
logiciels de programmation possèdent des bibliothèques de fonctions permettant le calcul approché
des fonctions usuelles (exp, ln, sin, cos, sqrt, etc...), ce qui permet d'initialiser I0.
- 15 -
Calcul d'une intégrale
En Python En Scilab
Le calcul des premières valeurs donne les résultats suivants (on n'en donne que 8 décimales) :
n In
1 0.0468982
2 0.0310180
3 0.0231535
4 0.0184647
5 0.0153529
6 0.0131377
7 0.0114806
8 0.0101944
9 0.0091671
10 0.0083287
11 0.0076220
12 0.0071138
13 0.0057850
14 0.0135789
15 – 0.0691221
... ...
20 7483.468
xn xn+1
La 14ème valeur est fausse. En effet, pour x ∈ [0, 1], ≥ donc In ≥ In+1 pour tout n. Or
10 + x x + 10
I14 > I13. La valeur I15 est aberrante puisque négative. La 20ème l'est également puisque
1
0 ≤ In ≤ ⌠ n 1
x = n + 1. La raison de ce dysfonctionnement provient du fait que, dès la première
⌡0
valeur, I0 est approché à 10–15 près, mais que la récurrence multiplie l'erreur commise par 10 à
chaque itération. L'erreur apparaît donc de manière flagrante au bout d'une quinzaine d'itérations.
Une méthode pour calculer de façon correcte In, n étant donné, consiste dans le cas présent à partir
1
de In+20 à qui on attribue l'approximation grossière , puis, pour k décroissant de n + 19 à n, à
n + 20
1 1
appliquer la relation de récurrence inverse Ik = ( – Ik+1). L'erreur initiale est divisée par 10 à
10 k + 1
chaque itération et deviendra inférieure à la précision des nombres flottants au bout de 20 itérations.
- 16 -
Calcul d'une intégrale
En Python En Scilab
Les premières valeurs donnent cette fois, de manière beaucoup plus fiable :
n In
1 0.0468982
2 0.0310180
3 0.0231535
4 0.0184647
5 0.0153529
6 0.0131377
7 0.0114806
8 0.0101944
9 0.0091672
10 0.0083280
11 0.0076294
12 0.0070390
13 0.0065333
14 0.0060954
15 0.0057125
... ...
20 0.0043470
On a indiqué en bleu les chiffres qui diffèrent du premier calcul, ce qui fait apparaître la propagation
des erreurs qui se sont produites dans le dit calcul.
EXEMPLE 2 :
On souhaite écrire une fonction de paramètres n et qui calcule la quantité :
2n × 2 – 2 + 2 + 2 + ... + 2 + 2
constituée de n racines empilées. Il suffit de partir d'une variable valant initialement 2, puis de lui
ajouter 2 et de prendre la racine carrée, et cela n – 2 fois. La dernière opération est la différence
finale. On multiplie enfin par 2n.
- 17 -
Calcul d'une racine
En Python En Scilab
- 18 -
restera même si on multiplie par le grand nombre 2n. On peut néanmoins obtenir des valeurs
approchées convenable de la suite en multipliant par la quantité conjuguée :
22 – (2 + 2+ 2 + ... + 2 + 2)
2– 2+ 2+ 2 + ... + 2 + 2 =
2+ 2+ 2+ 2 + ... + 2 + 2
2– 2+ 2 + ... + 2 + 2
=
2 + 2 + 2 + 2 + ... + 2 + 2
Le nombre de racines du numérateur a diminué de 1 mais il est de la même forme que
précédemment. On itère donc l'utilisation des conjugués pour arriver à l'expression finale suivante :
1 1
× × ...
2 + 2 + 2 + 2 + ... + 2 + 2 2 + 2 + 2 + ... + 2 + 2
1 1
× × × 2
2+ 2+x 2 + x
ce qui conduit à la fonction suivante, plus compliquée mais numériquement plus fiable :
On peut montrer en fait que l'expression qu'on cherche à calculer n'est autre que 2n+1 sin( π ). On
2n+1
peut donc effectuer le calcul direct :
Calcul direct
En Python En Scilab
Même au-delà de n = 30, on pourra constater que g(n) et h(n) affichent la même valeur
3.14159265359, où l'on reconnaît une valeur approchée de π.
EXEMPLE 3 :
- 19 -
ex + e–x ex – e–x
On pose ch(x) = et sh(x) = . Les logiciels mathématiques connaissent ces fonctions,
2 2
généralement sous le nom cosh et sinh. Il en est de même des calculatrices scientifiques. On vérifiera
facilement que, pour tout x, ch2(x) – sh2(x) = 1. Considérons la fonction :
f : x → ch2(x2) – sh2(x2)
Cette fonction est mathématiquement constante égale à 1. Pourtant son tracé à l'aide d'un logiciel sur
l'intervalle [–10, 10] donne :
x
O
Expliquer le phénomène.
Ainsi, la programmation d'une fonction peut sembler mathématiquement correcte, mais être
numériquement problématique.
Elles sont également utiles pour programmer une boucle d'itération du type tant que, lorsque la
condition d'arrêt est complexe à formuler car pouvant provenir de plusieurs conditions :
Termine ← Faux
tant que non Termine faire
<Blocs d'instruction dont certaines changent Termine en Vrai
si une condition d'arrêt est vérifiée>
finfaire
Il existe également des fonctions à valeurs booléennes. Ainsi, le logiciel Maple possède une fonction
isprime définie sur les entiers et répondant Vrai ou Faux selon que le paramètre est un entier premier
ou non.
d) Les caractères
Un caractère représente une lettre ou un chiffre et sont les éléments constitutifs des chaînes de
caractères. Ils sont souvent codés sur un octet. Un octet permet de coder 256 caractères, suffisant
pour traiter les majuscules et minuscules de l'alphabet latin. On peut également coder les caractères
accentuées, mais ce codage n'est pas universel. De plus, le codage des divers alphabets du monde a
- 20 -
conduit au développement de caractères au format Unicode pouvant être codés sur un nombre
variable d'octets, allant parfois jusqu'à quatre octets voire plus.
Voici par exemple comment on peut chercher un sous-mot de p lettres dans un mot de n lettres. i
étant donné entre 0 et n – p, on compare pour j variant de 0 à p – 1 les lettres soumot[i+j] à mot[j]. Si
les p lettres coïcident, le sous-mot est bien inclus dans le mot à partir du rang i. Sinon, on augmente i
de 1. La boucle sur j n'a pas besoin d'être menée à terme si une lettre diffère. Une boucle tant que
s'impose donc. De même la boucle sur i s'arrête dès que le sous-mot a été trouvé. Il est commode
d'utiliser une variable booléenne Trouve servant de drapeau ; elle prend la valeur Vrai tant qu'on n'a
pas trouvé de lettres qui diffère entre le sous-mot et le mot. Si cette variable a gardé la valeur vrai
lorsqu'on a passé en revue le sous-mot en entier, c'est que ce dernier est bien inclus dans le mot.
Initialement, on donne à Trouve la valeur Faux afin de démarrer la boucle sur i. On a mis en
commentaire les relations vérifiées par les variables au cours du calcul. On donne comme résultat le
couple constitué de la variable booléenne Trouve et de l'indice à partir duquel se trouve le sous-mot,
ou –1 si le sous-mot n'est pas inclus dans le mot. On suppose qu'on accède à la lettre i du mot à l'aide
de la syntaxe mot[i].
Trouve ← Faux
i←0
tant que (non Trouve) et (i<=n-p) faire
Trouve ← Vrai
j←0 # sousmot[0...j-1] = mot[i...i+j-1] = mot vide
tant que Trouve et (j<p) faire # sousmot[0...j-1] = mot[i...i+j-1] est un invariant
# de boucle
si sousmot[j]<>mot[i+j] alors # sousmot n'est pas inclus dans mot
Trouve ← Faux
sinon # sousmot[0...j] = mot[i...i+j]
j ← j+1 # sousmot[0...j-1] = mot[i...i+j-1]
finsi # ou bien Trouve = Faux ou bien sousmot[0...j-1] = mot[i...i+j-1]
finfaire # ou bien Trouve = Faux ou bien j=p et sousmot[0...p-1] = mot[i...i+p-1]
# et donc : ou bien Trouve = Faux ou bien sousmot est inclus dans mot
# à partir du rang i
si non Trouve alors i ← i + 1
finfaire # ou bien Trouve = Vrai (et donc sousmot est inclus dans mot dès le rang i)
# ou bien i = n-p+1 et sousmot n'est pas inclus dans mot
si Trouve alors resultat ← [Trouve, i]
sinon resultat ← [Trouve,-1]
- 21 -
Recherche d'un sous-mot
En Python En Scilab
f) Affectation de variables
La plupart des langages de programmation impose de déclarer le type de chaque variable avant son
utilisation. D'autres définissent ce type implicitement au moment de la première affectation. La
connaissance de ce type est nécessaire pour savoir quelles fonctions on peut appliquer à la variable.
Par ailleurs, le type de la variable utilisée est nécessaire pour que le logiciel puisse réserver une place
en mémoire adéquate à cette variable. Ainsi, lorsqu'on affecte une valeur de type entier long à une
variable a, une adresse en mémoire est réservée à a. Cette adresse est la première de quatre octets
successifs dans lesquels est stockée la valeur de a. Pour effectuer une opération avec a, il suffit
d'aller lire à l'adresse affectée à a les données contenues à cette adresse.
❑ Les affectations par adresses se font de la manière suivante. Une affectation b ← a attribue à b la
même adresse que a. a et b sont alors deux noms synonymes pour désigner la même adresse. Une
modification de a modifiera donc aussi b, à moins que le logiciel n'attribue une nouvelle adresse à a
au moment de sa nouvelle affectation. Les affectations par adresses sont souvent effectuées pour les
- 22 -
structures de données complexes et lourdes, et pour lesquelles la recopie des données demande un
temps d'exécution non négligeable. Ces structures sont examinées au paragraphe suivant.
a) Les listes
Nous nous bornerons à donner des exemples de syntaxe sur les listes en Python. Les opérations
usuelles qu'on mène sur les listes sont les suivantes :
❑ Création
a = [5,9,6]
L'instruction précédente crée une liste de 3 éléments. En Python, l'indice du premier élément est 0.
Ci-dessus, l'indice du dernier élément est donc 2. On peut également créer un intervalle de valeurs au
moyen de la fonction range(indicedeb, indicefin) ou range(indicedeb, indicefin, increment). Pour stocker
dans une liste explicite les éléments de cet intervalle, on utilise à partir de la version 3 de Python la
syntaxe list(range(indicedeb, indicefin)), l'indice de début étant inclus et l'indice de fin étant exclu. Cette
commande est analogue en Scilab à indicedeb:indicefin-1. Si indicedeb vaut 0, on peut l'omettre.
❑ Suppression
del a[1:4] supprime les éléments d'indice 1 à 3
a.pop() enlève le dernier élément de la liste,
❑ Accès à un élément
a[5] 5ème élément de la liste a
a[5]=42 changement de la valeur du 5ème élément de la liste
a[5:9] sous-liste depuis l'indice 5 inclus, à l'indice 9 exclu
❑ Fonctions diverses
len(a) longueur d'une liste
b) Les tableaux
Les langages de programmation possèdent également des structures indicées appelées tableaux.
Ceux-ci sont adaptés aux problèmes où le nombre d'éléments est connu à l'avance, correspondant par
exemple aux notions de vecteurs ou de matrice en mathématiques.
- 23 -
Voici des exemples de tableaux en Python, utilisant la classe ndarray du module numpy. La première
instruction se borne à une déclaration d'un tableau T constitué de 10 éléments sans les définir
explicitement pour le moment, la seconde définit un tableau U par ses six éléments, la troisième
définit un tableau V de longueur 15 uniquement constitué de 1, la quatrième définit un tableau W à
deux indices, pouvant servir à représenter une matrice 2 × 4.
import numpy
T = numpy.empty(10)
U = numpy.array([1,2,3,5,8,13])
V = numpy.ones(15)
W = numpy.array([ [1,-2,3,4] , [5,-3,4,0] ])
En Scilab, on écrira :
U = [1,2,3,5,8,13]
V = ones(15)
W = [ [1,-2,3,4] , [5,-3,4,0] ]
On accède au i-ème élément du tableau U par la syntaxe U[i] en Python et U(i) en Scilab. La syntaxe
de Scilab est peu heureuse car elle ne distingue pas l'élément d'un tableau du calcul d'une fonction
appliqué sur un élément. Dans la suite, nous utiliserons donc les crochets, plus répandus parmi les
langages de programmation. Par ailleurs, par défaut, le premier indice du tableau est 0 en Python ou
Java, mais 1 en Scilab ou Maple, ce qui peut entraîner des désagréments quand on passe d'un langage
à l'autre. Ainsi, avec les exemples précédents, U[0] vaut 1 en Python, et U[1] vaut 2, alors que ces
deux éléments sont respectivement en Scilab par U(1) et U(2). En ce qui concerne le tableau W, on
utilise la syntaxe W[i,j] ou W[i][j] ou W(i,j) selon les langages. Une demande d'accès à un élément du
tableau pour un indice au-delà de la longueur du tableau entraîne une erreur d'exécution. Il existe des
instructions (size ou shape) spécifique à chaque langage pour connaître la longueur d'un tableau.
c) Exemples
Soit T un tableau ou une liste possédant n éléments, de T[0] jusqu'à T[n–1]. Voici quelques
algorithmes relatifs à ce tableau.
i←0
tant que T[i] <> k et i<n-1 faire i ← i+1 finfaire # on s'arrête si T[i] = k ou si i = n-1 indice
# du dernier élément du tableau
si T[i] = k alors i sinon -1 finsi
L'inégalité i < n – 1 doit être stricte car, si on avait i ≤ n – 1 et k absent du tableau, alors i prendrait
nécessairement la valeur n en fin de boucle et le test T[i] <> k entraînerait un débordement du
tableau et une erreur d'exécution.
S←0
pour i de 0 à n-1 faire S ← S + T[i] finfaire
moyenne := S/n
n–1
1
développant le carré, elle vaut également
n
∑ T[i]2 – m2. Si la moyenne a déjà été calculée, on peut
i=0
V←0
pour i de 0 à n-1 faire
V ← V + (T[i] - moyenne)**2 # ** est l'élévation à une puissance
finfaire
variance := V/n
S←0
V←0
pour i de 0 à n-1 faire
S ← S + T[i]
V ← V + T[i]**T[i]
finfaire
moyenne := S/n
variance = V/n - m**2
Cependant cette deuxième méthode amène souvent à ajouter dans le calcul de V des termes T[i]2
dont l'ordre de grandeur peut varier notablement, ce qui entraîne une imprécision sur V, une
deuxième imprécision étant due au calcul final de la variance qui retranche deux nombres qui peuvent
être grands, mais comparables. On retrouve ici les difficultés évoquées sur le calcul avec les nombres
flottants.
d) Affectation de variables
Soit a une liste ou un tableau. Le logiciel attribue à a une adresse à partir de laquelle il est capable de
trouver les éléments de a. Que fait l'instruction suivante ?
b←a # b = a en Scilab ou Python
Ainsi, en Scilab :
A = [1 2 3]
B=A // B est une copie de A
B(2) = 5 // B vaut maintenant [1 5 3], mais A n'a pas changé
Il convient donc de bien comprendre si l'instruction ← copie une valeur ou copie une adresse. Le
comportement des variables est totalement différent dans les deux cas.
e) Temps de calcul
Il convient de mener une réflexion sur le temps de calcul lié à l'utilisation des listes ou tableaux. Ce
temps de calcul est directement lié à la façon dont ces structures sont implémentées dans la mémoire
de la machine. En principe, les noms de tableau ou de liste désignent deux modes différents
d'implémentation de ces structures.
❑ Les tableaux : la machine réserve en mémoire une place successive à chaque élément du tableau a
constituée de n éléments. Dans ce cas, connaissant l'indice i d'un élément, la machine est capable de
déterminer à quelle adresse précise se trouve l'élément a[i]. L'accés à cet élément se fait alors en un
temps indépendant de n. On dira que le temps d'accès est en O(1). Par contre l'insertion d'un nouvel
élément ou la suppression d'un élément à un indice i donné demande de décaler tous les éléments à la
droite de cet indice, vers la droite en cas d'insertion pour laisser de la place au nouvel élément, vers
la gauche en cas de suppression pour éliminer la place laissée vacante. Le temps d'exécution d'une
suppression ou d'une insertion est alors en O(n). Ce type de structure est particulièrement adapté
lorsque des accès aux éléments de la liste sont nombreux, mais qu'il y a peu d'insertion ou de
suppression d'éléments.
❑ Les listes : la machine stocke en mémoire les éléments de façon chaînée. Partant d'un élément
appelé tête de la liste, chaque élément a connaissance de l'adresse où se trouve l'élément suivant.
Pour accèder au i-ème élément, on parcourt la chaîne depuis la tête jusqu'à l'élément désiré et le
temps d'accès au i-ème élément est en O(n). Par contre, une fois atteint cet élément i, la suppression
de l'élément i + 1 se fait en O(1) puisqu'il suffira d'indiquer à l'élément i quelle est l'adresse de
l'élément i + 2. De même, une fois atteint l'élément i, l'insertion d'un nouvel élément entre celui-ci est
le suivant se fait en O(1) : on indique à l'élément i l'adresse du nouvel élément, et on indique au
nouvel élément l'adresse de l'élément situé auparavant en i + 1. Ce type de structure est
particulièrement adapté aux données où l'on fait de nombreuses insertions ou suppressions à partir
d'un point donné (c'est typiquement le cas des traitements de textes par exemple). L'avantage de
cette structure réside également dans le fait qu'on n'a pas besoin de réserver a priori en mémoire une
- 26 -
plage d'adresses successives pour stocker les données. Celles-ci peuvent être dispersées. C'est
intéressant quand on ignore a priori quelle sera la taille finale de la liste.
Selon les cas, c'est à l'utilisateur de déterminer quelle est le type de structure le mieux adapté au
problème qu'il se pose et à consulter la documentation pour savoir si le langage de programmation
qu'il utilise prévoit le type de données qu'il souhaite et sa forme syntaxique. La différence n'est pas
anodine lorsque n s'élève à plusieurs centaines de milliers de données.
d ← b**2 - 4*c
si d>0
alors nbs ← 2
sinon
si d=0
alors nbs ← 1
sinon nbs ← 0
finsi
finsi
Il n'existe aucun algorithme répondant à la question posée de façon certaine. En effet, un tel
algorithme, s'il existait, signifierait qu'à l'issue d'un calcul, on est capable de dire si un nombre est
rigoureusement nul. Or il n'existe aucun procédé général qui puisse répondre à cette question. A titre
d'exemple, comment savoir si le nombre suivant est nul ?
d = 5 + 22 + 2 5 – 11 + 2 29 – 16 – 2 29 + 2 55 – 10 29
Quand on demande une valeur approchée de d à Xcas, il donne une valeur négative, quand on
demande à Python, geogebra ou à un tableur, la réponse est nulle, quand on demande à Maple, la
réponse est positive.
Si le cas précédent paraît trop simple au lecteur, considérer la suite (dn)n≥1 définie par :
dn = 0 si n17 + 9 et (n + 1)17 + 9 admettent 1 comme seul diviseur commun
dn = 1 si n est pair et n17 + 9 et (n + 1)17 + 9 ont un autre diviseur commun que 1
dn = –1 si n est impair et n17 + 9 et (n + 1)17 + 9 ont un autre diviseur commun que 1
∞
dn
et soit d = ∑ 10n. d est-il nul ? positif ? négatif ? On remarquera que, dans les deux exemples
n=1
Concernant les équations du second degré, le seul algorithme raisonnable est celui qui donne des
valeurs approchées des solutions complexes. En effet, même si par exemple d est numériquement
- 27 -
négatif pour la machine alors qu'il est mathématiquement positif ou nul, les solutions complexes
approchées auront certes des parties imaginaires mais celles-ci seront très faibles, et les solutions
complexes données seront numériquement très proches des solutions réelles.
Le même problème se pose pour la résolution d'une équation par dichotomie. Le problème de
concours E3A 2017 de la filière PSI demandait d'écrire en Python une fonction
rech_dicho(f,a,b,eps) de paramètre une fonction f continue, deux réels a et b tels que f soit
définie sur [a, b] avec f(a)f(b) < 0 et une marge d'erreur eps, cette fonction devant donner une
valeur approchée d'une racine r de f à eps près en procédant par dichotomie (clairement inspiré
d'une démonstration par dichotomie du théorème des valeurs intermédiaires). Un tel algorithme
n'existe malheureusement pas. Considérons en effet un nombre d élément de ]–1, 1[ et soit f la
fonction suivante définie sur [0, 1] :
1
f(x) = (3d + 3)x –1 si x ≤
3
1 2
=d si ≤ x ≤
3 3
2
= (3 – 3d)x + 3d – 2 si x ≥
3
On vérifiera facilement que, mathématiquement :
cette fonction f est continue sur [0, 1]
1 1 1 1
si d > 0, la seule racine de f est < < –
3 + 3d 3 2 10
1 2
si d = 0, les racines de f forme l'ensemble [ , ]
3 3
2 – 3d 2 1 1
si d < 0, la seule racine de f est > > +
3 – 3d 3 2 10
L'algorithme rech_dicho prétend trouver une valeur approchée d'une racine de f. Appliquons
alors le rech_dicho supposé exister à cette fonction, en prenant pour d par exemple la valeur
1
5 + 22 + 2 5 – 11 + 2 29 – 16 – 2 29 + 2 55 – 10 29, a = 0, b = 1, eps = (nous
10
supposons que le prétendu algorithme a la possibilité de calculer une valeur approchée de d à la
précision qu'il souhaite). Si la réponse est comprise entre 0.4 et 0.6, on serait alors certain que,
mathématiquement d = 0. Ainsi, l'algorithme par dichotomie demandé par le concours serait capable
dans ce cas de prouver l'égalité algébrique :
5+ 22 + 2 5 – 11 + 2 29 – 16 – 2 29 + 2 55 – 10 29 = 0
∞
dn
Mieux, si on l'applique au deuxième d = ∑ n proposé plus haut (il suffit pour cela d'intégrer dans la
n=1 10
fonction définissant f un calcul approché de d à toute précision demandée), et si la réponse de
rech_dicho était encore comprise entre 0.4 et 0.6, cela signifierait que l'algorithme est capable de
dire que pour tout n, le pgcd de n17 + 9 et (n + 1)17 + 9 vaut 1.
Un autre réponse de rech_dicho conduirait à d'autres résultats invraisemblables. Il est
inconcevable que, de l'algorithme demandé, on puisse en déduire une inégalité algébrique ou
l'existence d'un entier vérifiant une propriété arithmétique donnée.
Il eut été plus raisonnable que l'algorithme par dichotomie rech_dicho(f,a,b,eps) se borne à
demander une valeur x telle que x – r < eps (r racine de f) ou que f(x) < eps. Autrement dit, on
- 28 -
demande une valeur x qui approche la racine à eps près, ou alors une valeur x telle que f(x) soit
nulle à eps près.
def rech_dicho(f,a,b,eps):
mini=a
maxi=b
c=(mini+maxi)/2
tant que (maxi-mini>eps) et abs(f(c))>eps
si f(mini)*f(c)<0
maxi=c
sinon
mini=c
finsi
c=(mini+maxi)/2
fintantque
return(c)
Un algorithme se prouve au même titre qu'un théorème mathématique. La plupart des algorithmes
que nous avons donnés ont leur preuve indiquée en bleu en commentaire. Il s'agit essentiellement de
prouver qu'une itération produit bien ce pour quoi elle a été conçue. Le raisonnement consiste à
mettre en évidence un invariant de boucle, propriété qui est vraie avant la première itération, qu'on
suppose vraie au début de la i-ème itération et dont on vérifie qu'elle est encore vraie à l'issue de la i-
ème itération. Elle sera donc vraie à la sortie de boucle et on vérifie alors si la valeur de l'invariant de
boucle à la sortie de l'itération donne bien le résultat attendu.
Outre les exemples déjà donnés, en voici un dernier. Considérons l'algorithme suivant dont le
paramètre est une variable X donnée par l'utilisateur, appartenant à l'intervalle [1, 10[. E est un
nombre petit servant de marge d'erreur (par exemple 10–10).
S←1
Y←0
Z←X
tant que S > E faire
S ← S/2
si Z^2 >= 10 alors Z ← Z^2/10
Y ← Y+S
- 29 -
sinon Z ← Z^2
finsi
finfaire:
Il convient d'apporter une autre preuve, celle que le programme se termine. En effet, si l'utilisation
d'une boucle pour I variant de ... à ... est nécessairement finie, il n'en est pas de même des boucles tant
que dont on ignore si elle ne pourrait pas boucler indéfiniment. La preuve repose ici sur le fait que la
valeur de S est divisée par 2 à chaque itération, donc deviendra inférieure à E strictement positif.
- 30 -
en fonction de n. Seul l'ordre de grandeur de cette quantité nous intéresse. On se contente en général
d'évaluer le nombre d'itérations réalisées.
EXEMPLE 2 : la recherche du plus petit élément d'un tableau numérique. L'algorithme donné plus
haut consiste à parcourir l'ensemble du tableau. Le temps de calcul est en O(n). Mais si on sait que le
tableau est trié par ordre croissant, il suffit évidemment de prendre le premier élément du tableau. Le
temps de calcul est en O(1).
EXEMPLE 3 : La recherche d'un élément donné d'un tableau numérique. L'algorithme donné plus
haut consiste également à parcourir l'ensemble du tableau. Le temps de calcul est en O(n). Mais si on
sait que le tableau est trié par ordre croissant, on peut procéder par dichotomie. On considère un
élément situé au centre du tableau et si l'élément cherché est plus grand, on poursuit itérativement la
recherche dans la partie droite du tableau, sinon on poursuit dans la partie gauche. Ces deux parties
n
sont constituées de éléments. La dichotomie revient donc à chaque fois à chercher l'élément dans
2
un sous-tableau deux fois plus petit que le tableau précédent. Comme pour l'exponentiation rapide, le
temps de calcul est en O(ln(n)) négligeable devant n. Si de nombreuses recherches doivent être
faites, il convient de décider s'il ne faut pas une fois pour toute trier le tableau. Les algorithmes de tri
font partie du programme de deuxième année.
IV : Bases de données
Ce paragraphe a pour but de se familiariser avec le vocabulaire utilisé dans les systèmes de gestion de
bases de données, dits bases de données relationnelles.
La liste des livres possédés par la bibliothèque doit contenir un certain nombre d'informations.
Chaque livre est caractérisé par des attributs que sont : son titre, son auteur, son éditeur, son année
d'édition, sa cote. La cote a pour but d'identifier chaque exemplaire de livre de façon unique alors
qu'il peut y avoir plusieurs exemplaires du même livre. La cote sert donc de clef d'identification
unique pour chaque exemplaire possédé par la bibliothèque. On la qualifie de clef primaire. On
affecte un nom conventionnel à chacun de ces attributs (par exemple ici : titre, auteur, edition,
annee_edition, cote).
Chaque attribut est affecté à un type d'information présent dans la base de données. Les valeurs que
peut prendre cette information décrit un domaine, propre à chaque attribut, et noté dom(A) si A est
le nom de l'attribut considéré. Ainsi dom(titre) = dom(auteur) = dom(editeur) = {chaînes de
caractères}. dom(annee_edition) = . dom(cote) est une chaine de caractères conventionnels,
Passons maintenant à la liste des emprunteurs. Un emprunteur est défini par les attributs que sont :
nom, prénom, adresse, numéro d'inscription, attributs que nous nommerons nom, prenom, adresse,
num_inscription.
Enfin, la situation d'un livre est définie par les attributs que sont : sa cote, son état (en rayon,
emprunté, en réserve), la date d'emprunt et le numéro d'inscription de son emprunteur s'il est
emprunté. On peut convenir que ces deux dernières données sont nulles si le livre n'est pas emprunté.
Les attributs seront nommés ici cote, etat, date_emprunt, num_inscription.
Une partie U de cet ensemble d'attributs définit un schéma relationnel. Nous avons précédemment
défini de la sorte les schémas relationnels suivants, chacun correspondant à une catégorie de données
qu'on se propose de traiter :
Livre par la partie U1 = {titre, auteur, editeur, annee_edition, cote}.
Emprunteur par la partie U2 = {nom, prenom, adresse, num_inscription}.
Situation par la partie U3 = {cote, etat, date_emprunt, num_inscription}.
On remarque que cote est un attribut commun à Livre et à Situation, et que num_inscription est
commun à Emprunteur et à Situation. Si on veut lier plus spécifiquement l'attribut au schéma
relationnel dont il est issu, on précisera Livre.cote ou Situation.cote, et Emprunteur.num_inscription ou
Situation.num_emprunteur. Il est également possible de donner des noms différents d'attributs, d'une
part à la cote du schéma Livre, d'autre part à la cote du schéma Situation. Il est ensuite possible de
faire se correspondre ces deux noms différents (voir plus loin la notion de jointure).
Lors de la création d'une base de données, la première chose qui est demandée à l'utilisateur est de
créer la liste des attributs et les schémas relationnels qu'il souhaite utiliser. L'ensemble des schémas
relationnels ainsi choisis s'appelle un schéma de base de données
- 32 -
BIBLIOTHEQUE =
{Livre[titre, auteur, editeur, date_edition, cote],
Emprunteur[nom, prenom, adresse, num_inscription]
Situation[cote, etat, date_emprunt, num_inscription]}
2- Données et relations
Un livre sera représenté dans la base de données par l'enregistrement d'un quintuplet de données
relatives aux attributs U1, par exemple :
<Les misérables, Victor Hugo, Editions Dubois, 2002, HUG-0145>
si l'ordre des attributs a été précisé, ou sinon sous la forme :
<titre : Les misérables, auteur : Victor Hugo, éditeur : Editions Dubois, annee_edition : 2002, cote :
HUG-0145>
Un emprunteur sera représenté dans la base de données par un quadruplet de données relatives aux
attributs U2, par exemple :
<nom : Bonnot, prénom : Jean, adresse : 2 rue du Pont 21000 Dijon, num_inscription : 3017>
La situation d'un livre est donnée par un quadruplet de données relatives aux attributs U3, par
exemple :
<cote: HUG-0145, état : emprunté, date_emprunt : 10/05/2013, num_inscription : 3017>
La bibliothèque dispose évidemment de plusieurs livres. Pour cela, on dresse la liste des
enregistrements décrivant chaque livre. Cette liste s'appelle une relation ou une instance sur le
schéma relationnel Livre. Elle forme une partie de l'ensemble dom(titre) × dom(auteur) × dom (editeur) ×
dom(annee_edition) × dom(cote). Une relation peut être visualisée comme une table à deux entrées. On
affecte à chaque attribut du schéma relationnel une colonne, et on fait figurer en i-ème ligne un n-
uplet représentant le i-ème enregistrement de la relation. A l'intersection de la i-ème ligne et de la j-
ème colonne figure donc la valeur du j-ème attribut pour le i-ème élément. Par exemple :
Livre
titre auteur éditeur annee_edition cote
<Les misérables, Victor Hugo, Editions Dubois, 2002, HUG-0145>
<Les misérables, Victor Hugo, Editions Dubois, 2002, HUG-0146>
<Boule de Suif, Guy de Maupassant, Editions Durand, 2005, MAU-0238>
<Quatre-Vingt-Treize, Victor Hugo, Editions Dubois, 2002, HUG-0202>
<Germinal, Emile Zola, Editions Martin, 2004, ZOL-0134>
etc...
Les n-uplets figurant dans une relation doivent posséder une clef qui permet de les identifier de
manière unique. On exclut en effet d'avoir deux enregistrements rigoureusement identiques (il y
aurait redondance). C'est le logiciel de gestion de base de données qui, au moment où l'on procède à
l'enregistrement d'un nouveau n-uplet, doit s'assurer de cette non-redondance. C'est la cote qui joue
ici le rôle de clef. Dans le cas du schéma Emprunteur, la clef est jouée par le numéro d'inscription de
l'utilisateur, mais on peut aussi choisir comme clef le triplet <nom, prenom, adresse> attendu qu'a
priori, une seule personne portant un nom et un prénom donné habite à l'adresse indiquée. Dans le
cas du schéma Situation, la clef est jouée par la cote du livre considéré. Ci-dessus, il y a deux
exemplaires du livre "Les misérables", affectés de cotes différentes. Les clefs sont choisies si possible
- 33 -
de façon à posséder un nombre minimal d'attributs permettant d'identifier l'enregistrement de manière
unique.
Une base de données ou instance d'un schéma de base de données permet alors d'associer à
chaque schéma relationnel une instance sur celui-ci. C'est une réalisation concrète du schéma de base
de données précédemment défini.
Ayant défini ces schémas relationnels, puis enregistré les relations correspondantes, l'utilisateur va
vouloir interroger sa base de données en lui adressant des requêtes. Ces requêtes sont en fait des
opérations algébriques sur la base de données.
La réunion :
On peut réunir deux relations R et S en une seule R ∪ S. Les enregistrements de R ∪ S sont les n-
uplets qui appartiennent à R ou à S. Cela se produit lorsqu'on veut fusionner en un seul fichier tous
les enregistrements provenants de fichiers différents. Un logiciel gérant des bases de données et qui
permet la réunion de deux relations doit faire en sorte de supprimer les doublons provenant
d'enregistrements qui figurent à la fois dans la relation R et dans la relation S, pour n'en garder qu'un
seul.
L'intersection :
On peut intersecter deux relations R et S en une seule R ∩ S. Les enregistrements de R ∩ S sont les
n-uplets qui appartiennent à la fois à R et à S. On ne souhaite garder ici que les enregistrements
communs à plusieurs fichiers. La relation résultante peut être vide.
La différence :
On peut effectuer la différence de deux relations R et S. La relation R – S ou R \ S est constituée des
enregistrements qui appartiennent à R, mais pas à S.
Il existe aussi des opérateurs spécifiques aux bases de données. Les opérateurs les plus courants
sont :
La sélection (ou restriction) :
Elle permet de sélectionner des lignes (i.e. des n-uplets) de la relation selon un critère donné, et
d'éliminer les autres. La sélection s'applique sur une seule relation. On la représentera par le symbole
σ, en précisant en indice les critères retenus. Ainsi, la sélection des lignes de la relation Livre ayant
pour attribut auteur = "Victor Hugo" donne comme résultat :
<Les misérables, Victor Hugo, Editions Dubois, 2002, HUG-0145>
<Les misérables, Victor Hugo, Editions Dubois, 2002, HUG-0146>
<Quatre-Vingt-Treize, Victor Hugo, Editions Dubois, 2002, HUG-0202>
etc...
et le résultat obtenu se note :
σ{auteur = "Victor Hugo"}(Livre)
On obtient une partie de la relation Livre qui correspond à l'ensemble suivant :
A = {<t, "Victor Hugo", e, a, c> ∈ Livre}
La condition de sélection peut être plus complexe, en portant sur plusieurs attributs.
- 34 -
La projection :
Elle permet de sélectionner des attributs de la relation (i.e des colonnes) et d'éliminer les autres. On
la représentera par le symbole π, en indiquant en indice les attributs retenus. Dans l'exemple
précédent, si on ne souhaite conserver que l'information concernant le titre et la cote des ouvrages,
on appliquera une projection sur l'attribut titre et l'attribut cote :
π{titre,cote}(A)
ce qui donne comme résultat :
<Les misérables, HUG-0145>
<Les misérables, HUG-0146>
<Quatre-Vingt-Treize, HUG-0202>
etc...
et correspond à l'ensemble :
{<t, c> | ∃ <e, a>, Livre(t, "Victor Hugo", e, a, c)}
Ci-dessus, la notation Livre(t, "Victor Hugo", e, a, c) peut être vue comme un prédicat prenant la valeur
Vrai ou Faux selon que le n-uplet <t, "Victor Hugo", e, a, c> appartient ou non à Livre. On peut
appliquer à ces prédicats les opérateurs logiques usuels ("et", "ou", "non", voire même "implique" et
"équivaut à"), ainsi que les quantificateurs "il existe" et "quel que soit". On effectue alors du calcul
relationnel.
La jointure :
Elle permet de combiner les informations de plusieurs relations, ayant des noms d'attributs communs.
Elle se note . Considérons par exemple les schémas de relation Livre et Situation. Si l'on veut
savoir si tel livre, identifié par son titre et son auteur, est disponible, il faudra rechercher les cotes de
ces livres dans la relation Livre, puis regarder dans la relation Situation si parmi les cotes retenues,
l'une d'entre elles possède un attribut etat dont la valeur soit "en rayon". Pour cela, il faut relier Livre
à Situation au moyen de l'attribut commun cote. C'est là le rôle de l'opérateur de jointure.
Si Rel1 et Rel2 sont deux relations ayant pour ensemble d'attributs respectivement Att1 et Att2, alors
Rel1 Rel2 est la relation dont l'ensemble d'attributs est Att1 ∪ Att2 et dont les éléments sont les n-
uplets t tels que t|Att1 soit élément de Rel1, et t|Att2 soit élément de Rel2. Les valeurs des attributs de t
communs à Att1 et Att2 sont nécessairement les mêmes.
Dans l'exemple envisagé, la jointure de Livre par Situation donnera une unique relation recensant tous
les livres ainsi que leur situation. On pourra alors sélectionner ceux qui sont en rayon et qui
répondent au titre et à l'auteur voulu.
Si Att1 = Att2, alors Rel1 Rel2 n'est autre que l'intersection Rel1 ∩ Rel2.
Si Att1 et Att2 sont disjoints, alors Rel1 Rel2 n'est autre que le produit cartésien Rel1 × Rel2, dont
les éléments sont constitués de n-uplets dont la première partie appartient à Rel1, et la deuxième à
Rel2.
On peut également effectuer une jointure au moyen d'attributs portant des noms différents, à
condition qu'ils aient le même domaine. Il suffit de préciser en option quels sont les attributs à
identifier.
On peut vérifier que la jointure est associative. La jointure de Rel1 avec la relation {< >} constituée
d'une unique ligne sans attribut redonne Rel1.
- 35 -
Le renommage :
Cette opérateur permet de renommer un attribut. Pour effectuer une jointure entre deux relations, il
est nécessaire en effet que celles-ci aient un attribut en commun. Si ce n'est pas le cas, il est possible
de faire correspondre un attribut de la première relation avec un attribut de la seconde en renommant
l'un des attributs. Notons ρ le renommage. Alors ρ{attribut1 → attribut2}(Nomrelation) désigne l'opérateur qui
renomme dans la relation Nomrelation l'attribut1 en attribut2.
Plus généralement, si Nomrelation est une relation ayant les attributs A1, A2, A3, ..., alors
ρ{A1 → B1, A2 → B2}(Nomrelation)
crée une relation ayant les attributs B1, B2, A3, ... et possédant les éléments t pour lesquels il existe u
dans Rel tels que t(Bi) = u(Ai), pour i = 1, 2.
La division cartésienne :
Soient S et R deux relations, telles que le schéma d'attributs Att2 de S soit inclus dans le schéma
d'attributs Att1 de R. La division de la relation R par la relation S est une relation T dont le schéma
d'attributs est la différence Att1 \ Att2. Ses enregistrements t sont tels que la concaténation de t avec
n'importe quel enregistrement de S donne un enregistrement de R. Par exemple, si R a pour attributs
(auteur, editeur) et S a pour attribut (editeur), alors R/S donne la liste des auteurs publiés par tous les
éditeurs.
Les requêtes dans un logiciel donné sont décrites dans un langage compréhensible par ce logiciel.
C'est le cas du langage SQL dont nous donnerons quelques éléments. La plupart du temps,
l'utilisateur n'a pas à connaître ce langage car une interface sert d'intermédiaire entre l'utilisateur et la
base de donnée, par exemple par l'intermédiaire d'un formulaire à remplir. Ce formulaire est traduit
par l'interface en instructions SQL à exécuter par le serveur de données pour répondre à la requête.
Néanmoins, les logiciels de traitement des données offrent généralement la possibilité d'adresser des
requêtes directement en langage SQL, ce qui permet de formuler exactement les critères souhaités,
apportant des possibilités plus larges que la seule utilisation d'un formulaire dont le cadre d'utilisation
est parfois limité. Signalons également l'architecture dite trois-tiers, séparée entre une interface
accessible à l'utilisateur qui saisit ses requêtes sous forme conviviale et affiche les vues demandées,
un logiciel intermédiaire qui effectue la partie logique du traitement (traduction des demandes en
SQL et traitement des données recherchées sur le serveur) et le serveur de données lui-même. Si le
serveur est par exemple unique, les logiciels intermédiaires peuvent être localisés en plusieurs pays
du monde, ce qui permet une souplesse de fonctionnement et décharge le serveur d'une partie du
traitement. De plus, on peut modifier le module de traduction des requêtes ou de présentation des
réponses pour l'améliorer sans que le serveur gérant les données elles-mêmes n'en soit affecté, pas
plus que ne l'est l'outil utilisé par le client.
- 36 -
Nous nous bornerons à donner la syntaxe de quelques commandes SQL relatives à la consultation
d'une base de données. Il existe évidemment des commandes pour créer une base de données, y
insérer des éléments ou les supprimer, ou modifier ces éléments, ainsi que des commandes pour gérer
les droits d'accès aux bases de données, mais celles-ci sont en général également disponibles pour
l'utilisateur au moyen d'une interface dont les menus permettent de gérer ces commandes.
La structure générale la plus élémentaire d'une commande SQL relative à une requête a la forme
suivante :
SELECT liste des attributs // on opère ici une projection π sur les attributs voulus
FROM liste des relations // on indique ici les relations utilisées
WHERE liste des conditions // on opère ici une sélection σ selon les critères choisis
Union : R ∪ S
SELECT * FROM R
UNION
SELECT * FROM S
Intersection : R ∩ S
SELECT * FROM R
INTERSECT
SELECT * FROM S
Produit cartésien : R × S
SELECT *
FROM R,S
Différence : R – S
SELECT *
FROM R
EXCEPT SELECT * FROM S
Projection : π{nomatt1,nomatt2}(R)
SELECT nomatt1, nomatt2
FROM R
Le langage possède également des commandes telles que IN permettant de tester l'appartenance d'un
élément à un ensemble, ou BETWEEN permettant de sélectionner les éléments numériques
appartenant à un intervalle. Il possède aussi des fonctions dites d'agrégation, qui calcule le minimum,
le maximum, la somme, la moyenne ou le nombre d'éléments d'une suite (MIN, MAX, SUM, AVG,
COUNT). Par exemple, le nombre d'éléments dans la relation R est donnée par :
SELECT COUNT(*)
FROM R
Il peut exister plusieurs formules de l'algèbre relationnelle et donc plusieurs commandes SQL
répondant à une même requête du calcul propositionnel. Chacune de ces formules correspond à un
algorithme qui sera plus ou moins efficace en temps de calcul ou en espace mémoire occupé.
4- Exemples
a) Quels sont les livres de Victor Hugo que possède la bibliothèque ?
Cette requête ne nécessite que la consultation de la relation Livre. Elle consiste simplement à
sélectionner dans Livre les enregistrements dont l'attribut auteur prend la valeur "Victor Hugo". On
cherche donc :
σ{auteur = "Victor Hugo"}(Livre)
et la commande SQL correspondante est :
SELECT *
FROM Livre
WHERE auteur = "Victor Hugo"
On peut aussi effectuer une recherche sur une partie du nom de l'auteur, en utilisant le symbole % qui
remplace toute suite de lettres. La commande suivante :
SELECT *
FROM Livre
WHERE auteur LIKE "%Hug%"
cherchera tous les livres dont l'auteur possède la suite de lettres HUG, quel que soit l'endroit où se
situe cette suite de lettres dans le nom d'auteur.
On peut aussi demander à ce que le résultat de la requête soit affiché par ordre croissant d'un des
champs, par exemple le titre :
SELECT *
- 38 -
FROM Livre
WHERE auteur LIKE "%Hug%"
ORDER BY titre
b) Quels sont les titres de livres de Victor Hugo que possède la bibliothèque ?
C'est une variante de la requête précédente, où l'on ne s'intéresse qu'aux titres. Il suffit d'opérer une
projection selon l'attribut titre sur la vue trouvée en a) :
π{titre}(σ{auteur = "Victor Hugo"}(Livre))
et la commande SQL correspondante est :
SELECT titre
FROM Livre
WHERE auteur = "Victor Hugo"
La vue des réponses est :
{<t> | ∃ e, a, c, Livre(t, "Victor Hugo", e, a, c)}
Dans l'expression ∃ e, a, c, Livre(t, "Victor Hugo", e, a, c), la variable t non affectée d'un quantificateur,
s'appelle variable libre.
e) Quelles sont les adresses des emprunteurs d'un livre de Victor Hugo ?
On doit pour cela consulter les trois instances. On effectue une jointure entre Livre et Situation selon
l'attribut cote, puis une jointure avec Emprunteur selon l'attribut num_inscription. On sélectionne
ensuite les enregistrements dont la valeur de auteur est "Victor Hugo" et celle de etat est "emprunté",
puis on projette selon l'attribut adresse :
π{adresse}(σ{auteur = "Victor Hugo", etat = "emprunté"}(Livre Situation Emprunteur))
On obtient la vue :
- 39 -
{<adr> | ∃ t, e, a, c, d, n, nm, pr, n Livre(t, "Victor Hugo", e, a, c) et Situation(c, "emprunté", d, n) et
Emprunteur(nm ,pr, adr, n)}
La commande SQL est :
SELECT adresse
FROM Livre JOIN Situation ON Livre.cote = Situation.Cote JOIN Emprunteur ON
Situation.num_inscription = Emprunteur.num_inscription
WHERE auteur = "Victor Hugo" AND etat= "emprunté"
La façon dont cette requête est traitée peut modifier notablement le temps de calcul. Si on effectue la
jointure des trois relations, on risque d'obtenir une relation dont la taille est conséquente. Dans le cas
présent, il vaut mieux d'abord effectuer la jointure entre Livre et Situation, puis sélectionner dans
cette jointure les livres dont l'auteur est Victor Hugo, puis seulement après, effectuer la jointure entre
la relation résultat obtenue et la relation Emprunteur avant d'effectuer une sélection sur l'état.
π{adresse}(σ{etat = "emprunté"}(σ{auteur = "Victor Hugo"}(Livre Situation)) Emprunteur))
En SQL, on va donc créer une vue obtenue par jointure de Livre et Situation, à laquelle on applique la
sélection sur l'auteur :
CREATE VIEW Vue AS
SELECT * FROM Livre JOIN Situation ON Livre.cote = Situation.Cote
WHERE auteur = "Victor Hugo";
Puis, on effectue la jointure entre le résultat précédent et la relation Emprunteur :
SELECT adresse
FROM Vue
JOIN Emprunteur ON vue.num_inscription = Emprunteur.num_inscription
WHERE etat= "emprunté";
f) Quels sont les livres empruntés par l'emprunteur dénommé Jean Bonnot ? :
On procède comme au e) :
SELECT Livre.cote, titre, auteur
FROM Livre JOIN Situation ON Livre.cote = Situation.Cote JOIN Emprunteur ON
Situation.num_inscription = Emprunteur.num_inscription
WHERE nom = "Bonnot" AND prenom= "Jean" AND etat = "emprunté"
h) Quelles sont les cotes des livres dont l'édition est comprise entre 2000 et 2010 ?
SELECT cote
FROM Livre
WHERE annee_edition BETWEEN 2000 AND 2010
- 40 -
◆
- 41 -