Арифметическое объяснение произвольной точности
Я пытаюсь изучить C и столкнулся с невозможностью работать с действительно большими числами (т. е. 100 цифр, 1000 цифр и т. д.). Я знаю, что для этого существуют библиотеки, но я хочу попытаться реализовать это сам.
Я просто хочу знать, есть ли у кого-нибудь или может предоставить очень подробное, тупое объяснение арифметики произвольной точности.
8 ответов:
все дело в адекватном хранении и алгоритмах для обработки чисел как меньших частей. Предположим, у вас есть компилятор, в котором
intможет быть только от 0 до 99, и вы хотите обрабатывать номера до 999999 (мы будем беспокоиться только о положительных числах здесь, чтобы сохранить его простым).вы делаете это, давая каждому число три
ints и используя те же правила, которые вы (должны были) узнали еще в начальной школе для сложения, вычитания и других основных оперативный.в библиотеке произвольной точности нет фиксированного ограничения на количество базовых типов, используемых для представления наших чисел, просто любая память может содержать.
дополнение например:
123456 + 78:12 34 56 78 -- -- -- 12 35 34работа с наименее значимого конца:
- начальный перенос = 0.
- 56 + 78 + 0 carry = 134 = 34 с 1 carry
- 34 + 00 + 1 carry = 35 = 35 с 0 carry
- 12 + 00 + 0 carry = 12 = 12 с 0 carry
это, на самом деле, как дополнение обычно работает на битовом уровне внутри вашего процессора.
вычитание аналогично (используя вычитание базового типа и заимствование вместо переноса), умножение может быть выполнено с повторными добавлениями (очень медленно) или перекрестными продуктами (быстрее), а деление сложнее, но может быть сделано путем сдвига и вычитания чисел (длинное деление, которое вы узнали бы как a ребенок.)
я на самом деле написал библиотеки, чтобы делать такие вещи, используя максимальные мощности десяти, которые могут быть помещены в целое число при квадрате (чтобы предотвратить переполнение при умножении на два
ints вместе, например, 16-битintбудучи ограниченным от 0 до 99 для создания 9,801 (int использование от 0 до 9,999 для генерации 99,980,001 (некоторые трюки, чтобы следить для.
1/ при добавлении или умножении чисел, предварительно выделите максимальное пространство, необходимое затем уменьшить позже, если вы обнаружите, что это слишком много. Например, добавление двух 100 - "цифр" (где цифра-это
int) номера никогда не дадут вам больше, чем 101 цифр. Умножение 12-значного числа на 3-значное число никогда не будет генерировать более 15 цифр (добавьте количество цифр).2/ для дополнительной скорости нормализуйте (уменьшите объем памяти, необходимый для) номера только в случае крайней необходимости - моя библиотека имела это как отдельный вызов, поэтому пользователь может выбирать между скоростью и проблемами хранения.
3/ сложение положительного и отрицательного числа-это вычитание, а вычитание отрицательного числа-это то же самое, что добавление эквивалентного положительного. Вы можете сохранить довольно много кода, если методы add и subtract будут вызывать друг друга после корректировки знаков.
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операции используются для быстрого умножения или деления aLongIntпо значению обертки (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.
отдела также сложно, но основано на ранней арифметике (метод "gazinta" для "goes into"). Рассмотрим следующее длинное деление для
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 Multiple Precision Arithmetic Library - это значительно лучше и быстрее, чем мои собственные библиотеки.
у него есть довольно неудачная ошибка в том, что он просто выйдет, если у него закончится память (довольно фатальный недостаток для библиотеки общего назначения, на мой взгляд), но если вы можете посмотреть мимо этого, это довольно хорошо что он делает.
если вы не можете использовать его по причинам лицензирования (или потому, что вы не хотите, чтобы ваше приложение просто выходило без видимых причин), вы могли бы по крайней мере получить алгоритмы оттуда для интеграции в свой собственный код.
я также обнаружил, что СД в MPIR (вилка GMP) более поддаются обсуждению потенциальных изменений - они кажутся более дружественной к разработчику группой.
в то время как повторное изобретение колеса очень хорошо для вашего личного назидания и обучения, его также чрезвычайно большая задача. Я не хочу отговаривать вас, поскольку это важное упражнение и то, что я сделал сам, но вы должны знать, что есть тонкие и сложные проблемы на работе, которые решают большие пакеты.
например, умножения. Наивно, вы можете подумать о методе "школьника", т. е. написать одно число над другим, а затем сделать длинное умножение как ты учился в школе. пример:
123 x 34 ----- 492 + 3690 --------- 4182но этот метод чрезвычайно медленный(O (n^2), n-количество цифр). Вместо этого современные пакеты bignum используют либо дискретное преобразование Фурье, либо числовое преобразование, чтобы превратить это в операцию по существу O(n ln(n)).
и это только для целых чисел. Когда вы попадаете в более сложные функции на некотором типе реального представления числа (log, sqrt, exp и т. д.) все становится еще больше сложный.
Если вы хотите получить некоторые теоретические знания, я настоятельно рекомендую прочитать первую главу книги Yap,"фундаментальные проблемы алгоритмической алгебры". Как уже упоминалось, библиотека gmp bignum-это отличная библиотека. Для реальных чисел я использовал mpfr и мне это понравилось.
Не изобретайте велосипед: он может оказаться квадратным!
используйте стороннюю библиотеку, например GNU MP, это проверено и проверено.
Вы делаете это в основном такие же, как вы делаете с карандашом и бумагой...
- число должно быть представлено в буфере (массиве), способном принимать произвольный размер (что означает использование
mallocиrealloc) по мере необходимости- вы реализуете основную арифметику как можно больше, используя поддерживаемые языком структуры, и имеете дело с переносом и перемещением точки основания вручную
- вы просматриваете тексты числового анализа, чтобы найти эффективные аргументы для работы с помощью более сложная функция
- вы только реализуете столько, сколько вам нужно.
обычно вы будете использовать в качестве базовой единицы вычислений
- байты, содержащие с 0-99 или 0-255
- 16 бит слова contaning увядают 0-9999 или 0--65536
- 32-битные слова, содержащие...
- ...
как диктует ваша архитектура.
выбор двоичной или десятичной базы зависит от ваших желаний максимальная эффективность космоса, человеческая удобочитаемость, и присутсвие отсутствия поддержки математики двоичного кода десятичной (BCD) на вашем обломоке.
вы можете сделать это с высоким уровнем школьной математики. Хотя в реальности используются более продвинутые алгоритмы. Так например добавить два 1024-байтовых числа:
unsigned char first[1024], second[1024], result[1025]; unsigned char carry = 0; unsigned int sum = 0; for(size_t i = 0; i < 1024; i++) { sum = first[i] + second[i] + carry; carry = sum - 255; }результат должен быть больше на
one placeв случае добавления позаботиться о максимальных значениях. Посмотрите на это :9 + 9 ---- 18TTMath это отличная библиотека, если вы хотите учиться. Он построен с использованием C++. Приведенный выше пример был глупым, но это то, как сложение и вычитание делается в генерал!
хорошая ссылка на эту тему вычислительная сложность математических операций. Он говорит вам, сколько места требуется для каждой операции, которую вы хотите реализовать. Например, если у вас есть два
N-digitчисел, то вам нужно2N digitsдля хранения результата умножения.как Митч сказал, это далеко не простая задача для реализации! Я рекомендую вам взглянуть на TTMath, если вы знаете C++.
одна из конечных ссылок (IMHO) - Это том TAOCP кнута II. он объясняет множество алгоритмов для представления чисел и арифметических операций над этими представлениями.
@Book{Knuth:taocp:2, author = {Knuth, Donald E.}, title = {The Art of Computer Programming}, volume = {2: Seminumerical Algorithms, second edition}, year = {1981}, publisher = {\Range{Addison}{Wesley}}, isbn = {0-201-03822-6}, }
предполагая, что вы хотите написать большой целочисленный код самостоятельно, это может быть удивительно просто сделать, говоря как кто-то, кто сделал это недавно (хотя и в MATLAB.) Вот несколько трюков, которые я использую:
Я сохранил каждую отдельную десятичную цифру в виде двойного числа. Это делает многие операции простыми, особенно вывод. Хотя он занимает больше места, чем вы могли бы пожелать, память здесь дешевая, и это делает умножение очень эффективным, если вы можете свернуть a пара векторов эффективно. Кроме того, вы можете хранить несколько десятичных цифр в двойном формате, но будьте осторожны, что свертка для умножения может вызвать числовые проблемы на очень больших числах.
хранить знаковый бит отдельно.
добавление двух чисел в основном заключается в добавлении цифр, а затем проверьте перенос на каждом шаге.
умножение пары чисел лучше всего делать в виде свертки следует проводить шаг, по крайней мере, если у вас есть быстрый код свертки на кране.
даже когда вы храните числа в виде строки отдельных десятичных цифр, деление (также mod / rem ops) может быть сделано, чтобы получить примерно 13 десятичных цифр за раз в результате. Это намного эффективнее, чем деление, которое работает только на 1 десятичной цифре за раз.
чтобы вычислить целочисленную степень целого числа, вычислите двоичное представление экспоненты. Затем используйте повторяющиеся операции возведения в квадрат для вычисления необходимых мощностей.
многие операции (факторинг, тесты на примитивность и т. д.) выиграет от операции powermod. То есть,когда вы вычисляете mod(a^p, N), уменьшите результат mod N на каждом шаге возведения в степень, где p было выражено в двоичной форме. Не вычисляйте сначала a^p, а затем попробуйте уменьшить его mod N.
вот простой (наивный ) пример, который я сделал в PHP.
я реализовал "добавить" и "умножить" и использовал это для примера экспоненты.
http://adevsoft.com/simple-php-arbitrary-precision-integer-big-num-example/
код СНиП
// Add two big integers function ba($a, $b) { if( $a === "0" ) return $b; else if( $b === "0") return $a; $aa = str_split(strrev(strlen($a)>1?ltrim($a,"0"):$a), 9); $bb = str_split(strrev(strlen($b)>1?ltrim($b,"0"):$b), 9); $rr = Array(); $maxC = max(Array(count($aa), count($bb))); $aa = array_pad(array_map("strrev", $aa),$maxC+1,"0"); $bb = array_pad(array_map("strrev", $bb),$maxC+1,"0"); for( $i=0; $i<=$maxC; $i++ ) { $t = str_pad((string) ($aa[$i] + $bb[$i]), 9, "0", STR_PAD_LEFT); if( strlen($t) > 9 ) { $aa[$i+1] = ba($aa[$i+1], substr($t,0,1)); $t = substr($t, 1); } array_unshift($rr, $t); } return implode($rr); }
Comments