So implementieren Sie big int in C ++


80

Ich möchte eine große int-Klasse in C ++ als Programmierübung implementieren - eine Klasse, die Zahlen verarbeiten kann, die größer als eine lange int sind. Ich weiß, dass es bereits mehrere Open Source-Implementierungen gibt, aber ich würde gerne meine eigenen schreiben. Ich versuche ein Gefühl dafür zu bekommen, was der richtige Ansatz ist.

Ich verstehe, dass die allgemeine Strategie darin besteht, die Zahl als Zeichenfolge abzurufen, sie dann in kleinere Zahlen (z. B. einzelne Ziffern) aufzuteilen und sie in einem Array zu platzieren. Zu diesem Zeitpunkt sollte es relativ einfach sein, die verschiedenen Vergleichsoperatoren zu implementieren. Mein Hauptanliegen ist, wie ich Dinge wie Addition und Multiplikation implementieren würde.

Ich suche einen allgemeinen Ansatz und Rat im Gegensatz zum eigentlichen Arbeitscode.


4
Das erste - Ziffernfolgen sind in Ordnung, aber denken Sie an Basis 2 ^ 32 (4 Milliarden ungerade verschiedene Ziffern). Vielleicht sogar Basis 2 ^ 64 in diesen Tagen. Zweitens arbeiten Sie immer mit vorzeichenlosen ganzzahligen "Ziffern". Sie können die Zweierkomplementierung für vorzeichenbehaftete große Ganzzahlen selbst durchführen. Wenn Sie jedoch versuchen, Ihre Überlaufbehandlung usw. mit vorzeichenbehafteten Ganzzahlen durchzuführen, treten Probleme mit undefiniertem Verhalten auf.
Steve314

3
Was Algorithmen betrifft - für eine Basisbibliothek sind diejenigen, die Sie in der Schule gelernt haben, ungefähr richtig.
Steve314

1
Wenn Sie die Multi-Präzisions-Mathematik selbst durchführen möchten, sollten Sie sich Donald Knuths Kunst der Computerprogrammierung ansehen . Ich glaube, Band II, Seminumerische Algorithmen, Kapitel 4, Multiple Precision Arithmetic, ist das, woran Sie interessiert sind. Siehe auch Hinzufügen von 2 Ganzzahlen beliebiger Größe in C ++? , der Code für einige C ++ - Bibliotheken und OpenSSL bereitstellt.
JWW

Antworten:


36

Dinge, die für eine große int-Klasse zu beachten sind:

  1. Mathematische Operatoren: +, -, /, *,% Vergessen Sie nicht, dass sich Ihre Klasse auf beiden Seiten des Operators befindet, dass die Operatoren verkettet werden können, dass einer der Operanden ein int, float, double usw. Sein kann .

  2. E / A-Operatoren: >>, << Hier erfahren Sie, wie Sie Ihre Klasse ordnungsgemäß aus Benutzereingaben erstellen und auch für die Ausgabe formatieren.

  3. Konvertierungen / Casts: Finden Sie heraus, in welche Typen / Klassen Ihre big int-Klasse konvertierbar sein soll und wie Sie die Konvertierung richtig handhaben. Eine Schnellliste würde double und float enthalten und kann int (mit ordnungsgemäßer Überprüfung der Grenzen) und complex (vorausgesetzt, sie kann den Bereich verarbeiten) enthalten.


1
Sehen Sie hier für die idiomatischen Möglichkeiten , um die Betreiber zu tun.
Mooing Duck

5
Für ganze Zahlen sind Operator << und >> Bitverschiebungsoperationen. Sie als E / A zu interpretieren, wäre ein schlechtes Design.
Dave

3
@ Dave: Außer dass es Standard-C ++ ist operator<<und operator>>mit iostreams für E / A.

9
@ Dave Sie können immer noch << und >> für Bitverschiebungsoperationen zusammen mit E / A für Streams definieren ...
miguel.martin

45

Eine lustige Herausforderung. :) :)

Ich gehe davon aus, dass Sie Ganzzahlen beliebiger Länge wollen. Ich schlage folgenden Ansatz vor:

Betrachten Sie die binäre Natur des Datentyps "int". Denken Sie daran, einfache Binäroperationen zu verwenden, um zu emulieren, was die Schaltkreise in Ihrer CPU tun, wenn sie Dinge hinzufügen. Wenn Sie ausführlicher interessiert sind, lesen Sie diesen Wikipedia-Artikel über Halb- und Volladdierer . Sie werden etwas Ähnliches tun, aber Sie können so niedrig wie das sein - aber da ich faul bin, dachte ich, ich würde einfach darauf verzichten und eine noch einfachere Lösung finden.

Bevor wir jedoch auf algorithmische Details zum Addieren, Subtrahieren und Multiplizieren eingehen, wollen wir eine Datenstruktur finden. Eine einfache Möglichkeit besteht natürlich darin, Dinge in einem std :: vector zu speichern.

template< class BaseType >
class BigInt
{
typedef typename BaseType BT;
protected: std::vector< BaseType > value_;
};

Möglicherweise möchten Sie überlegen, ob Sie den Vektor mit einer festen Größe erstellen und vorab zuordnen möchten. Der Grund dafür ist, dass Sie für verschiedene Operationen jedes Element des Vektors - O (n) - durchlaufen müssen. Vielleicht möchten Sie sofort wissen, wie komplex eine Operation sein wird, und ein festes n macht genau das.

Nun aber zu einigen Algorithmen zur Bearbeitung der Zahlen. Sie könnten es auf logischer Ebene tun, aber wir werden diese magische CPU-Leistung verwenden, um die Ergebnisse zu berechnen. Was wir jedoch von der logischen Darstellung von Half- und FullAdders übernehmen werden, ist die Art und Weise, wie mit Carry umgegangen wird. Überlegen Sie als Beispiel, wie Sie den Operator + = implementieren würden . Für jede Zahl in BigInt <> :: value_ würden Sie diese hinzufügen und prüfen, ob das Ergebnis eine Form von Übertrag erzeugt. Wir werden es nicht bitweise tun, sondern uns auf die Art unseres BaseType verlassen (sei es lang oder int oder kurz oder was auch immer): Es läuft über.

Wenn Sie zwei Zahlen hinzufügen, muss das Ergebnis sicherlich größer sein als die größere dieser Zahlen, oder? Ist dies nicht der Fall, ist das Ergebnis übergelaufen.

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. ;-)

Die andere arithmetische Operation verläuft analog. Heck, Sie könnten sogar die stl-Funktoren std :: plus und std :: minus, std :: times und std :: divides verwenden, ..., aber achten Sie auf den Übertrag. :) Sie können Multiplikation und Division auch mithilfe Ihrer Plus- und Minusoperatoren implementieren. Dies ist jedoch sehr langsam, da dadurch die Ergebnisse berechnet werden, die Sie bereits in früheren Aufrufen von Plus und Minus in jeder Iteration berechnet haben. Es gibt viele gute Algorithmen für diese einfache Aufgabe, verwenden Sie Wikipedia oder das Web.

Und natürlich sollten Sie Standardoperatoren implementieren wie operator<<(verschieben Sie einfach jeden Wert in value_ für n Bits nach links, beginnend mit value_.size()-1... oh und denken operator<Sie an den Übertrag :), - Sie können hier sogar ein wenig optimieren, indem Sie das überprüfen grobe Anzahl von Ziffern mit size()zuerst. Und so weiter. Dann machen Sie Ihre Klasse nützlich, indem Sie mit std :: ostream befreundet sind operator<<.

Hoffe dieser Ansatz ist hilfreich!


6
"int" (wie signiert) ist eine schlechte Idee. Undefiniertes Standardverhalten bei Überläufen macht es schwierig (wenn nicht unmöglich), die Logik zumindest portabel richtig zu machen. Es ist jedoch ziemlich einfach, zu zweit mit vorzeichenlosen ganzen Zahlen zu arbeiten, wobei das Überlaufverhalten streng definiert ist, um Modulo 2 ^ n-Ergebnisse zu liefern.
Steve314

28

Es gibt einen vollständigen Abschnitt dazu: [Die Kunst der Computerprogrammierung, Band 2: Seminumerische Algorithmen, Abschnitt 4.3 Multiple Precision Arithmetic, S. 265-318 (Hrsg. 3)]. Weitere interessante Materialien finden Sie in Kapitel 4, Arithmetik.

Wenn Sie sich wirklich keine andere Implementierung ansehen möchten, haben Sie sich überlegt, was Sie lernen möchten? Es müssen unzählige Fehler gemacht werden, und diese aufzudecken ist lehrreich und auch gefährlich. Es ist auch schwierig, wichtige Rechenökonomien zu identifizieren und über geeignete Speicherstrukturen zu verfügen, um schwerwiegende Leistungsprobleme zu vermeiden.

Eine Herausforderung für Sie: Wie wollen Sie Ihre Implementierung testen und wie möchten Sie nachweisen, dass die Arithmetik korrekt ist?

Möglicherweise möchten Sie, dass eine andere Implementierung getestet wird (ohne zu prüfen, wie sie funktioniert), aber es wird mehr als das erfordern, um verallgemeinern zu können, ohne ein exkrutierendes Testniveau zu erwarten. Vergessen Sie nicht, Fehlermodi zu berücksichtigen (Probleme mit zu wenig Speicher, zu wenig Stapel, zu lange Laufzeit usw.).

Habe Spaß!


2
Der Vergleich mit einer Referenzimplementierung bringt Sie nicht weiter, denn dann haben Sie ein anderes Problem: Wie kann man testen, ob die Referenzimplementierung auch korrekt ist? Das gleiche Problem besteht beim Testen des Wissens im Allgemeinen: Wenn ein Mann einen anderen testen muss, wer wird den ersteren testen? Es gibt keinen Ausweg aus diesem Problem außer einem, der vor langer Zeit erfunden wurde: Beweise aus Axiomen. Wenn die Menge der Axiome als richtig angesehen wird (keine Widersprüche) und der Beweis gemäß den Regeln der Logik richtig abgeleitet wird, kann er nicht falsch sein, selbst für eine unendliche Anzahl von Fällen, auf die niemand möglicherweise testen könnte.
SasQ


5

Sobald Sie die Ziffern der Zahl in einem Array haben, können Sie die Addition und Multiplikation genau so durchführen, wie Sie es mit Langschrift tun würden.


4

Vergessen Sie nicht, dass Sie sich nicht auf 0-9 als Ziffern beschränken müssen, dh Bytes als Ziffern (0-255) verwenden müssen, und Sie können trotzdem lange Handrechnungen durchführen, wie Sie es für Dezimalstellen tun würden. Sie könnten sogar ein Array von langen verwenden.


Wenn Sie die Zahlen in Dezimalzahlen darstellen möchten (dh nur für Sterbliche), ist der Algorithmus 0-9 pro Nibble einfacher. Gib einfach den Speicher auf.
dmckee --- Ex-Moderator Kätzchen

Sie glauben, es ist einfacher, BCD-Algorithmen zu erstellen als ihre regulären binären Gegenstücke?
Eclipse

2
AFAIK-Basis 10 wird häufig verwendet, da das Konvertieren großer Zahlen in Basis 255 (oder etwas, das keine Potenz von 10 ist) von / in Basis 10 teuer ist und die Eingabe und Ausgabe Ihrer Programme im Allgemeinen in Basis 10 erfolgt.
Tobi

@Tobi: Ich würde wahrscheinlich empfehlen, die Basis 10000 beizubehalten unsigned, was schnelles E / A und eine einfache Multiplikation ist, mit dem Nachteil, dass 59% des Speicherplatzes verschwendet werden. Ich empfehle die Basis (2 ^ 32) für fortgeschrittenes Lernen, die für alles außer E / A viel schneller als die Basis 10/10000 ist, aber die Multiplikation / Division viel schwieriger zu implementieren ist.
Mooing Duck

3

Ich bin nicht davon überzeugt, dass die Verwendung eines Strings der richtige Weg ist - obwohl ich selbst noch nie Code geschrieben habe, denke ich, dass die Verwendung eines Arrays eines numerischen Basistyps eine bessere Lösung sein könnte. Die Idee ist, dass Sie einfach das erweitern, was Sie bereits haben, genauso wie die CPU ein einzelnes Bit in eine Ganzzahl erweitert.

Zum Beispiel, wenn Sie eine Struktur haben

typedef struct {
    int high, low;
} BiggerInt;

Sie können dann manuell native Vorgänge für jede der "Ziffern" (in diesem Fall hoch und niedrig) ausführen, wobei Sie die Überlaufbedingungen berücksichtigen:

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;
}

Es ist ein vereinfachtes Beispiel, aber es sollte ziemlich offensichtlich sein, wie man es auf eine Struktur erweitert, die eine variable Anzahl der von Ihnen verwendeten numerischen Basisklasse aufweist.


Mit Zeichenfolge meinte das OP, eine Zeichenfolge zu nehmen, die die gewünschte Zahl in ihrer numerischen Darstellung enthält (unter welcher Basis auch immer) und das BigInt mit dem Wert zu initialisieren.
KTC

STLPLUS verwendet einen String, um eine große Ganzzahl zu halten.
lsalamon

2

Verwenden Sie die Algorithmen, die Sie in der 1. bis 4. Klasse gelernt haben.
Beginnen Sie mit der Einsen-Spalte, dann den Zehner-Spalten und so weiter.


2

Wie andere sagten, machen Sie es auf altmodische Weise, aber halten Sie sich davon fern, dies alles in Basis 10 zu tun. Ich würde vorschlagen, alles in Basis 65536 zu tun und Dinge in einer Reihe von Longs zu speichern.


1

Wenn Ihre Zielarchitektur die BCD-Darstellung von Zahlen (Binary Coded Decimal) unterstützt, können Sie Hardware-Unterstützung für die Multiplikation / Addition mit langer Hand erhalten, die Sie durchführen müssen. Wenn Sie den Compiler dazu bringen, BCD-Anweisungen auszugeben, müssen Sie sich darüber informieren ...

Die Chips der Motorola 68K-Serie hatten dies. Nicht dass ich bitter wäre oder so.


0

Mein Anfang wäre, ein Array von Ganzzahlen beliebiger Größe zu haben, wobei 31 Bit und die 32n als Überlauf verwendet werden.

Die Starteroperation wäre ADD und dann MAKE-NEGATIVE unter Verwendung des 2er-Komplements. Danach fließt die Subtraktion trivial und sobald Sie add / sub haben, ist alles andere machbar.

Es gibt wahrscheinlich ausgefeiltere Ansätze. Dies wäre jedoch der naive Ansatz der digitalen Logik.


0

Könnte versuchen, so etwas zu implementieren:

http://www.docjar.org/html/api/java/math/BigInteger.java.html

Sie würden nur 4 Bits für eine einzelne Ziffer 0 - 9 benötigen

Ein Int-Wert würde also jeweils bis zu 8 Stellen zulassen. Ich habe beschlossen, mich an eine Reihe von Zeichen zu halten, damit ich doppelt so viel Speicher verwende, aber für mich wird es nur einmal verwendet.

Auch wenn alle Ziffern in einem einzigen Int gespeichert werden, wird dies zu kompliziert, und wenn überhaupt, kann es sogar zu einer Verlangsamung führen.

Ich habe keine Geschwindigkeitstests, aber wenn ich mir die Java-Version von BigInteger anschaue, scheint es, als würde sie sehr viel Arbeit leisten.

Für mich mache ich das unten

//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

OP hat nie gesagt, dass er sich auf Dezimalstellen konzentrieren möchte.
Einpoklum

-1

Subtrahieren Sie 48 von Ihrer Ganzzahl und drucken Sie, um die Anzahl der großen Ziffern zu erhalten. Führen Sie dann die grundlegende mathematische Operation aus. Ansonsten werde ich eine Komplettlösung anbieten.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.