C ++でbigintを実装する方法


80

プログラミング演習として、C ++でbigintクラスを実装したいと思います。これは、longintより大きい数値を処理できるクラスです。すでにいくつかのオープンソースの実装があることは知っていますが、私は自分で実装したいと思います。私は正しいアプローチが何であるかを感じ取ろうとしています。

一般的な戦略は、数値を文字列として取得し、それをより小さな数値(たとえば、1桁)に分割して配列に配置することであることを理解しています。この時点で、さまざまな比較演算子を実装するのは比較的簡単です。私の主な関心事は、足し算や掛け算などをどのように実装するかです。

実際に機能するコードではなく、一般的なアプローチとアドバイスを探しています。


4
まず、数字列は問題ありませんが、基数2 ^ 32(40億の奇数の異なる数字)で考えてください。最近は2 ^ 64をベースにしているかもしれません。次に、常に符号なし整数の「数字」を使用します。符号付き整数の2の補数は自分で実行できますが、符号付き整数を使用してオーバーフロー処理などを実行しようとすると、標準で定義されていない動作の問題が発生します。
Steve314 2011

3
アルゴリズムに関しては、基本的なライブラリの場合、学校で学んだものはほぼ正しいです。
Steve314 2011

1
自分で多倍長演算を実行したい場合は、DonaldKnuthのArtof ComputerProgrammingをご覧になることをお勧めします。第II巻、半数値アルゴリズム、第4章、複数精度の算術演算に関心があると思います。C++で任意サイズの2つの整数を追加する方法も参照してください、一部のC ++ライブラリとOpenSSLのコードを提供します。
jww 2017

回答:


37

大きなintクラスについて考慮すべきこと:

  1. 数学演算子:+、-、/、*、%クラスが演算子のいずれかの側にある可能性があること、演算子を連鎖できること、オペランドの1つがint、float、doubleなどである可能性があることを忘れないでください。

  2. I / O演算子:>>、<<ここで、ユーザー入力からクラスを適切に作成する方法と、出力用にクラスをフォーマットする方法を理解します。

  3. 変換/キャスト:big intクラスを変換できるタイプ/クラスと、変換を適切に処理する方法を理解します。クイックリストにはdoubleとfloatが含まれ、int(適切な境界チェックを使用)とcomplex(範囲を処理できると仮定)が含まれる場合があります。


1
演算子を実行する慣用的な方法については、ここを参照してください。
Mooing Duck 2012

5
整数の場合、演算子<<および>>はビットシフト演算です。それらをI / Oとして解釈するのは悪い設計です。
デイブ

3
@Dave:それの標準C ++が使用することを除いてoperator<<operator>>してiostreamI / Oのためのsの。

9
あなたはまだ...ストリームのI / Oと一緒にビットシフト操作のために>> <<定義とすることができます@Dave
miguel.martin

46

楽しい挑戦。:)

任意の長さの整数が必要だと思います。次のアプローチをお勧めします。

データ型「int」のバイナリの性質を考慮してください。単純な二項演算を使用して、CPU内の回路が何かを追加するときに何をするかをエミュレートすることを検討してください。より詳細に関心がある場合は、半加算器と全加算器に関するこのウィキペディアの記事を読むことを検討してください。あなたはそれに似た何かをするでしょう、しかしあなたはそれと同じくらい低いレベルに下がることができます-しかし怠惰なので、私はただ見捨てて、もっと簡単な解決策を見つけるだろうと思いました。

しかし、加算、減算、乗算に関するアルゴリズムの詳細に入る前に、いくつかのデータ構造を見つけましょう。もちろん、簡単な方法は、std :: vectorに物事を保存することです。

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

固定サイズのベクトルを作成するかどうか、および事前に割り当てるかどうかを検討することをお勧めします。理由は、さまざまな操作の場合、ベクトルの各要素(O(n))を通過する必要があるためです。操作がどれほど複雑になるかを直接知りたいと思うかもしれませんが、固定nはまさにそれを行います。

しかし、ここで、数値の操作に関するいくつかのアルゴリズムについて説明します。ロジックレベルでそれを行うこともできますが、その魔法のCPUパワーを使用して結果を計算します。しかし、HalfAddersとFullAddersのロジック図から引き継ぐのは、キャリーの処理方法です。例として、+ =演算子をどのように実装するかを考えてみましょう。BigInt <> :: value_の数値ごとに、それらを追加して、結果が何らかの形のキャリーを生成するかどうかを確認します。ビット単位では実行しませんが、BaseTypeの性質(long、int、shortなど)に依存します。オーバーフローします。

確かに、2つの数値を加算すると、結果はそれらの数値の大きい方よりも大きくなるはずですよね?そうでない場合、結果はオーバーフローしました。

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

他の算術演算も同様です。stl-functors std :: plusとstd :: minus、std :: times、std :: dividesなどを使用することもできますがキャリーに注意してください。:)プラス演算子とマイナス演算子を使用して乗算と除算を実装することもできますが、各反復でプラスとマイナスを以前に呼び出したときにすでに計算した結果が再計算されるため、非常に時間がかかります。この単純なタスクには、ウィキペディアまたはWebを使用する ための優れたアルゴリズムがたくさんあります。

そしてもちろん、次のような標準演算子を実装する必要がありますoperator<<(value_の各値をnビット左にシフトしvalue_.size()-1ます...ああ、キャリーを覚えておいてoperator<ください:)-ここで少し最適化して、size()最初の大まかな桁数。等々。次に、befriendig std :: ostreamを使用して、クラスを便利にしますoperator<<

このアプローチがお役に立てば幸いです。


6
「int」(署名されている場合)は悪い考えです。オーバーフロー時の標準の未定義の動作は、少なくとも移植可能に、ロジックを正しくすることを(不可能ではないにしても)困難にします。ただし、符号なし整数を2の補数で処理するのは非常に簡単です。オーバーフロー動作は、2 ^ nを法とする結果を与えるものとして厳密に定義されています。
Steve314 2011

29

これに関する完全なセクションがあります:[The Art of Computer Programming、vol.2:Seminumerical Algorithms、section 4.3 Multiple Precision Arithmetic、pp。265-318(ed.3)]。他の興味深い資料は、第4章「算術」にあります。

本当に別の実装を見たくない場合は、何を学ぼうとしているのかを考えましたか?犯すべき間違いは無数にあり、それらを明らかにすることは有益であり、危険でもあります。重要な計算経済を特定し、深刻なパフォーマンスの問題を回避するための適切なストレージ構造を用意することにも課題があります。

あなたへの挑戦の質問:あなたはあなたの実装をどのようにテストするつもりですか、そしてそれが正しいことを実証するためにどのように提案しますか?

別の実装を(どのように実行するかを見ずに)テストすることもできますが、耐え難いレベルのテストを期待せずに一般化できるようになるには、それ以上の時間がかかります。障害モード(メモリ不足、スタック不足、実行時間が長すぎるなど)を考慮することを忘れないでください。

楽しんで!


2
いくつかのリファレンス実装と比較しても、それ以上はわかりません。別の問題があるためです。リファレンス実装も正しいかどうかをテストする方法は?同じ問題は、一般的な知識のテストにもあります。ある人が別の人をテストする必要がある場合、誰が前者をテストしますか?ずっと前に発明された公理から証明することを除いて、この問題から抜け出す方法はありません。公理のセットが正しいと見なされ(矛盾がない)、論理の規則に従って証明が適切に導出された場合、誰もテストできない可能性のある無限のケースであっても、それは間違いではありません。
SasQ 2014


5

配列に数値の桁が入ったら、長時間の場合とまったく同じように加算と乗算を行うことができます。


4

数字として0〜9に制限する必要がないことを忘れないでください。つまり、バイトを数字(0〜255)として使用しても、10進数の場合と同じように長い演算を実行できます。longの配列を使用することもできます。


数値を10進数で表したい場合(つまり、単なる定命の者の場合)、ニブルあたり0〜9のアルゴリズムの方が簡単です。ストレージをあきらめるだけです。
dmckee ---元モデレーターの子猫

BCDアルゴリズムは、通常のバイナリの対応するアルゴリズムよりも簡単だと思いますか?
Eclipse

2
AFAIK基数10がよく使用されます。これは、基数255(または10の累乗ではないもの)の大きな数値を基数10との間で変換するのにコストがかかり、プログラムの入出力は通常10進数になるためです。–
Tobi

@Tobi:ベース10000を保持することをお勧めしますunsigned。これは、高速IOであり、乗算が簡単です。これは、ストレージスペースの59%を浪費するという欠点です。より高度な学習には基数(2 ^ 32)をお勧めします。これは、IOを除くすべての基数10/10000よりもはるかに高速ですが、乗算/除算の実装ははるかに困難です。
Mooing Duck 2012

3

文字列を使用することが正しい方法であるとは確信していません。自分でコードを記述したことはありませんが、基本数値型の配列を使用する方が良い解決策になると思います。アイデアは、CPUが1ビットを整数に拡張するのと同じ方法で、すでに持っているものを単純に拡張するというものです。

たとえば、構造がある場合

typedef struct {
    int high, low;
} BiggerInt;

次に、オーバーフロー条件に注意しながら、各「桁」(この場合は高低)に対してネイティブ操作を手動で実行できます。

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

これは少し単純な例ですが、使用している基本数値クラスの変数番号を持つ構造体に拡張する方法はかなり明白です。


文字列とは、OPは、数値表現に必要な数値を含む文字列を(任意の基数で)取得し、BigIntを値で初期化することを意味しました。
KTC

STLPLUSは、文字列を使用して大きな整数を保持します。
lsalamon 2009

2

1年生から4年生で学んだアルゴリズムを使用します。
1の列から始めて、次に10の列というように続きます。


2

他の人が言ったように、昔ながらの長い道のりでそれを行いますが、これをすべて基数10で行うことは避けてください。すべてを基数65536で行い、物事をlongの配列に格納することをお勧めします。


1

ターゲットアーキテクチャが数値のBCD(2進化10進数)表現をサポートしている場合は、実行する必要のあるロングハンドの乗算/加算に対するハードウェアサポートを取得できます。コンパイラにBCD命令を出力させることは、あなたが読まなければならないことです...

Motorola68Kシリーズチップにはこれがありました。私が苦いというわけではありません。


0

私の出発点は、31ビットと32nをオーバーフローとして使用して、任意のサイズの整数の配列を作成することです。

スターター操作はADDになり、次に2の補数を使用してMAKE-NEGATIVEになります。その後、減算は簡単に流れ、add / subを取得すると、他のすべてが実行可能になります。

おそらくもっと洗練されたアプローチがあります。しかし、これはデジタルロジックからの素朴なアプローチです。


0

このようなものを実装してみることができます:

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

1桁の0〜9には4ビットしか必要ありません

したがって、Int値はそれぞれ最大8桁を許可します。私はcharの配列に固執することに決めたので、2倍のメモリを使用しますが、私にとっては1回しか使用されていません。

また、すべての桁を1つの整数に格納すると、複雑になりすぎて、速度が低下する可能性があります。

速度テストはありませんが、JavaバージョンのBigIntegerを見ると、非常に多くの作業を行っているようです。

私のために私は以下を行います

//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は、10進数に焦点を合わせたいと言ったことはありません。
einpoklum 2016

-1

整数の文字列から48を引き、印刷して大きな桁の数を取得します。次に、基本的な数学演算を実行します。そうでなければ私は完全な解決策を提供します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.