J'essaie d'apprendre le C et j'ai rencontré l'impossibilité de travailler avec de VRAIMENT grands nombres (c'est-à-dire 100 chiffres, 1000 chiffres, etc.). Je suis conscient qu'il existe des bibliothèques pour ce faire, mais je veux essayer de l'implémenter moi-même.
Je veux juste savoir si quelqu'un a ou peut fournir une explication très détaillée et stupide de l'arithmétique de précision arbitraire.
C'est une question de stockage et d'algorithmes adéquats pour traiter les nombres comme de plus petites pièces. Supposons que vous ayez un compilateur dans lequel un int
ne peut être compris que de 0 à 99 et que vous souhaitez gérer les nombres jusqu'à 999999 (nous ne nous soucierons que des nombres positifs ici pour rester simple).
Pour ce faire, donnez à chaque nombre trois int
s et utilisez les mêmes règles que vous (auriez dû) apprendre à l'école primaire pour l'addition, la soustraction et les autres opérations de base.
Dans une bibliothèque de précision arbitraire, il n'y a pas de limite fixe sur le nombre de types de base utilisés pour représenter nos nombres, tout ce que la mémoire peut contenir.
Addition par exemple: 123456 + 78
:
12 34 56
78
-- -- --
12 35 34
Travailler à partir de l'extrémité la moins significative:
C'est en fait ainsi que l'addition fonctionne généralement au niveau du bit à l'intérieur de votre CPU.
La soustraction est similaire (en utilisant la soustraction du type de base et emprunter au lieu de porter), la multiplication peut être effectuée avec des ajouts répétés (très lent) ou des produits croisés (plus rapide) et la division est plus délicate mais peut être effectuée par décalage et soustraction des nombres impliqué (la longue division que vous auriez apprise enfant).
J'ai en fait écrit des bibliothèques pour faire ce genre de choses en utilisant les puissances maximales de dix qui peuvent tenir dans un entier lorsqu'il est au carré (pour éviter le débordement lors de la multiplication de deux int
s ensemble, comme un _ 16 bits int
étant limité à 0 à 99 pour générer 9 801 (<32 768) lorsqu'il est au carré, ou 32 bits int
utilisant 0 à 9 999 pour générer 99 980 001 (<2 147 483 648)), ce qui a grandement facilité les algorithmes.
Quelques astuces à surveiller.
1/Lors de l'ajout ou de la multiplication de nombres, pré-allouez l'espace maximum nécessaire puis réduisez plus tard si vous trouvez que c'est trop. Par exemple, l'ajout de deux nombres de 100 "chiffres" (où chiffre est un int
) ne vous donnera jamais plus de 101 chiffres. Multiplier un nombre à 12 chiffres par un nombre à 3 chiffres ne générera jamais plus de 15 chiffres (ajoutez le nombre de chiffres).
2/Pour plus de rapidité, normalisez (réduisez le stockage requis pour) les numéros uniquement si cela est absolument nécessaire - ma bibliothèque a eu cela comme un appel séparé afin que l'utilisateur puisse décider entre la vitesse et les problèmes de stockage.
3/L'addition d'un nombre positif et négatif équivaut à une soustraction, et la soustraction d'un nombre négatif revient à ajouter l'équivalent positif. Vous pouvez enregistrer un peu de code en demandant aux méthodes add et soustract de s'appeler après avoir ajusté les signes.
4/Évitez de soustraire les grands nombres des petits puisque vous vous retrouvez invariablement avec des nombres comme:
10
11-
-- -- -- --
99 99 99 99 (and you still have a borrow).
Au lieu de cela, soustrayez 10 de 11, puis annulez-le:
11
10-
--
1 (then negate to get -1).
Voici les commentaires (transformés en texte) d'une des bibliothèques pour lesquelles j'ai dû faire cela. Le code lui-même est malheureusement protégé par des droits d'auteur, mais vous pourrez peut-être sélectionner suffisamment d'informations pour gérer les quatre opérations de base. Supposons dans ce qui suit que -a
et -b
représente des nombres négatifs et a
et b
sont des nombres nuls ou positifs.
Pour l'addition , si les signes sont différents, utilisez la soustraction de la négation:
-a + b becomes b - a
a + -b becomes a - b
Pour soustraction , si les signes sont différents, utilisez l'addition de la négation:
a - -b becomes a + b
-a - b becomes -(a + b)
Aussi une manipulation spéciale pour nous assurer que nous soustrayons les petits nombres des grands:
small - big becomes -(big - small)
La multiplication utilise les mathématiques d'entrée de gamme comme suit:
475(a) x 32(b) = 475 x (30 + 2)
= 475 x 30 + 475 x 2
= 4750 x 3 + 475 x 2
= 4750 + 4750 + 4750 + 475 + 475
La manière dont cela est réalisé implique d'extraire chacun des chiffres de 32 un à la fois (en arrière) puis d'utiliser add pour calculer une valeur à ajouter au résultat (initialement zéro).
Les opérations ShiftLeft
et ShiftRight
sont utilisées pour multiplier ou diviser rapidement un LongInt
par la valeur d'habillage (10 pour les mathématiques "réelles"). Dans l'exemple ci-dessus, nous ajoutons 475 à zéro 2 fois (le dernier chiffre de 32) pour obtenir 950 (résultat = 0 + 950 = 950).
Ensuite, nous avons quitté le décalage 475 pour obtenir 4750 et le décalage droit 32 pour obtenir 3. Ajoutez 4750 à zéro 3 fois pour obtenir 14250 puis ajoutez au résultat de 950 pour obtenir 15200.
Décalage à gauche 4750 pour obtenir 47500, décalage à droite 3 pour obtenir 0. Puisque le décalage à droite 32 est maintenant nul, nous avons terminé et, en fait, 475 x 32 équivaut à 15200.
La division est également délicate mais basée sur une arithmétique précoce (la méthode "gazinta" pour "va dans"). Considérez la longue division suivante pour 12345 / 27
:
457
+-------
27 | 12345 27 is larger than 1 or 12 so we first use 123.
108 27 goes into 123 4 times, 4 x 27 = 108, 123 - 108 = 15.
---
154 Bring down 4.
135 27 goes into 154 5 times, 5 x 27 = 135, 154 - 135 = 19.
---
195 Bring down 5.
189 27 goes into 195 7 times, 7 x 27 = 189, 195 - 189 = 6.
---
6 Nothing more to bring down, so stop.
Par conséquent 12345 / 27
est 457
avec le reste 6
. Vérifier:
457 x 27 + 6
= 12339 + 6
= 12345
Ceci est implémenté en utilisant une variable de réduction (initialement zéro) pour faire tomber les segments de 12345 un par un jusqu'à ce qu'il soit supérieur ou égal à 27.
Ensuite, nous soustrayons simplement 27 de cela jusqu'à ce que nous arrivions en dessous de 27 - le nombre de soustractions est le segment ajouté à la ligne supérieure.
Lorsqu'il n'y a plus de segments à abattre, nous avons notre résultat.
Gardez à l'esprit que ce sont des algorithmes assez basiques. Il existe de bien meilleures façons de faire de l'arithmétique complexe si vos nombres sont particulièrement importants. Vous pouvez regarder quelque chose comme GNU Multiple Precision Arithmetic Library - c'est nettement meilleur et plus rapide que mes propres bibliothèques.
Il a la fâcheuse plutôt malheureuse en ce qu'il se terminera simplement s'il manque de mémoire (une faille plutôt fatale pour une bibliothèque à usage général à mon avis) mais, si vous pouvez regarder au-delà, il est assez bon dans ce qu'il fait.
Si vous ne pouvez pas l'utiliser pour des raisons de licence (ou parce que vous ne voulez pas que votre application se termine sans raison apparente), vous pouvez au moins en obtenir les algorithmes pour les intégrer dans votre propre code.
J'ai également constaté que les bods sur MPIR (une fourchette de GMP) sont plus ouverts aux discussions sur les changements potentiels - ils semblent plus conviviaux pour les développeurs bouquet.
Bien que réinventer la roue soit extrêmement bon pour votre édification et votre apprentissage personnels, c'est aussi une tâche extrêmement importante. Je ne veux pas vous dissuader car c'est un exercice important et que j'ai fait moi-même, mais vous devez être conscient qu'il y a des problèmes subtils et complexes au travail que de plus gros paquets abordent.
Par exemple, la multiplication. Naïvement, vous pourriez penser à la méthode `` écolier '', c'est-à-dire écrire un nombre au-dessus de l'autre, puis faire une longue multiplication comme vous l'avez appris à l'école. exemple:
123
x 34
-----
492
+ 3690
---------
4182
mais cette méthode est extrêmement lente (O (n ^ 2), n étant le nombre de chiffres). Au lieu de cela, les packages bignum modernes utilisent soit une transformée de Fourier discrète soit une transformation numérique pour transformer cela en une opération essentiellement O (n ln (n)).
Et c'est juste pour les entiers. Lorsque vous entrez dans des fonctions plus compliquées sur un certain type de représentation réelle du nombre (log, sqrt, exp, etc.), les choses deviennent encore plus compliquées.
Si vous souhaitez avoir des connaissances théoriques, je vous recommande fortement de lire le premier chapitre du livre de Yap, "Problèmes fondamentaux d'algèbre algorithmique" . Comme déjà mentionné, la bibliothèque gmp bignum est une excellente bibliothèque. Pour les vrais chiffres, j'ai utilisé mpfr et j'ai bien aimé.
Ne réinventez pas la roue: elle pourrait s'avérer carrée!
Utilisez une bibliothèque tierce, telle que GNU MP , qui a fait ses preuves.
Vous le faites essentiellement de la même manière que vous le faites avec un crayon et du papier ...
malloc
et realloc
) selon les besoinsEn règle générale, vous utiliserez comme unité de calcul de base
comme dicté par votre architecture.
Le choix de la base binaire ou décimale dépend de vos désirs pour une efficacité spatiale maximale, une lisibilité humaine et la présence de l'absence de prise en charge mathématique décimale codée binaire (BCD) sur votre puce.
Vous pouvez le faire avec des mathématiques de niveau secondaire. Bien que des algorithmes plus avancés soient utilisés en réalité. Ainsi, par exemple, pour ajouter deux nombres de 1024 octets:
unsigned char first[1024], second[1024], result[1025];
unsigned char carry = 0;
unsigned int sum = 0;
for(size_t i = 0; i < 1024; i++)
{
sum = first[i] + second[i] + carry;
carry = sum - 255;
}
le résultat devra être plus grand de one place
en cas d'ajout pour faire attention aux valeurs maximales. Regarde ça :
9
+
9
----
18
TTMath est une excellente bibliothèque si vous voulez apprendre. Il est construit en C++. L'exemple ci-dessus était idiot, mais c'est ainsi que l'addition et la soustraction se font en général!
Une bonne référence sur le sujet est Complexité informatique des opérations mathématiques . Il vous indique l'espace requis pour chaque opération que vous souhaitez implémenter. Par exemple, si vous avez deux N-digit
numéros, alors vous avez besoin de 2N digits
pour stocker le résultat de la multiplication.
Comme Mitch l'a dit, ce n'est de loin pas une tâche facile à mettre en œuvre! Je vous recommande de jeter un œil à TTMath si vous connaissez C++.
L'une des références ultimes (à mon humble avis) est le TAOCP Volume II de Knuth. Il explique de nombreux algorithmes pour représenter les nombres et les opérations arithmétiques sur ces représentations.
@Book{Knuth:taocp:2,
author = {Knuth, Donald E.},
title = {The Art of Computer Programming},
volume = {2: Seminumerical Algorithms, second edition},
year = {1981},
publisher = {\Range{Addison}{Wesley}},
isbn = {0-201-03822-6},
}
En supposant que vous souhaitiez écrire un grand code entier vous-même, cela peut être étonnamment simple à faire, parlé comme quelqu'un qui l'a fait récemment (bien que dans MATLAB.) Voici quelques-unes des astuces que j'ai utilisées:
J'ai stocké chaque chiffre décimal individuel comme un double nombre. Cela rend de nombreuses opérations simples, en particulier la sortie. Bien qu'il occupe plus de stockage que vous ne le souhaiteriez, la mémoire est bon marché ici et rend la multiplication très efficace si vous pouvez convoluer efficacement une paire de vecteurs. Alternativement, vous pouvez stocker plusieurs chiffres décimaux dans un double, mais attention alors que la convolution pour faire la multiplication peut causer des problèmes numériques sur de très grands nombres.
Stockez un bit de signe séparément.
L'ajout de deux chiffres consiste principalement à ajouter les chiffres, puis vérifiez s'il y a report à chaque étape.
Il est préférable de multiplier une paire de nombres sous forme de convolution suivie d'une étape de report, du moins si vous avez un code de convolution rapide à portée de main.
Même lorsque vous stockez les nombres sous la forme d'une chaîne de chiffres décimaux individuels, la division (également les opérations mod/rem) peut être effectuée pour obtenir environ 13 chiffres décimaux à la fois dans le résultat. C'est beaucoup plus efficace qu'une division qui ne fonctionne qu'avec un seul chiffre décimal à la fois.
Pour calculer une puissance entière d'un entier, calculez la représentation binaire de l'exposant. Utilisez ensuite des opérations de quadrature répétées pour calculer les puissances selon les besoins.
De nombreuses opérations (factorisation, tests de primalité, etc.) bénéficieront d'une opération Powermod. Autrement dit, lorsque vous calculez mod (a ^ p, N), réduisez le résultat mod N à chaque étape de l'exponentiation où p a été exprimé sous forme binaire. Ne calculez pas d'abord un ^ p, puis essayez de le réduire mod N.
Voici un exemple simple (naïf) que j'ai fait en PHP.
J'ai implémenté "Ajouter" et "Multiplier" et l'ai utilisé pour un exemple d'exposant.
http://adevsoft.com/simple-php-arbitrary-precision-integer-big-num-example/
Coupure de code
// Add two big integers
function ba($a, $b)
{
if( $a === "0" ) return $b;
else if( $b === "0") return $a;
$aa = str_split(strrev(strlen($a)>1?ltrim($a,"0"):$a), 9);
$bb = str_split(strrev(strlen($b)>1?ltrim($b,"0"):$b), 9);
$rr = Array();
$maxC = max(Array(count($aa), count($bb)));
$aa = array_pad(array_map("strrev", $aa),$maxC+1,"0");
$bb = array_pad(array_map("strrev", $bb),$maxC+1,"0");
for( $i=0; $i<=$maxC; $i++ )
{
$t = str_pad((string) ($aa[$i] + $bb[$i]), 9, "0", STR_PAD_LEFT);
if( strlen($t) > 9 )
{
$aa[$i+1] = ba($aa[$i+1], substr($t,0,1));
$t = substr($t, 1);
}
array_unshift($rr, $t);
}
return implode($rr);
}