プログラミング演習として、C++でbig intクラスを実装したいと思います。longintより大きな数値を処理できるクラスです。私はすでにいくつかのオープンソース実装が存在することを知っていますが、私は独自のものを書きたいです。私は正しいアプローチが何であるかを感じようとしています。
一般的な戦略では、数字を文字列として取得し、それを小さな数字(たとえば、1桁)に分割し、配列に配置することを理解しています。この時点で、さまざまな比較演算子を実装するのは比較的簡単なはずです。私の主な関心事は、加算や乗算などを実装する方法です。
実際の作業コードではなく、一般的なアプローチとアドバイスを探しています。
大きなintクラスで考慮すべき事項:
数学演算子:+、-、/、*、%クラスが演算子のどちら側にあるか、演算子を連鎖できるか、オペランドの1つがint、float、doubleなどであることを忘れないでください。
I/O演算子:>>、<<これは、ユーザー入力からクラスを適切に作成する方法、および出力用にフォーマットする方法を見つける場所です。
変換/キャスト:big intクラスをどのタイプ/クラスに変換できるか、および変換を適切に処理する方法を見つけます。クイックリストにはdoubleとfloatが含まれ、int(適切な境界チェック付き)とcomplex(範囲を処理できると仮定)が含まれる場合があります。
楽しいチャレンジ。 :)
任意の長さの整数が必要だと思います。次のアプローチをお勧めします。
データ型「int」のバイナリの性質を考慮してください。単純なバイナリ演算を使用して、CPUの回路が何かを追加するときに実行する処理をエミュレートすることを検討してください。より深く興味がある場合は、 この半加算器と全加算器に関するウィキペディアの記事 を読むことを検討してください。あなたはそれに似た何かをするでしょうが、あなたはそれと同じくらい低いレベルに行くことができます-しかし、怠けているので、私はただ先を見て、さらに簡単な解決策を見つけるだろうと思いました。
しかし、加算、減算、乗算に関するアルゴリズムの詳細に入る前に、データ構造を見つけましょう。もちろん、単純な方法は、std :: vectorに物事を保存することです。
_template< class BaseType >
class BigInt
{
typedef typename BaseType BT;
protected: std::vector< BaseType > value_;
};
_
固定サイズのベクトルを作成するかどうか、および事前に割り当てるかどうかを検討できます。理由は、さまざまな操作のために、ベクトルの各要素-O(n)を通過する必要があるからです。操作がどれだけ複雑になるかをすぐに知りたいと思うかもしれませんが、固定nはまさにそれを行います。
しかし、数値の操作に関するいくつかのアルゴリズムに移りました。ロジックレベルで実行できますが、その魔法のCPUパワーを使用して結果を計算します。しかし、Half-およびFullAddersの論理図から引き継ぐのは、キャリーを扱う方法です。例として、+ = operatorの実装方法を検討します。 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を使用することもできますが、キャリーに注意してください。 :)プラス演算子とマイナス演算子を使用して乗算と除算を実装することもできますが、それは非常に時間がかかります。これは、各反復で以前のプラスとマイナスの呼び出しで既に計算した結果を再計算するためです。この単純なタスク、 sewikipedia またはwebには、多くの優れたアルゴリズムがあります。
そしてもちろん、_operator<<
_などの標準演算子を実装する必要があります(value_の各値をnビットだけ左にシフトし、value_.size()-1
...で始まり、キャリーを覚えてください:) 、_operator<
_-ここで少し最適化することもできます。最初にsize()
で大まかな桁数を確認します。等々。次に、stfriend:std :: ostream _operator<<
_を使用して、クラスを有用にします。
このアプローチが役立つことを願っています!
これに関する完全なセクションがあります:[The Art of Computer Programming、vol.2:Seminumerical Algorithms、section 4.3 Multiple Precision Arithmetic、pp.265-318(ed.3)]。他の興味深い資料は、第4章「算術演算」で見つけることができます。
別の実装を実際に見たくない場合は、学習することを検討していますか?なすべき間違いは無数にあり、それらを発見することは有益であり、危険でもあります。重要な計算経済を特定し、深刻なパフォーマンスの問題を回避するための適切なストレージ構造を持つことにも課題があります。
チャレンジ質問:実装をどのようにテストし、算術が正しいことを実証することをどのように提案しますか?
別の実装をテストする必要があるかもしれませんが(どのように実行するかを見なくても)、テストの厳しいレベルを期待せずに一般化できるようになるには、それ以上のものが必要になります。失敗モード(メモリ不足の問題、スタック不足、実行時間が長すぎるなど)を考慮することを忘れないでください。
楽しむ!
おそらく標準線形時間アルゴリズムで追加する必要があります
しかし、掛け算には http://en.wikipedia.org/wiki/Karatsuba_algorithm を試すことができます
配列に数値の数字が入ったら、手書きのように正確に加算と乗算を行うことができます。
数字として0-9に制限する必要がないことを忘れないでください、つまり、数字(0-255)としてバイトを使用し、10進数の場合と同じように長い手計算を行うことができます。長い配列を使用することもできます。
文字列を使用するのが正しい方法だとは確信していません。自分でコードを書いたことはありませんが、基本数値型の配列を使用する方がより良い解決策になると思います。アイデアは、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;
}
これは少し単純な例ですが、使用している基本数値クラスの変数の数が可変である構造に拡張する方法はかなり明白なはずです。
他の人が言ったように、昔ながらのロングハンドのやり方でやりますが、これをすべて10進数でやるのは避けてください。
1年生から4年生で学んだアルゴリズムを使用します。
1の列から始め、次に10の列など。
ターゲットアーキテクチャが数値のBCD(バイナリコード10進数)表現をサポートしている場合、必要な速記の乗算/加算のハードウェアサポートを取得できます。コンパイラーにBCD命令を発行させることは、読み進めなければならないものです...
Motorola 68Kシリーズチップにはこれがありました。私が苦いというわけではありません。
私の出発点は、31ビットと32n'dをオーバーフローとして使用して、整数の任意のサイズの配列を持つことです。
スターターopはADDで、MAKE-NEGATIVEで、2の補数を使用します。その後、減算は簡単に流れ、add/subを取得すると、他のすべてが実行可能になります。
おそらくもっと洗練されたアプローチがあります。しかし、これはデジタルロジックからの単純なアプローチです。
このようなものを実装してみてください:
http://www.docjar.org/html/api/Java/math/BigInteger.Java.html
1桁の0〜9に必要なのは4ビットのみです。
したがって、Int値はそれぞれ最大8桁を許可します。文字の配列に固執することにしたので、2倍のメモリを使用しますが、私にとっては1回しか使用されていません。
また、すべての数字を1つのintに格納すると、複雑になり、速度が低下する可能性があります。
速度テストはありませんが、BigIntegerのJavaバージョンを見ると、非常に多くの作業を行っているようです。
私のために私は以下を行います
//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