Як реалізувати великий int у C ++


80

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

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

Я шукаю загальний підхід та поради на відміну від фактичного робочого кодексу.


4
Перше - рядки цифр чудові, але думайте з точки зору базису 2 ^ 32 (4 мільярди непарних різних цифр). Можливо, навіть база 2 ^ 64 в наші дні. По-друге, завжди працюйте з беззнаковими цілими "цифрами". Ви можете зробити доповнення двох для підписаних великих цілих чисел самостійно, але якщо ви спробуєте виконати обробку переповнення тощо з підписаними цілими числами, ви зіткнетеся з невизначеними стандартами проблемами поведінки.
Steve314

3
Що стосується алгоритмів - то для базової бібліотеки ті, про які ви дізналися в школі, мають право.
Steve314

1
Якщо ви хочете самостійно виконати багатоточну математику, то я пропоную вам поглянути на « Мистецтво комп’ютерного програмування» Дональда Кнута . Я вважаю, що том II, напівчислові алгоритми, глава 4, арифметика множинної точності, є тим, що вас цікавить. Також див. Як додати 2 цілі числа довільного розміру в C ++? , який надає код для деяких бібліотек C ++ та OpenSSL.
jww

Відповіді:


37

Що потрібно врахувати для великого класу int:

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

  2. Оператори вводу-виводу: >>, << Тут ви з'ясуєте, як правильно створити свій клас із вводу користувача, і як його форматувати для виводу.

  3. Конверсії / трансляції: з’ясуйте, до яких типів / класів має бути конвертований ваш великий клас int та як правильно обробляти конвертацію. Швидкий список міститиме подвійне та плаваюче, а може включати int (з належним контролем меж) та складний (за умови, що він може обробляти діапазон).


1
Дивіться тут ідіоматичні способи роботи операторів.
Mooing Duck

5
Для цілих чисел оператори << та >> є операціями зсуву бітів. Інтерпретувати їх як введення-виведення було б поганим дизайном.
Дейв

3
@ Dave: Крім того, що це стандарт C ++ використовувати operator<<і operator>>з iostreamс для введення / виведення.

9
@Dave Ви все ще можете визначити << і >> для операцій зсуву бітів разом із введенням / виведенням для потоків ...
miguel.martin

46

Веселий виклик. :)

Я припускаю, що вам потрібні цілі числа довільної довжини. Я пропоную такий підхід:

Розглянемо двійкову природу типу даних "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 (будь то довгий чи int або короткий чи що завгодно): він переповнюється.

Звичайно, якщо ви додасте два числа, результат повинен бути більшим за більший із цих чисел, так? Якщо це не так, то результат переповнений.

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 :: розділяє, ..., але майте на увазі перенесення. :) Ви також можете реалізувати множення і ділення, використовуючи оператори плюс і мінус, але це дуже повільно, оскільки це перераховує результати, які ви вже обчислили в попередніх викликах, на плюс і мінус у кожній ітерації. Існує багато хороших алгоритмів для цього простого завдання, використовуйте wikipedia або Інтернет.

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

Сподіваюся, такий підхід корисний!


6
"int" (як підписано) - погана ідея. Стандарти невизначеної поведінки при переповненнях ускладнюють (якщо не неможливо) отримати логіку правильно, принаймні портативно. Правда, працювати вдвічі доповнено цілими беззнаками, де поведінка переповнення суворо визначена як дання результатів за модулем 2 ^ n.
Steve314

29

Про це є повний розділ: [Мистецтво комп’ютерного програмування, т. 2: Напівчисельні алгоритми, розділ 4.3 Багатоточна арифметика, с. 265-318 (ред. 3)]. Ви можете знайти інші цікаві матеріали в розділі 4, Арифметика.

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

Викличне запитання для вас: Як ви маєте намір перевірити свою реалізацію та як ви пропонуєте продемонструвати, що її арифметика правильна?

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

Веселіться!


2
Порівняння з деякою посилальною реалізацією не призведе до подальшого розвитку, оскільки тоді у вас є інша проблема: як перевірити, чи справжня реалізація посилання також є правильною? Така ж проблема і з тестуванням знань загалом: якщо одна людина повинна перевірити іншу, хто буде перевіряти першу? З цієї проблеми немає виходу, крім однієї, давно придуманої: доведення з аксіом. Якщо набір аксіом вважається правильним (відсутність суперечностей), а доказ виводиться належним чином згідно з логічними правилами, це не може бути помилковим, навіть для нескінченної кількості випадків, на які ніхто не міг би перевірити.
SasQ


5

Отримавши цифри числа в масиві, ви можете робити додавання та множення точно так, як робили б їх від руки.


4

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


Якщо ви хочете представити числа в десяткових цифрах (тобто для простих смертних), алгоритм 0-9 на клювання простіший. Просто відмовтеся від сховища.
dmckee --- кошеня екс-модератора

Ви вважаєте, що це простіше робити алгоритми BCD, ніж їх звичайні двійкові аналоги?
Eclipse

2
AFAIK base 10 часто використовується, оскільки перетворення великих чисел в базу 255 (або щось, що не має потужності 10) з / в базу 10 є дорогим, а вхід і вихід ваших програм, як правило, буде в базі 10.
Tobi

@Tobi: Я б, мабуть, порекомендував базу 10000, що зберігається unsigned, що є швидким введенням-введенням та простим у множенні, недоліком того, що він витрачає 59% місця для зберігання. Я рекомендую base (2 ^ 32) для більш просунутого навчання, яке набагато швидше, ніж base 10/10000 для всього, крім IO, але набагато складніше здійснити множення / ділення.
Мукінг-качка

3

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

Наприклад, якщо у вас є структура

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

Це дещо спрощений приклад, але повинно бути досить очевидно, як розширити структуру, яка мала змінну кількість будь-якого базового числового класу, який ви використовуєте.


Під рядком операційна система означала взяти рядок, що містить потрібне число, у його числовому поданні (під будь-якою базою) та ініціалізувати BigInt зі значенням.
KTC

STLPLUS використовує рядок для зберігання великого цілого числа.
lsalamon

2

Використовуйте алгоритми, які ви вивчили в 1-4 класі.
Почніть із стовпця з одиницями, потім з десятків тощо.


2

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


1

Якщо ваша цільова архітектура підтримує представлення чисел у форматі BCD (двійково закодований десятковий), ви можете отримати деяку апаратну підтримку для множення / складання з довжини, яку вам потрібно зробити. Змусити компілятор видавати інструкції BCD - це те, про що вам доведеться прочитати ...

У мікросхем серії Motorola 68K це було. Не те, що я гіркий чи що.


0

Моїм початком було б мати масив цілих чисел довільного розміру, використовуючи 31 біт і 32n'd як переповнення.

Стартовим варіантом буде ДОДАТИ, а потім, ЗРОБИТИ НЕГАТИВНИЙ, використовуючи доповнення 2. Після цього віднімання триває тривіально, і як тільки ви додасте / додаєте, все інше можна здійснити.

Ймовірно, існують більш складні підходи. Але це був би наївний підхід з цифрової логіки.


0

Спробуйте реалізувати щось подібне:

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

Вам знадобляться лише 4 біти для однієї цифри 0 - 9

Отже, значення Int має містити до 8 цифр кожна. Я вирішив дотримуватися масиву символів, тому використовую подвійну пам'ять, але для мене вона використовується лише 1 раз.

Крім того, при зберіганні всіх цифр в одному int це надмірно ускладнює це, і, якщо щось, це може навіть сповільнити його.

У мене немає ніяких тестів швидкості, але, дивлячись на 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 ніколи не говорив, що він хоче зосередитись на десяткових цифрах.
einpoklum

-1

відніміть 48 із рядка цілого числа та надрукуйте, щоб отримати число великої цифри. потім виконати основну математичну операцію. в іншому випадку я надам повне рішення.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.