Présentation du problème
Dans le cours d'algorithmique de première année, nous avons étudié le modèle de donnée nombre et nous avons vu que la représentation des nombres en machine posait certains problèmes, taille limitée, précision des calculs, etc.
On cherche dans ce chapitre à dépasser la limitation physique imposée par les processeurs qui ne permettent de réaliser des calculs arithmétiques que sur des entiers (ou des flottants) dont la taille ne dépasse pas celle d'un registre machine, soit 64 bits pour la plupart des machines modernes. Nous nous limiterons aux quatre opérations arithmétiques +, −, × et / sur les entiers naturels pour ne pas compliquer inutilement le propos.
Pour dépasser la limitation des registres en machine, on représente un nombre entier à l'aide de la liste de ses chiffres. Comme nous lisons (occidentaux) de la gauche vers la droite, il est naturel de placer le chiffre le plus significatif à gauche et le moins significatif à droite afin de pouvoir estimer rapidement la magnitude du nombre avant même d'en terminer la lecture. Il est donc tentant de calquer cette représentation dans la liste, mais en plaçant les chiffres dans la liste du moins significatif au plus significatif, et en indexant la liste à partir de 0, l'index fournit la puissance de la base associée ce qui facilite l'écriture des algorithmes. Ainsi le nombre \(N=3014\) en base 10 est représenté par la liste \[L=[4,1,0,3]\] et on a par définition \[N=\sum_{i=0}^3L[{\color{yellow}i}]10^{\color{yellow}i}.\] Avec cette représentation, les nombres ne sont limités en taille que par l'espace mémoire disponible dans la machine et il est très facile de coder un entier de plusieurs milliers de chiffres. C'est de cette manière que certains langages de programmation, comme Python, permet d'effectuer des calculs sur des entiers arbitrairement grands.
Pour effectuer une addition, nous allons tout simplement appliquer l'algorithme étudié au primaire. Si \(A\) et \(B\) désignent les deux nombres représentés par un tableau, on additionne les chiffres des deux tableaux terme-à-terme dans l'ordre croissant des indices en propageant une retenue si la somme est supérieure ou égale à la base \(b\). La table ci-dessous traite l'addition des deux entiers \(4.521.078+294.383.493\) comme nous l'avons apprise, la progression du calcul se faisant de la droite vers la gauche de la table, donc en partant des chiffres des unités :
indice | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|
retenue | 1 | 1 | 1 | |||||||
\(A\) | 4 | 5 | 2 | 1 | 0 | 7 | 8 | |||
\(B\) | 2 | 9 | 4 | 3 | 8 | 3 | 4 | 9 | 3 | |
\(A+B\) | 2 | 9 | 8 | 9 | 0 | 4 | 5 | 7 | 1 |
L'algorithme s'en déduit sans grande difficulté. Il faut prendre garde aux débordements de liste, les tailles de \(A\) et \(B\) ne sont pas nécessairement égales. Ici, on règle la différence des tailles en testant à chaque étape si l'on déborde ou non de chacune des listes. L'intégration de la dernière retenue dans le résultat du calcul est obtenue en avançant jusqu'à \(n\) inclus, c'est-à-dire en débordant d'une unité de la taille maximale des deux listes.
Addition(A,B,base):liste d'entiers DONNÉES A,B: listes d'entiers base: entier VARIABLES i,n,retenue,xi,yi: entiers R: liste d'entiers DEBUT n ← max(|A|,|B|) R ← Allouer(n + 1, |entier|) // cellules initialisees a 0 retenue ← 0 i ← 0 TQ (i ≤ n) FAIRE xi ← (i < |A|) ? A[i] : 0 yi ← (i < |B|) ? B[i] : 0 R[i] ← A[i] + B[i] + retenue retenue ← 0 SI (R[i] ≥ base) ALORS retenue ← 1 R[i] ← R[i] - base FSI i ← i + 1 FTQ SI (R[i - 1] > 0) ALORS Effacer(R[i - 1]) FSI RETOURNER R FIN
Nous allons résoudre le problème de la multiplication de deux entiers \(A\cdot B\), là aussi avec l'algorithme étudié au primaire dans lequel chaque chiffre de \(B\) est multiplié à chaque chiffre de \(A\), en commençant par les chiffres des unités.
Observons l'algorithme de la multiplication. Les retenues des produits successifs dans la table sont listées dans l'ordre inverse chronologique pour correspondre aux positions des chiffres de \(B\).
indice | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|
retenues | 1241 | 0010 | 0000 | |||||||
\(A\) | 5 | 2 | 1 | |||||||
\(B\) | 2 | 4 | 9 | 3 | ||||||
\(j=0\) | 1 | 5 | 6 | 3 | ||||||
\(j=1\) | 4 | 6 | 8 | 9 | ||||||
\(j=2\) | 2 | 0 | 8 | 4 | ||||||
\(j=3\) | 1 | 0 | 4 | 2 | ||||||
\(A\cdot B\) | 1 | 2 | 9 | 8 | 8 | 5 | 3 |
Il est inutile de stocker les \(|B|\) lignes des produits successifs par les chiffres \(B[j]\) de \(B\), il suffit d'additionner les résultats au fur et à mesure du calcul, le décalage des indices vers la gauche étant fourni par la variable \(j\) indexant les chiffres de \(B\) :
Multiplication(A,B,base):liste d'entiers données A,B: listes d'entiers base: entier variables i,j,retenue: entiers R: liste; debut R ← Allouer(|A| + |B|, |entier|); // cellules initialisees à 0 j ← 0; // indice des chiffres de B TQ (j < |B|) FAIRE retenue ← 0 i ← 0; // indice des chiffres de A TQ (i < |A|) FAIRE R[i + j] ← R[i + j] + A[i] * B[j] + retenue retenue ← R[i + j] DIV base R[i + j] ← R[i + j] MOD base i ← i + 1 FTQ R[i + j] ← retenue j ← j + 1 FTQ RETOURNER R FIN
Complexité
On rappelle qu'en base \(b\) un nombre \(N\) s'écrit sur \(\lfloor\log_b N\rfloor+1\) chiffres. L'analyse de la complexité ne pose aucune difficulté particulière. Pour l'addition, puisque nous construisons le résultat \(R\) à \(1+n=1+\max\{|A|,|B|\}\) chiffres, il faut \(\Theta(n)\) opérations.
La multiplication demande \(|A|\cdot|B|\) produits de chiffres pour multiplier l'ensemble des chiffres de \(A\) avec ceux de \(B\). Il y a également \(|A|\cdot|B|+1\) additions sur \(\max\{|A|,|B|\}\) chiffres pour obtenir les chiffres du résultat. Donc \(\Theta(|A|\cdot|B|)\) opération globalement.
Travaux pratiques
Les programmes sont à écrire en langage \(C\). Les types préfixés par la lettre u/l sont en fait préfixés par unsigned/long, par exemple uchar désigne le type unsigned char et ullong désigne le type unsigned long long.typedef struct { uint nbc; uint* chiffres; } tnombre;et implantez la fonction tnombre I2N(int n, uint base) qui convertit un entier en nombre multiprécision en base base.
Remarques : (1) Pour augmenter les performances de ces algorithmes, on peut s'appuyer sur l'ual du processeur qui est capable de réaliser des additions sur \(n\) bits (32 ou 64 selon l'architecture). Autrement dit il est beaucoup plus efficace de travailler en base \(2^{32}\) au lieu de la base 10. Les chiffres de la base sont alors saisis sous forme d'entiers naturels représentant leur ordre dans la numération choisie. Par exemple, pour la base 16, on n'utilisera pas les symboles \(A,B,C,D,E,F\) pour coder les 6 chiffres manquants mais tout simplement \(10\), \(11\), \(12\), \(13\), \(14\) ,\(15\).
(2) Il faut noter qu'en langage Python, contrairement au langage \(C\), les calculs multiprécision sont déjà intégrés, les exercices ci-dessus décrivent simplement comment procéder. Il existe évidemment d'autres algorithmes beaucoup plus efficaces pour additionner et multiplier rapidement (cf. TD), ceux présentés ici ne sont rien d'autre que ceux qui ont été appris à l'école primaire.