Adloun

La représentation des nombres

Cours complet · informatique (tronc commun des prépas scientifiques), chapitre 11 · prépas scientifiques, tronc commun

Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre

<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>11.1 Introduction et motivation

Le chapitre 1 l'avait promis en passant : 0.1 + 0.2 ne vaut pas 0.3, et il faudrait un jour comprendre pourquoi. Ce jour est arrivé. Une machine ne manipule que des suites finies de bits — des et des par paquets de taille fixe — et tout nombre doit s'y loger : les entiers, qui y tiennent exactement mais pas tous ; les réels, qui n'y tiennent presque jamais et qu'on y approche par les flottants. De cette finitude découlent des phénomènes que tout programmeur scientifique rencontre : le débordement des entiers à taille fixe (que Python masque, mais pas les autres langages ni les bibliothèques numériques), l'impossibilité de représenter , l'absorption des petits nombres par les grands, l'annulation catastrophique des soustractions — et l'interdiction, déjà décrétée au chapitre 1, de comparer des flottants par ==.

L'objectif du chapitre est fixé par le programme : sans formalisation théorique, comprendre les enjeux de la représentation pour expliquer les difficultés rencontrées et les précautions à prendre. On saura, à la fin, répondre en connaissance de cause aux trois questions du calcul numérique : que peut contenir un mot machine ? qu'est-ce qu'un flottant, exactement ? et que vaut une égalité entre deux résultats de calcul ?

11.2 Les entiers positifs sur des mots de taille fixe

11.2.1 Le mot machine

Définition 11.1Bit, mot, écriture binaire

Le bit est l'unité d'information : ou . Un mot de taille est une suite de bits ; il représente naturellement l'entier positif

— l'écriture en base deux, où chaque position vaut une puissance de (comme chaque position décimale vaut une puissance de ). Exemple sur bits : .

Proposition 11.2Plage représentable

Un mot de bits représente exactement les entiers de : valeurs distinctes, ni une de plus ni une de moins — il y a précisément suites de bits. Repères : bits à (les pixels du chapitre 8 !), bits environ , bits environ .

Exemple 11.3Le débordement

Que vaut sur bits ? L'addition binaire propage une retenue qui sort du mot : — un bit de trop, qui est perdu. Résultat stocké : . C'est le débordement (overflow) : sur bits, l'arithmétique est silencieusement modulo . Le bogue du milieu de la dichotomie (chapitre 5) en était une manifestation : peut déborder même quand le résultat final tiendrait ; le compteur kilométrique qui repasse à zéro en est l'image fidèle.

iRemarque

La conversion de base n'est pas un objectif du programme — mais savoir lire une écriture binaire courte et retrouver les puissances de (, déjà rencontré au chapitre 5) reste indispensable pour raisonner sur les plages et les débordements. En Python, bin(211) affiche '0b11010011' et int(&quot;11010011&quot;, 2) revient — utile pour expérimenter.

11.3 Les entiers signés : le complément à deux

11.3.1 Représenter les négatifs

Définition 11.4Complément à deux

Sur des mots de bits, la convention du complément à deux représente les entiers de : les mots dont le bit de tête vaut codent les positifs à comme avant ; les mots de bit de tête codent les négatifs, le mot représentant l'entier . Autrement dit, l'entier négatif est codé par le mot de valeur . Sur bits :

motcomme entier positifen complément à deux
le plus grand positif
le plus petit négatif
ImportantPourquoi cette convention ?

Parce qu'elle rend l'addition uniforme : additionner deux mots modulo donne le bon résultat que les opérandes soient positifs ou négatifs — le matériel n'a qu'un seul circuit d'addition. Vérification sur bits : se calcule — c'est bien . Et l'opposé de se calcule mécaniquement : inverser tous les bits puis ajouter (car , donc ).

AttentionL'asymétrie et ses pièges

La plage est asymétrique : sur bits, existe mais n'existe pas. Conséquence troublante : l'opposé de sur bits… déborde, et le calcul mécanique (inverser, ajouter ) redonne lui-même — ! Ce piège réel a son écho dans toutes les bibliothèques à entiers fixes. Plus généralement, les débordements signés font passer brutalement du plus grand positif au plus petit négatif : sur bits — le compteur qui « fait le tour » par le côté des négatifs.

11.4 Les entiers de Python : la multi-précision

Définition 11.5Entiers multi-précision

Les entiers de Python (int) ne vivent pas dans un mot de taille fixe : leur taille s'ajuste au besoin (on dit multi-précision), et 2 ** 1000 se calcule exactement — chiffres décimaux, aucun débordement, jamais. C'est un choix du langage, précieux pour les mathématiques (les exacts du chapitre 5), et qui distingue Python de C, Java ou des tableaux numpy, où les entiers restent à taille fixe.

ImportantLe prix : une complexité qui n'est plus

Tout le cours a compté « une addition ». C'est vrai tant que les nombres tiennent dans un mot machine — et faux pour les grands entiers multi-précision : additionner deux entiers de chiffres coûte , les multiplier coûte davantage encore. Ainsi puissance(2, n) par exponentiation rapide fait multiplications… mais les derniers produits portent sur des nombres de bits, et leur coût domine tout : la complexité réelle est bien plus que opérations élémentaires. Le programme signale honnêtement cette difficulté à évaluer la complexité des opérations sur les grands entiers : nos analyses en « opérations élémentaires » supposent des nombres de taille bornée — l'hypothèse mérite d'être dite quand elle cesse d'être vraie (cryptographie, factorielles, combinatoire exacte).

11.5 Réels, décimaux, flottants

11.5.1 Trois ensembles à ne pas confondre

Définition 11.6Réels, décimaux, flottants
  • Les réels () forment un continu : presque tous (, , ) ont un développement infini — aucun mot fini ne peut les représenter exactement.
  • Les décimaux sont les réels à écriture décimale finie ( ; ) — ceux de la vie courante et des mesures.
  • Les flottants (type float) sont les nombres effectivement représentables dans un mot machine de taille fixe : des fractions binaires finies. Ensemble fini, troué, borné.

La surprise centrale du chapitre : décimal n'implique pas flottant. Le nombre a un dénominateur divisible par : en base deux, son développement est infini (, périodique) — exactement comme en base dix. Le flottant nommé 0.1 est donc une approximation de , et tout part de là :


>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False

Trois arrondis (sur , sur , sur leur somme) ne retombent pas sur l'arrondi de : il n'y a là aucun bogue — seulement de l'arithmétique en base deux.

11.5.2 Mantisse et exposant

Définition 11.7Représentation flottante

Un flottant s'écrit, à la manière de la notation scientifique mais en base deux :

où la mantisse est un nombre binaire de la forme (un nombre fixe de bits après la virgule) et l'exposant un entier signé d'une plage fixe. Le mot machine se partage entre le bit de signe, l'exposant et la mantisse — pour les flottants usuels de bits : . Le nombre , qui n'a pas d'écriture , reçoit une représentation spéciale dédiée. (C'est tout ce qu'il faut savoir : la norme technique sous-jacente, ses infinis et ses cas spéciaux ne sont pas au programme.)

Proposition 11.8Ce que la forme implique
  • Précision relative : bits de mantisse offrent environ chiffres décimaux significatifs — relatifs à la taille du nombre : les flottants sont denses près de , espacés loin de lui. Entre deux flottants consécutifs voisins de , l'écart est ; voisins de , l'écart dépasse… .
  • Au-delà de , les entiers eux-mêmes ont des trous : n'est pas représentable en flottant — float(253) == float(253 + 1) vaut True. (Les int de Python, eux, restent exacts : deux mondes.)
  • Plage : l'exposant borné limite les flottants à environ — au-delà, débordement.

11.6 La précision des calculs

11.6.1 Deux phénomènes à connaître

Exemple 11.9L'absorption

Additionner un petit nombre à un grand peut ne rien changer : pour aligner les exposants, la mantisse du petit est décalée jusqu'à disparaître.


>>> 1e16 + 1.0 == 1e16
True
>>> (1e16 + 1.0) - 1e16
0.0
>>> 1e16 - 1e16 + 1.0
1.0

Les deux dernières lignes calculent « la même chose » mathématiquement — et donnent puis : l'addition flottante n'est pas associative. Conséquence algorithmique réelle : sommer un million de petits termes après un grand les perd tous ; les sommer d'abord entre eux les sauve — l'ordre de sommation est un choix d'algorithme.

Exemple 11.10L'annulation catastrophique

Soustraire deux nombres proches détruit les chiffres significatifs : les chiffres de tête, égaux, s'annulent, et il ne reste que les chiffres du fond — c'est-à-dire le bruit d'arrondi. Le chapitre 4 l'avait rencontrée sans la nommer : la variance par sur des données autour de rend des résultats absurdes (parfois négatifs !), quand la formule en deux passages, qui soustrait avant d'élever au carré, reste saine. Autre classique : pour ,


>>> x = 1e-8
>>> (1 - math.cos(x)) / x**2          # formule naïve : numérateur annulé
0.0

alors que la vraie valeur est (développement limité du cours de mathématiques). Le remède est toujours le même : reformuler pour éviter la soustraction de proches — ici , exact et stable.

11.6.2 Les précautions

Méthode : Les règles du calcul flottant

  • Jamais == entre flottants calculés (chapitre 1, désormais expliqué) : comparer par écart, abs(a - b) &lt; eps — absolu pour des grandeurs d'ordre connu, relatif (abs(a - b) &lt; eps * max(abs(a), abs(b))) sinon.
  • Jamais de flottant comme condition d'arrêt exacte : la boucle while x != 1.0 du chapitre 1 ne s'arrête pas ; on borne par un seuil ou l'on compte en entiers.
  • Méfiance sur les soustractions de proches (annulation) et les additions de disparates (absorption) : réordonner, reformuler.
  • Les entiers déguisés se traitent en entiers : compter, indexer, dénombrer en int — exacts en Python ; le flottant est pour les grandeurs continues.
  • Pour voir un flottant : print(f&quot;{x:.20f}&quot;) affiche au-delà des chiffres et révèle l'approximation que l'affichage courant arrondit poliment.

<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>11.7 Exercices résolus

Niveau (Application directe du cours)

Exercice 1 : Lire des mots

Sur bits : donner la valeur des mots et , d'abord comme entiers positifs, puis en complément à deux. Donner le mot codant .

Démonstration (Solution)

— bit de tête : il vaut dans les deux conventions. comme positif ; bit de tête , donc en complément à deux : . Pour : son code est — ou par la recette mécanique : , bits inversés , plus un : . ✓ (Les deux méthodes — l'addition de et l'inversion-plus-un — doivent toujours coïncider : se vérifier l'une par l'autre est le contrôle naturel.)

Exercice 2 : Simuler la machine en Python

Écrire vers_signe(mot, k) (la valeur en complément à deux d'un entier ) et addition_machine(x, y, k) (l'addition signée telle que la ferait un processeur bits), puis reproduire les deux pièges du cours : et sur bits.

Démonstration (Solution)

def vers_signe(mot: int, k: int) -> int:
    """Interprète un mot de k bits en complément à deux."""
    assert 0 <= mot < 2 ** k
    return mot if mot < 2 ** (k - 1) else mot - 2 ** k

def addition_machine(x: int, y: int, k: int) -> int:
    """Additionne deux entiers signés comme un processeur k bits (modulo 2^k)."""
    return vers_signe((x + y) % (2 ** k), k)

assert addition_machine(127, 1, 8) == -128       # le tour du compteur
assert addition_machine(-1, 1, 8) == 0           # l'addition uniforme fonctionne
assert addition_machine(-128, -128, 8) == 0      # double débordement !
def oppose_machine(x: int, k: int) -> int:
    return vers_signe((-x) % (2 ** k), k)
assert oppose_machine(-128, 8) == -128           # -(-128) == -128 : l'asymétrie

Le couple « interpréter / réduire modulo » suffit à simuler fidèlement l'arithmétique machine — et les assertions sont les pièges du cours, devenus exécutables. (Python, justement parce que ses entiers ne débordent pas, est l'outil idéal pour étudier les débordements des autres : le modèle est dans le modulo.)

Exercice 3 : Décimal, mais pas flottant

Parmi ; ; ; ; : lesquels sont exactement représentables en binaire ? Donner leur écriture binaire le cas échéant, et énoncer le critère général.

Démonstration (Solution)

Un décimal est représentable en binaire fini si et seulement si, écrit en fraction irréductible, son dénominateur est une puissance de . Ainsi : ✓ ; ✓ ; ✓ ; mais et ont un facteur au dénominateur : développement binaire infini (périodique), donc arrondi en machine. Vérification expérimentale :


>>> print(f"{0.625:.20f}")
0.62500000000000000000
>>> print(f"{0.1:.20f}")
0.10000000000000000555

(Le critère est le miroir exact de la base dix, où échoue à cause du facteur : chaque base a ses fractions interdites — la base deux interdit le , et le système décimal de la vie courante en est truffé.)

Niveau (Application avec raisonnement intermédiaire)

Exercice 4 : Mesurer l'epsilon machine

Écrire un programme qui détermine expérimentalement le plus petit tel que 1.0 + eps != 1.0, et confronter au annoncé par la taille de mantisse. Que donne la même expérience autour de au lieu de ?

Démonstration (Solution)

eps, q = 1.0, 0
while 1.0 + eps / 2 != 1.0:
    # Invariant : 1.0 + eps != 1.0 ; variant : la boucle s'arrête quand
    # eps/2 passe sous l'écart entre flottants voisins de 1
    eps, q = eps / 2, q + 1
print(q, eps)            # 52,  2.220446049250313e-16  (= 2**-52)

gros = float(2 ** 20)
e2, q2 = 1.0, 0
while gros + e2 / 2 != gros:
    e2, q2 = e2 / 2, q2 + 1
print(q2, e2)            # 32,   2.3e-10 : 2**-52 * 2**20

Autour de , l'écart entre flottants consécutifs est — la précision relative de bits de mantisse. Autour de , le même calcul donne : l'écart absolu a été multiplié par , exactement le facteur d'échelle. (C'est la définition vivante de « précision relative » : les flottants offrent partout les mêmes chiffres significatifs, donc une précision absolue proportionnelle à la grandeur — d'où la règle des comparaisons à écart relatif pour les grandes valeurs.)

Exercice 5 : La somme dans le bon ordre

On veut calculer pour . Comparer la sommation par croissant et par décroissant (les deux en float), identifier la plus précise, expliquer par l'absorption.

Démonstration (Solution)

n = 10_000_000
croissant = 0.0
for k in range(1, n + 1):
    croissant += 1 / k**2
decroissant = 0.0
for k in range(n, 0, -1):
    decroissant += 1 / k**2
# croissant   : 1.6449339668472596   (faux dès le 12e chiffre)
# decroissant : 1.6449339668482315   (= la somme exacte arrondie !)

L'écart entre les deux ordres se loge dans les derniers chiffres, et c'est l'ordre décroissant qui est le plus fidèle. Raison : en sommant par croissant, l'accumulateur devient vite grand () tandis que les termes deviennent minuscules () — chaque petit terme est partiellement absorbé par l'accumulateur. En sommant par décroissant, les petits termes s'agrègent d'abord entre eux en sous-totaux de leur taille, qui pèsent ensuite face aux grands : moins de pertes. Règle pratique : sommer du plus petit au plus grand. (L'écart reste ici modeste — la série converge vite — mais le principe gouverne le calcul scientifique réel ; les bibliothèques sérieuses utilisent des sommations compensées qui annulent presque totalement ces pertes, raffinement hors programme dont l'exercice 25 de la banque donne l'idée.)

Exercice 6 : Le trou à

Vérifier expérimentalement que float(253) == float(253 + 1) mais que 253 == 253 + 1 est faux ; expliquer par la mantisse ; en déduire une règle sur le passage int float en Python.

Démonstration (Solution)

Les int de Python sont multi-précision : et y sont distincts, et la comparaison entière le confirme. Mais la mantisse d'un flottant porte bits après le de tête : elle représente exactement les entiers jusqu'à , puis un entier sur deux (l'écart entre flottants consécutifs vaut dans ), un sur quatre ensuite, etc. , impair et hors plage exacte, est arrondi au flottant voisin : les deux conversions coïncident. Règle : convertir en float un entier au-delà de peut le modifier — les identifiants, montants en centimes ou compteurs géants restent donc en int de bout en bout, et l'on se méfie des fonctions qui convertissent en douce (math.sqrt, moyennes, numpy). (L'erreur réelle correspondante : des identifiants à chiffres passés par un tableur ou un float ressortent arrondis — silencieusement faux, le pire genre de faux du chapitre 1.)

Exercice 7 : Reformuler pour survivre — la racine qui annule

Pour grand, la formule s'annule catastrophiquement. Le constater pour , proposer la reformulation stable (multiplier par la quantité conjuguée), comparer les deux résultats à la valeur de référence.

Démonstration (Solution)

import math
x = 1e12
naive = math.sqrt(x + 1) - math.sqrt(x)
stable = 1 / (math.sqrt(x + 1) + math.sqrt(x))
# naive  : 5.00003807246685e-07
# stable : 4.999999999998749e-07

Les deux racines valent et diffèrent de : leur soustraction efface chiffres communs sur les disponibles — il ne reste que chiffres fiables, et la version naïve est fausse dès le cinquième chiffre. La reformulation par la quantité conjuguée,

remplace la soustraction de proches par une addition : tous les chiffres survivent. (La quantité conjuguée du cours de mathématiques n'est donc pas qu'une astuce de calcul de limites : c'est un outil de stabilité numérique — même équivalence mathématique, comportements machine opposés, comme la variance du chapitre 4.)

Niveau (Raisonnement subtil ou plusieurs étapes)

Exercice 8 : Le coût réel des grands entiers

Mesurer le temps de a * b pour des entiers Python de chiffres (, nombres aléatoires), vérifier que la multiplication n'est pas , et en déduire une critique honnête de l'analyse « multiplications » de l'exponentiation rapide appliquée à 2 ** n.

Démonstration (Solution)

import random, time
for d in (1000, 10_000, 100_000):
    a = random.randrange(10 ** (d - 1), 10 ** d)
    b = random.randrange(10 ** (d - 1), 10 ** d)
    debut = time.perf_counter()
    for _ in range(100):
        c = a * b
    print(d, (time.perf_counter() - debut) / 100)
# temps typiques :  3e-6 s ;  9e-5 s ;  3e-3 s  — x30 quand d x10 : ni O(1) ni O(d)

Le temps croît environ comme (exposant de Karatsuba : — l'algorithme interne de Python est un « diviser pour régner » sur les chiffres, intermédiaire entre l'école et l'optimal) : la multiplication des grands entiers est une vraie fonction croissante de leur taille. Critique de l'analyse du chapitre 5 : pour puissance(2, n), les multiplications portent sur des nombres de plus en plus longs — le dernier produit manipule bits et coûte à lui seul plus que tous les précédents ; le coût total est dominé par la taille du résultat ( bits), pas par le compte des multiplications. L'analyse « » reste juste en nombre de multiplications, et c'est la bonne mesure quand les nombres sont bornés (flottants, calcul modulaire — où la réduction modulo borne tout, exercice 7 du chapitre 5) ; elle cesse d'être un temps quand les opérandes enflent. (C'est exactement la « difficulté à évaluer la complexité » que signale le programme : toute analyse repose sur un modèle de coût, et le modèle « opération arithmétique » a un domaine de validité qu'il faut savoir énoncer.)

Exercice 9 : L'égalité des flottants, version raisonnée

Écrire proches(a, b, rel, abs_) combinant tolérance relative et absolue : . Justifier chacun des deux termes par un scénario où l'autre seul échoue, et tester sur : deux calculs de contre ; deux grandes valeurs voisines ; deux valeurs minuscules dont l'une vaut .

Démonstration (Solution)

def proches(a: float, b: float, rel: float = 1e-9, abs_: float = 1e-12) -> bool:
    return abs(a - b) <= max(rel * max(abs(a), abs(b)), abs_)

assert proches(0.1 + 0.2, 0.3)                    # le classique
assert proches(1e15 + 1.0, 1e15)                  # grands : le RELATIF juge
assert proches(1e-15, 0.0)                        # près de zéro : l'ABSOLU sauve
assert not proches(1.0, 1.001)

Pourquoi le relatif : pour des grandeurs de l'ordre de , l'écart d'arrondi normal est de l'ordre de (précision relative ) — une tolérance absolue fixe de déclarerait différents deux calculs parfaitement concordants. Pourquoi l'absolu : si , le terme relatif vaut , et le test n'est vrai que pour exactement — comparer à zéro exigerait l'exactitude, précisément ce qu'on fuit ; le plancher absolu rétablit une zone de tolérance autour de . Le max des deux critères couvre ainsi toute la droite réelle. (C'est, à la lettre, le contrat de math.isclose de la bibliothèque standard — qu'on a maintenant les moyens de lire en connaisseur, et dont on comprend les deux paramètres.)

Exercice 10 : L'algorithme déstabilisé — la suite piégée

La suite , , vérifie mathématiquement (le démontrer). Calculer en flottants par la récurrence, comparer à , expliquer la divergence — et proposer le calcul correct.

Démonstration (Solution)

Mathématiques : l'équation caractéristique a pour racines et (produit , somme ✓) ; la solution générale est , et les conditions initiales donnent , : exactement.

Machine :


u, v = 1.0, 1/3
for _ in range(29):
    u, v = v, 13/3 * v - 4/3 * u
# v vaut   -14.3  au lieu de 3**-30   4.9e-15

Catastrophe totale : calculé vaut environ — ni la bonne valeur, ni le bon ordre de grandeur, ni même le bon signe. Explication : n'est pas représentable — le machine vaut avec , soit : la composante parasite , invisible au départ, est multipliée par à chaque pas et dévore la solution vraie qui, elle, est divisée par . Au rang , le parasite vaut : il dépasse dès et tout est perdu. L'algorithme est instable : il amplifie exponentiellement la moindre erreur d'entrée — aucun raffinement d'arrondi ne le sauvera.

Le calcul correct : ne pas utiliser cette récurrence — calculer directement 3.0 ** -n (stable), ou dérouler la récurrence en exact avec le type Fraction (chapitre 4 des mathématiques de l'atelier : l'arithmétique rationnelle de Python est exacte, au prix de numérateurs qui enflent). (Leçon finale du chapitre : la frontière ne passe pas entre « bonnes et mauvaises machines » mais entre algorithmes stables et instables — une récurrence mathématiquement irréprochable peut être numériquement suicidaire, et c'est l'analyse, pas le test sur trois valeurs, qui le révèle : machine était encore juste à près.)

Synthèse du chapitre (à retenir)
  • Entiers positifs sur bits : , plage ( mots) ; arithmétique silencieusement modulo : le débordement ( sur 8 bits).
  • Complément à deux : bit de tête valeur ; plage asymétrique ; un seul circuit d'addition (modulo ) pour signés et non signés ; opposé inverser les bits ; pièges : , .
  • Entiers Python : multi-précision, jamais de débordement, exacts — mais opérations non sur les grands nombres : le modèle « arithmétique en temps constant » vaut pour des nombres bornés, à dire explicitement (exponentiation rapide : multiplications, pas temps si le résultat enfle).
  • Réels décimaux flottants : un décimal est flottant ssi son dénominateur irréductible est une puissance de — et ne le sont pas, d'où 0.1 + 0.2 != 0.3 ; flottant (mantisse bits chiffres, exposant borné , zéro à part) ; précision relative : écart près de , entiers troués au-delà de .
  • Deux fléaux : absorption ( ; addition non associative — sommer du petit au grand) ; annulation catastrophique (soustraire des proches efface les chiffres : variance en un passage, — reformuler : quantité conjuguée, ).
  • Précautions : jamais == ni condition d'arrêt exacte sur flottants calculés ; comparaison mixte relative absolue (le plancher absolu pour le voisinage de — c'est math.isclose) ; entiers de comptage en int de bout en bout ; f&quot;{x:.20f}&quot; pour voir la vérité.
  • Stabilité : un algorithme qui amplifie les erreurs (récurrence à racine parasite ) est faux en machine même s'il est juste en mathématiques — l'erreur initiale inévitable ( arrondi) décide de tout.

11.8 Exercices d'entraînement

Cette banque d'exercices, classée par thème, couvre l'intégralité du chapitre. La numérotation prolonge celle des dix exercices résolus. Légende : application directe, raisonnement intermédiaire, approfondissement ; le symbole signale un classique incontournable.

A. Entiers à taille fixe

B. Flottants : représentation

C. Précision des calculs

D. Études

Continuer sur Adloun : animation, QCM, fiches, exercices