Как реализовать большой int в C ++ - PullRequest
78 голосов
/ 06 ноября 2008

Я хотел бы реализовать большой класс int в C ++ как упражнение по программированию - класс, который может обрабатывать числа, большие, чем long int. Я знаю, что уже есть несколько реализаций с открытым исходным кодом, но я бы хотел написать свою собственную. Я пытаюсь понять, что такое правильный подход.

Я понимаю, что общая стратегия состоит в том, чтобы получить число в виде строки, а затем разбить его на меньшие числа (например, на одну цифру) и поместить их в массив. На этом этапе должно быть относительно просто реализовать различные операторы сравнения. Моя главная задача - как бы я реализовал такие вещи, как сложение и умножение.

Я ищу общий подход и советы, а не рабочий код.

Ответы [ 13 ]

43 голосов
/ 06 ноября 2008

Веселое испытание. :)

Я предполагаю, что вы хотите целые числа произвольной длины. Я предлагаю следующий подход:

Рассмотрим двоичную природу типа данных "int". Подумайте об использовании простых двоичных операций, чтобы эмулировать действия схем в вашем процессоре, когда они что-то добавляют. Если вы заинтересованы в более глубоком изучении, прочитайте эту статью в Википедии о полусуммерах и полных сумматорах . Вы будете делать что-то похожее на это, но вы можете понизиться до такого низкого уровня - но, будучи ленивым, я подумал, что просто воздержусь и найду еще более простое решение.

Но прежде чем углубляться в алгоритмические подробности о сложении, вычитании, умножении, давайте найдем некоторую структуру данных. Конечно, простой способ хранить вещи в std :: vector.

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

Возможно, вы захотите подумать, хотите ли вы сделать вектор фиксированного размера и предварительно его выделить. Причина в том, что для разнообразных операций вам придется пройти через каждый элемент вектора - O (n). Возможно, вы захотите узнать, насколько сложной будет операция, и фиксированное n делает именно это.

Но теперь о некоторых алгоритмах работы с числами. Вы можете сделать это на логическом уровне, но мы будем использовать эту магическую мощность процессора для вычисления результатов. Но то, что мы извлечем из логической иллюстрации Half- и FullAdders, - это то, как они работают с переносами. В качестве примера рассмотрим, как можно реализовать оператор + = . Для каждого числа в BigInt <> :: value_ вы добавите их и посмотрите, не приведет ли результат к какой-либо форме переноса. Мы не будем делать это побитно, но будем полагаться на природу нашего BaseType (будь то long или int, short или что-то еще): он переполнен.

Конечно, если вы добавите два числа, результат должен быть больше, чем большее из этих чисел, верно? Если это не так, то результат переполнен.

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-функторы std :: plus и std :: minus, std :: times и std :: divides, ..., но помните о переносе. :) Вы также можете реализовать умножение и деление, используя операторы «плюс» и «минус», но это очень медленно, потому что это пересчитало бы результаты, которые вы уже рассчитывали в предыдущих вызовах плюс и минус в каждой итерации. Есть много хороших алгоритмов для этой простой задачи: использовать википедию или Интернет.

И, конечно, вы должны реализовать стандартные операторы, такие как operator<< (просто сдвиньте каждое значение в value_ влево для n битов, начиная с value_.size()-1 ... oh и запомните перенос :), operator< - здесь вы можете даже немного оптимизировать, сначала проверяя приблизительное количество цифр с помощью size(). И так далее. Тогда сделайте ваш класс полезным, используя befriendig std :: ostream operator<<.

Надеюсь, что этот подход полезен!

35 голосов
/ 06 ноября 2008

Что нужно учитывать для большого класса int:

  1. Математические операторы: +, -, /, *,% Не забывайте, что ваш класс может быть по обе стороны от оператор, что операторы могут быть прикован, что один из операндов может быть int, float, double и т. д.

  2. Операторы ввода / вывода: >>, << Это где вы выясните, как правильно создать свой класс из пользовательского ввода, а также как отформатировать его для вывода. </p>

  3. Конверсии / Приведения: выяснить какие типы / классы ваш большой инт класс должен быть конвертируемым, и как правильно обращаться с преобразование. Быстрый список будет включают двойные и плавающие, и могут включить int (с правильными границами проверка) и сложная (при условии может обрабатывать диапазон).

26 голосов
/ 06 ноября 2008

Здесь есть полный раздел: [Искусство компьютерного программирования, том 2: Полу численные алгоритмы, раздел 4.3, Арифметика с множественной точностью, с. 265-318 (изд. 3)] Вы можете найти другой интересный материал в главе 4 «Арифметика».

Если вы действительно не хотите смотреть на другую реализацию, задумывались ли вы над тем, что вы хотите узнать? Есть неисчислимые ошибки, которые можно сделать, и выявление их поучительно, а также опасно. Существуют также проблемы в определении важных вычислительных возможностей и наличии соответствующих структур хранения для избежания серьезных проблем с производительностью.

Задачный вопрос для вас: как вы собираетесь протестировать свою реализацию и как вы предлагаете продемонстрировать, что ее арифметика верна?

Возможно, вы захотите протестировать другую реализацию (не смотря на то, как она это делает), но для ее обобщения потребуется больше, чем ожидание мучительного уровня тестирования. Не забудьте рассмотреть режимы сбоев (из-за проблем с памятью, из-за стека, слишком долгой работы и т. Д.).

Веселись!

6 голосов
/ 06 ноября 2008

сложение, вероятно, должно быть сделано в стандартном линейном алгоритме времени
но для умножения вы можете попробовать http://en.wikipedia.org/wiki/Karatsuba_algorithm

5 голосов
/ 06 ноября 2008

Когда у вас есть цифры числа в массиве, вы можете делать сложение и умножение точно так же, как вы делали бы их от руки.

4 голосов
/ 06 ноября 2008

Не забывайте, что вам не нужно ограничивать себя цифрами 0-9, т. Е. Использовать байты в качестве цифр (0-255), и вы все равно можете делать арифметику длинных рук так же, как и для десятичных цифр. Вы могли бы даже использовать массив long.

3 голосов
/ 06 ноября 2008

Я не уверен, что использование строки - это правильный путь - хотя я сам никогда не писал код, я думаю, что использование массива базового числового типа может быть лучшим решением. Идея состоит в том, что вы просто расширите то, что у вас уже есть, точно так же, как процессор расширяет один бит в целое число.

Например, если у вас есть структура

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

Это немного упрощенный пример, но должно быть довольно очевидно, как расширить структуру, в которой было переменное число любого базового числового класса, который вы используете.

2 голосов
/ 06 ноября 2008

Как уже говорили другие, делайте это старомодным длинным способом, но держитесь подальше от выполнения всего этого на базе 10. Я бы посоветовал сделать все это на базе 65536 и хранить вещи в массиве длинных позиций.

1 голос
/ 06 ноября 2008

Если ваша целевая архитектура поддерживает представление чисел в двоично-десятичном формате, вы можете получить некоторую аппаратную поддержку для произвольного умножения / сложения, которое вам нужно сделать. Заставить компилятор выдавать инструкцию BCD - это то, о чем вам нужно прочитать ...

Чипы Motorola серии 68K имели это. Не то чтобы я горький или что-то в этом роде.

1 голос
/ 06 ноября 2008

Используйте алгоритмы, которые вы изучили с 1 по 4 класс.
Начните со столбца единиц, затем десятков и т. Д.

...