• Aucun résultat trouvé

2.3 Représentation des nombres réels

2.3.4 Arrondis

En général, un calcul faisant intervenir deux nombres flottants sur n bits ne donne pas un nombre représentable exactement sur n bits. Il suffit par exemple de prendre un nombre décimal non dyadique, comme 1/10 = 0.1. Celui ci s’écrit1.100110011001100 · · ·2× 2−4. (La périodicité du développement n’est pas un hasard, c’est le cas pour tous les

rationnels). La mantisse n’ayant qu’un nombre fini de bits, il est nécessaire de couper ce développement infini. Ainsi, la représentation par un flottant de 0.1 ne sera qu’une approximation. Elle est obtenue en prenant le flottant le plus proche10

Le fait que les réels ne soient représentés qu’approximativement fait que les égalités mathématiques ne tiennent plus avec des flottants. Voici trois exemples de ce qu’on peut obtenir en Python (sur 64 bits) :

>>> a=0.1 >>> b=0 >>> for i in range(10): ... b=b+a ... >>> b==1 False >>> a=2.**1000 >>> a==a+1 True >>> a, b, c=1, 2**-53, 1 >>> a+b-c==a-c+b False

Pour le premier exemple, 0.1 n’est représenté en mémoire que sous forme arrondie. La boucle à pour objet de calculer 10 × 0.1 en faisant 10 additions. Les erreurs d’approximation se cumulent, et au final on obtient un résultat très proche de 1, mais qui n’est pas 1 (j’obtiens 1 − 2−53).

10. Le lecteur voulant des précisions sur les règles d’approximation pourra se reporter à l’adresse : http://fr.wikipedia.org/wiki/IEEE_ 754.

2.3. REPRÉSENTATION DES NOMBRES RÉELS Lycée Masséna

Pour le deuxième, l’explication est la suivante : sur 64 bits, il y a 52 bits de mantisse. Le plus petit flottant strictement supérieur à 21000est donc (1 + 2−52) × 21000. Ainsi, 21000+ 1 est indiscernable de 21000. Plus exactement

le résultat de l’addition 21000+ 1 est arrondi au flottant le plus proche, à savoir 21000lui même. D’où l’égalitéa==a+1

qui peut paraître choquante !

Pour le dernier exemple, les opérations + et - ayant même priorité, elles sont évaluées de gauche à droite. Or ici 1 + 2−53est arrondi au flottant le plus proche, à savoir 1 lui-même. Donc dans l’exemple,a+b-c vaut exactement zéro. Par contre,a-c+b vaut b, car a et c sont tous deux égaux (à 1).

Il ne faut surtout pas croire à la lecture de ces exemples que les résultats obtenus via des opérations sur les nombres à virgule sont complètement faux en informatique. Néanmoins, il faut être conscient que dans le monde des flottants, les égalités mathématiques ne sont plus vérifiées « qu’à ε près », à cause des erreurs d’arrondi et de l’impossibilité de représenter de manière exacte les réels. La leçon à retenir des exemples et la suivante : sauf cas particuliers bien précis,

Le test d’égalité entre deux flottants n’est, en général, pas pertinent !

On se contentera d’un test de la forme |a − b| < , où  est un « petit » flottant, dépendant du problème. Par exemple, pour tester si un réel x est une racine d’un polynôme P , on se contentera par exemple de |P (x)| ≤ 2−20. Pour conclure, considérons le code Python suivant, qui résout une équation du second degré en donnant les racines réelles.

from math import sqrt def trinome(a,b,c):

assert a!=0, "ce n'est pas un trinôme du second degré!" Delta=b**2-4*a*c if Delta<0: print("Pas de racines !") elif Delta>0: r=sqrt(Delta) x1=(-b-r)/(2*a) x2=(-b+r)/(2*a)

print("Il y a deux racines distinctes, qui sont: ", x1, "et", x2) else:

print("Il y a une racine double, qui est: ", -b/(2*a))

Prenons un premier exemple :

>>> trinome(1,-1,-1)

Il y a deux racines distinctes, qui sont: -0.6180339887498949 et 1.618033988749895

On obtient des valeurs approchées très correctes de 1±

√ 5

2 . Cherchons maintenant les racines du polynôme x

2+ 2−600x.

On travaille sur 64 bits, ainsi les coefficients et les racines (0 et −2−600) sont tous représentables de manière exacte par un flottant.

>>> trinome(1,2**(-600),0)

Il y a une racine double, qui est: -1.204959932551442e-181

L’explication est simple : le discriminant du trinôme, qui est 2−1200, n’est pas représentable sur 64 bits. Il est arrondi à zéro, ce qui explique le déroulement : le discriminant étant trop petit pour être représentable, le programme conclut à l’existence d’une racine double alors qu’il y a deux racines distinctes, très proches. Voici un dernier exemple avec le polynôme (x − 0.1)2= x2− 0.2 + 0.01 :

>>> trinome(1,-0.2,0.01)

Il y a deux racines distinctes, qui sont: 0.09999999868291098 et 0.10000000131708903

Ici « l’erreur » est légèrement différente : les coefficients du polynôme ne sont pas représentables exactement, et le discriminant du polynôme « flottant » est non nul, ce n’est pas du à une erreur d’arrondi dans l’opération. Le programme renvoie donc deux racines distinctes, assez proches de 0.1.

Ce code fonctionne très bien dans la plupart des situations, seulement il faut garder à l’esprit que les coefficients sont représentés à un petit ε près, de même que le résultat du calcul des racines.

Chapitre 3

Analyse d’algorithmes

Introduction

Le but de ce chapitre est d’étudier de manière théorique les algorithmes. On va donner les outils permettant de répondre aux trois questions suivantes :

— l’algorithme s’arrête-t-il un jour ?

— est-ce qu’il fait bien ce qu’il est sensé faire ? Autrement dit, est-il correct ? — combien de temps met-il à s’exécuter ?

Le premier point s’appelle la terminaison de l’algorithme, le deuxième sa correction et le dernier sa complexité. Revoyons la notion d’algorithme en informatique.

Définition 3.1. Un algorithme est une fonction qui prend des données en argument, effectue une séquence finie non ambiguë d’instructions, et renvoie un résultat.

Étendons un peu cette définition en donnant une liste de points caractérisant un algorithme, par Donald Knuth1 : — finitude : « Un algorithme doit toujours se terminer après un nombre fini d’étapes. »

— définition précise : « Chaque étape d’un algorithme doit être définie précisément, les actions à transposer doivent être spécifiées rigoureusement et sans ambiguïté pour chaque cas. »

— entrées : « des quantités qui lui sont données avant qu’un algorithme ne commence. Ces entrées sont prises dans un ensemble d’objets spécifié. »

— sorties : « des quantités ayant une relation spécifiée avec les entrées. »

— rendement : « [. . . ] toutes les opérations que l’algorithme doit accomplir doivent être suffisamment basiques pour pouvoir être en principe réalisées dans une durée finie par un homme utilisant un papier et un crayon. » Pour décrire un algorithme, on lui donne en général un nom, on précise quels sont les paramètres (les entrées) et le résultat (les sorties) qu’il est sensé renvoyer. On précise aussi de quelle manière il agit sur son environnement : modification de la mémoire, affichage éventuel à l’écran, etc... Tout ceci constitue la spécification de l’algorithme. Dans nos algorithmes, outre les opérations d’affectations, d’entrée/sortie et de manipulations des variables, on peut trouver des blocs simples :

— bouclesfor ; — boucleswhile ;

— blocs conditionnelsif, elif,...,else. Ce découpage en blocs simples est essentiel.

3.1

Terminaison

Pour montrer qu’un algorithme termine quel que soit le jeu de paramètres passé en entrée respectant la spécifi- cation, il faut montrer que chaque bloc élémentaire décrit ci-dessus termine ! Or, les bouclesfor et les instructions conditionnelles terminent forcément. Le seul souci pourrait venir d’une bouclewhile.

3.1. TERMINAISON Lycée Masséna