Все дело в адекватном хранилище и алгоритмах обработки чисел как меньших частей. Предположим, у вас есть компилятор, в котором int
может быть только от 0 до 99, и вы хотите обрабатывать числа до 999999 (здесь мы будем беспокоиться только о положительных числах, чтобы было проще).
Вы делаете это, задавая каждому числу три int
s и используя те же правила, которые вы (должны были) выучить еще в начальной школе для сложения, вычитания и других основных операций.
В библиотеке произвольной точности нет фиксированного ограничения на количество базовых типов, используемых для представления наших чисел, только то, что может вместить память.
Дополнение, например 123456 + 78
:
12 34 56
78
-- -- --
12 35 34
Работаем с наименее значимого конца:
- начальный перенос = 0.
- 56 + 78 + 0 перенос = 134 = 34 с 1 переносом
- 34 + 00 + 1 перенос = 35 = 35 с 0 переносом
- 12 + 00 + 0 перенос = 12 = 12 с 0 переносом
Фактически так сложение обычно работает на битовом уровне внутри вашего процессора.
Вычитание аналогично (с использованием вычитания базового типа и заимствования вместо переноса), умножение может выполняться с помощью повторяющихся сложений (очень медленно) или перекрестных произведений (быстрее), а деление сложнее, но может выполняться путем сдвига и вычитания чисел. вовлечены (длинное деление вы выучили бы в детстве).
Я на самом деле написал библиотеки для такого рода вещей, используя максимальную степень десяти, которая может быть помещена в целое число в квадрате (чтобы предотвратить переполнение при умножении двух int
s вместе, например, 16-битное int
ограничение от 0 до 99, чтобы сгенерировать 9801 (<32768) в квадрате или 32-битное int
использование от 0 до 9999 для генерации 99 980 001 (<2 147 483 648)), что значительно упростило алгоритмы.
Некоторые хитрости, на которые следует обратить внимание.
1 / При сложении или умножении чисел предварительно выделите максимальное необходимое пространство, а затем уменьшите его, если вы обнаружите, что это слишком много. Например, добавление двух 100-значных (где цифра - это int
) числа никогда не даст вам более 101 цифры. При умножении 12-значного числа на 3-значное число никогда не будет больше 15 цифр (добавьте количество цифр).
2 / Для увеличения скорости нормализуйте (уменьшите необходимое для хранения) числа только в случае крайней необходимости - в моей библиотеке это было отдельным вызовом, чтобы пользователь мог выбирать между скоростью и хранением.
3 / Сложение положительного и отрицательного числа является вычитанием, а вычитание отрицательного числа аналогично сложению эквивалентного положительного числа. Вы можете сэкономить довольно много кода, заставив методы сложения и вычитания вызывать друг друга после настройки знаков.
4 / Избегайте вычитания больших чисел из маленьких, так как вы неизменно получаете такие числа, как:
10
11-
-- -- -- --
99 99 99 99 (and you still have a borrow).
Вместо этого вычтите 10 из 11, а затем отрицайте это:
11
10-
--
1 (then negate to get -1).
Вот комментарии (преобразованные в текст) из одной из библиотек, для которой мне пришлось это сделать. Сам код, к сожалению, защищен авторским правом, но вы можете выбрать достаточно информации для выполнения четырех основных операций. Предположим далее, что -a
и -b
представляют отрицательные числа, а a
и b
- ноль или положительные числа.
Для сложения , если знаки разные, используйте вычитание отрицания:
-a + b becomes b - a
a + -b becomes a - b
Для вычитания , если знаки разные, используйте сложение отрицания:
a - -b becomes a + b
-a - b becomes -(a + b)
Также специальная обработка, чтобы гарантировать, что мы вычитаем маленькие числа из больших:
small - big becomes -(big - small)
В умножении используется математика начального уровня следующим образом:
475(a) x 32(b) = 475 x (30 + 2)
= 475 x 30 + 475 x 2
= 4750 x 3 + 475 x 2
= 4750 + 4750 + 4750 + 475 + 475
Способ, которым это достигается, включает извлечение каждой из 32 цифр по одной (в обратном порядке), а затем использование add для вычисления значения, которое будет добавлено к результату (изначально нулевое).
ShiftLeft
и ShiftRight
операции используются для быстрого умножения или деления a LongInt
на значение переноса (10 для «реальной» математики). В приведенном выше примере мы прибавляем 475 к нулю 2 раза (последняя цифра 32), чтобы получить 950 (результат = 0 + 950 = 950).
Затем мы сдвигаем влево 475, чтобы получить 4750, и сдвиг вправо 32, чтобы получить 3. Добавляем 4750 к нулю 3 раза, чтобы получить 14250, затем прибавляем к результату 950, чтобы получить 15200.
Сдвиг влево 4750, чтобы получить 47500, сдвиг вправо, 3, чтобы получить 0. Поскольку смещение вправо 32 теперь равно нулю, мы закончили, и на самом деле 475 x 32 действительно равно 15200.
Деление также сложно, но основано на ранней арифметике (метод «газинты» для «входит в»). Рассмотрим следующее длинное деление 12345 / 27
:
457
+-------
27 | 12345 27 is larger than 1 or 12 so we first use 123.
108 27 goes into 123 4 times, 4 x 27 = 108, 123 - 108 = 15.
---
154 Bring down 4.
135 27 goes into 154 5 times, 5 x 27 = 135, 154 - 135 = 19.
---
195 Bring down 5.
189 27 goes into 195 7 times, 7 x 27 = 189, 195 - 189 = 6.
---
6 Nothing more to bring down, so stop.
Поэтому 12345 / 27
остается 457
с остатком 6
. Проверить:
457 x 27 + 6
= 12339 + 6
= 12345
Это реализуется с помощью переменной уменьшения (изначально равной нулю) для уменьшения сегментов 12345 по одному, пока она не станет больше или равна 27.
Затем мы просто вычитаем из него 27, пока не станет меньше 27 - количество вычитаний - это сегмент, добавленный к верхней строке.
Когда больше нет сегментов, которые нужно сбивать, у нас есть результат.
Имейте в виду, что это довольно простые алгоритмы. Есть гораздо лучшие способы выполнять сложные арифметические операции, если ваши числа будут особенно большими. Вы можете изучить что-то вроде библиотеки арифметических операций с множественной точностью GNU - она значительно лучше и быстрее моих собственных библиотек.
У него есть довольно досадная ошибка в том, что он просто выйдет, если у него закончится память (на мой взгляд, довольно фатальный недостаток для библиотеки общего назначения), но, если вы можете не обращать внимания на это, он довольно хорош в том, что делает.
Если вы не можете использовать его по причинам лицензирования (или потому, что вы не хотите, чтобы ваше приложение просто выходило без видимой причины), вы можете, по крайней мере, получить оттуда алгоритмы для интеграции в свой собственный код.
Я также обнаружил, что сотрудники MPIR ( ответвление GMP) более поддаются обсуждению потенциальных изменений - они кажутся более дружелюбными для разработчиков.