J'aimerais implémenter une grande classe int en C++ comme un exercice de programmation, une classe qui peut gérer des nombres plus grands qu'un long int. Je sais qu'il existe déjà plusieurs implémentations open source, mais j'aimerais écrire la mienne. J'essaie d'avoir une idée de la bonne approche.
Je comprends que la stratégie générale consiste à obtenir le nombre sous forme de chaîne, puis à le diviser en petits nombres (chiffres uniques par exemple) et à les placer dans un tableau. À ce stade, il devrait être relativement simple de mettre en œuvre les différents opérateurs de comparaison. Ma principale préoccupation est de savoir comment je mettrais en œuvre des choses comme l'addition et la multiplication.
Je recherche une approche et des conseils généraux par opposition au code de travail réel.
Choses à considérer pour une grande classe int:
Opérateurs mathématiques: +, -, /, *,% N'oubliez pas que votre classe peut être de chaque côté de l'opérateur, que les opérateurs peuvent être chaînés, que l'un des opérandes peut être un entier, un flottant, un double, etc. .
Opérateurs d'E/S: >>, << C'est ici que vous découvrez comment créer correctement votre classe à partir des entrées utilisateur et comment la formater pour la sortie également.
Conversions/conversions: déterminez dans quels types/classes votre grande classe int doit être convertible et comment gérer correctement la conversion. Une liste rapide comprendrait double et float, et peut inclure int (avec vérification des limites appropriées) et complexe (en supposant qu'il puisse gérer la plage).
Un défi amusant. :)
Je suppose que vous voulez des entiers de longueur arbitraire. Je suggère l'approche suivante:
Considérez la nature binaire du type de données "int". Pensez à utiliser des opérations binaires simples pour émuler ce que font les circuits de votre processeur lorsqu'ils ajoutent des éléments. Si vous êtes intéressé plus en profondeur, pensez à lire cet article wikipedia sur les demi-additionneurs et les additionneurs complets . Vous ferez quelque chose de similaire à cela, mais vous pouvez descendre aussi bas que cela - mais étant paresseux, je pensais que j'allais juste renoncer et trouver une solution encore plus simple.
Mais avant d'entrer dans les détails algorithmiques concernant l'ajout, la soustraction, la multiplication, trouvons une structure de données. Un moyen simple, bien sûr, est de stocker des choses dans un std :: vector.
template< class BaseType >
class BigInt
{
typedef typename BaseType BT;
protected: std::vector< BaseType > value_;
};
Vous voudrez peut-être considérer si vous voulez faire le vecteur d'une taille fixe et si le préallouer. La raison étant que pour diverses opérations, vous devrez passer par chaque élément du vecteur - O (n). Vous voudrez peut-être savoir par vous-même à quel point une opération sera complexe et un n fixe fait exactement cela.
Mais passons maintenant à quelques algorithmes sur le fonctionnement des nombres. Vous pourriez le faire au niveau logique, mais nous utiliserons cette puissance CPU magique pour calculer les résultats. Mais ce que nous allons prendre le relais de l'illustration logique des Half- et FullAdders, c'est la façon dont il traite les portages. Par exemple, considérez comment vous implémenteriez l'opérateur + = . Pour chaque nombre dans BigInt <> :: value_, vous devez les ajouter et voir si le résultat produit une forme de report. Nous ne le ferons pas au niveau du bit, mais comptons sur la nature de notre BaseType (qu'il soit long ou int ou court ou autre): il déborde.
Sûrement, si vous ajoutez deux nombres, le résultat doit être supérieur au plus grand de ces nombres, non? Si ce n'est pas le cas, le résultat a débordé.
template< class BaseType >
BigInt< BaseType >& BigInt< BaseType >::operator += (BigInt< BaseType > const& operand)
{
BT count, carry = 0;
for (count = 0; count < std::max(value_.size(), operand.value_.size(); count++)
{
BT op0 = count < value_.size() ? value_.at(count) : 0,
op1 = count < operand.value_.size() ? operand.value_.at(count) : 0;
BT digits_result = op0 + op1 + carry;
if (digits_result-carry < std::max(op0, op1)
{
BT carry_old = carry;
carry = digits_result;
digits_result = (op0 + op1 + carry) >> sizeof(BT)*8; // NOTE [1]
}
else carry = 0;
}
return *this;
}
// NOTE 1: I did not test this code. And I am not sure if this will work; if it does
// not, then you must restrict BaseType to be the second biggest type
// available, i.e. a 32-bit int when you have a 64-bit long. Then use
// a temporary or a cast to the mightier type and retrieve the upper bits.
// Or you do it bitwise. ;-)
L'autre opération arithmétique est analogue. Heck, vous pouvez même utiliser les stl-functors std :: plus et std :: minus, std :: times et std :: divides, ..., mais faites attention au carry. :) Vous pouvez également implémenter la multiplication et la division en utilisant vos opérateurs plus et moins, mais c'est très lent, car cela recalculerait les résultats que vous avez déjà calculés dans les appels précédents à plus et moins à chaque itération. Il existe de nombreux bons algorithmes pour cette tâche simple, tiliserwikipedia ou le Web.
Et bien sûr, vous devez implémenter des opérateurs standard tels que operator<<
(Il suffit de décaler chaque valeur dans value_ vers la gauche pour n bits, en commençant par la value_.size()-1
... oh et rappelez-vous le carry: ), operator<
- vous pouvez même optimiser un peu ici, en vérifiant d'abord le nombre approximatif de chiffres avec size()
. Etc. Rendez ensuite votre classe utile, par befriendig std :: ostream operator<<
.
J'espère que cette approche vous sera utile!
Il y a une section complète à ce sujet: [The Art of Computer Programming, vol.2: Seminumerical Algorithms, section 4.3 Multiple Precision Arithmetic, pp. 265-318 (ed.3)]. Vous pouvez trouver d'autres informations intéressantes dans le chapitre 4, Arithmétique.
Si vous ne voulez vraiment pas regarder une autre implémentation, avez-vous pensé à ce que vous voulez apprendre? Il y a d'innombrables erreurs à commettre et les découvrir est instructif et dangereux. Il est également difficile d'identifier d'importantes économies de calcul et de disposer de structures de stockage appropriées pour éviter de graves problèmes de performances.
Une question difficile pour vous: comment envisagez-vous de tester votre implémentation et comment proposez-vous de démontrer que son arithmétique est correcte?
Vous voudrez peut-être une autre implémentation pour tester (sans regarder comment elle le fait), mais il faudra plus que cela pour pouvoir généraliser sans attendre un niveau de test atroce. N'oubliez pas de considérer les modes d'échec (problèmes de mémoire insuffisante, de pile, exécution trop longue, etc.).
S'amuser!
l'addition devrait probablement être effectuée dans l'algorithme de temps linéaire standard
mais pour la multiplication, vous pouvez essayer http://en.wikipedia.org/wiki/Karatsuba_algorithm
Une fois que vous avez les chiffres du nombre dans un tableau, vous pouvez faire l'addition et la multiplication exactement comme vous le feriez à la main.
N'oubliez pas que vous n'avez pas besoin de vous limiter à 0-9 en tant que chiffres, c'est-à-dire d'utiliser des octets en tant que chiffres (0-255) et vous pouvez toujours faire de l'arithmétique à main longue comme vous le feriez pour des chiffres décimaux. Vous pouvez même utiliser un tableau de long.
Je ne suis pas convaincu que l'utilisation d'une chaîne soit la bonne voie à suivre - même si je n'ai jamais écrit de code moi-même, je pense que l'utilisation d'un tableau de type numérique de base pourrait être une meilleure solution. L'idée est que vous étendez simplement ce que vous avez déjà de la même manière que le CPU étend un seul bit en un entier.
Par exemple, si vous avez une structure
typedef struct {
int high, low;
} BiggerInt;
Vous pouvez ensuite effectuer manuellement des opérations natives sur chacun des "chiffres" (haut et bas, dans ce cas), en tenant compte des conditions de débordement:
BiggerInt add( const BiggerInt *lhs, const BiggerInt *rhs ) {
BiggerInt ret;
/* Ideally, you'd want a better way to check for overflow conditions */
if ( rhs->high < INT_MAX - lhs->high ) {
/* With a variable-length (a real) BigInt, you'd allocate some more room here */
}
ret.high = lhs->high + rhs->high;
if ( rhs->low < INT_MAX - lhs->low ) {
/* No overflow */
ret.low = lhs->low + rhs->low;
}
else {
/* Overflow */
ret.high += 1;
ret.low = lhs->low - ( INT_MAX - rhs->low ); /* Right? */
}
return ret;
}
C'est un peu un exemple simpliste, mais il devrait être assez évident de savoir comment étendre à une structure qui avait un nombre variable de la classe numérique de base que vous utilisez.
Comme d'autres l'ont dit, faites-le à l'ancienne, mais évitez de tout faire en base 10. Je suggère de tout faire en base 65536 et de stocker les choses dans un éventail de longs.
Utilisez les algorithmes que vous avez appris de la première à la quatrième année.
Commencez par la colonne des unités, puis les dizaines, etc.
Si votre architecture cible prend en charge la représentation BCD (décimale codée binaire) des nombres, vous pouvez obtenir une prise en charge matérielle pour la multiplication/addition à long terme que vous devez effectuer. Obtenir que le compilateur émette des instructions BCD est quelque chose que vous devrez lire ...
Les puces de la gamme Motorola 68K l'avaient. Pas que je sois amer ou quoi que ce soit.
Mon début serait d'avoir un tableau d'entiers de taille arbitraire, utilisant 31 bits et le 32n'd comme débordement.
L'op de démarrage serait ADD, puis MAKE-NEGATIVE, en utilisant le complément à 2. Après cela, la soustraction se déroule trivialement, et une fois que vous avez ajouté/sous, tout le reste est faisable.
Il existe probablement des approches plus sophistiquées. Mais ce serait l'approche naïve de la logique numérique.
Pourrait essayer d'implémenter quelque chose comme ceci:
http://www.docjar.org/html/api/Java/math/BigInteger.Java.html
Vous n'auriez besoin que de 4 bits pour un seul chiffre de 0 à 9
Ainsi, une valeur Int permettrait jusqu'à 8 chiffres chacun. J'ai décidé de m'en tenir à un tableau de caractères, donc j'utilise le double de la mémoire, mais pour moi, il n'est utilisé qu'une seule fois.
De plus, lorsque vous stockez tous les chiffres dans un seul int, cela le complique excessivement et peut même le ralentir.
Je n'ai pas de test de vitesse mais en regardant la version Java de BigInteger, il semble que cela fasse énormément de travail.
Pour moi, je fais ce qui suit
//Number = 100,000.00, Number Digits = 32, Decimal Digits = 2.
BigDecimal *decimal = new BigDecimal("100000.00", 32, 2);
decimal += "1000.99";
cout << decimal->GetValue(0x1 | 0x2) << endl; //Format and show decimals.
//Prints: 101,000.99