Главы:
Основание (base) системы счисления (СС) — число цифр:
- в десятичной СС 10 цифр: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
- в двоичной — две: 0, 1
- в шестнадцатеричной — 16: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
- и т.д.
В позиционной СС значение каждой цифры зависит от позиции (разряда).
Например в числе 472 четыре сотни, семь десятков и две единицы: 4·100 + 7·10 + 2·1
.
Разряды нумеруются с конца. Разряды слева — старшие, а справа — младшие.
Номер разряда совпадает со степенью base: 4·102 + 7·101 + 2·100
.
Большие числа можно хранить в виде массивов цифр (по одной цифре в каждом элементе массива). При этом математические операции придётся реализовать самостоятельно (например столбиком, как в школе).
Литература:
- https://e-maxx.ru/algo/big_integer (ещё в разделе bookz есть полезные Кнут и Окулов)
- https://ru.wikipedia.org/wiki/Длинная_арифметика
- https://en.wikipedia.org/wiki/Arbitrary-precision_arithmetic
- https://ru.wikipedia.org/wiki/Позиционная_система_счисления
Внимание! Уже упомянутая литература в последующих темах не упоминается.
Пример:
238
+ 932
────
1170
- Складываем младшие разряды (единицы):
8 + 2 = 10
. Так как10 >= base (10)
, то есть перенос в следующий столбец - Складываем разряды десятков:
3 + 3 + 1 = 7
. Переноса в столбец сотен нет, так как7 < 10
- Складываем разряды сотен:
2 + 9 + 0 = 11
. Так как11 >= 10
, то есть перенос в столбец тысяч
Реализация этого алгоритма: 1_basics_add_1.cpp.
Внимание! В исходниках зачастую используется код из предыдущих программ, поэтому важно изучать их последовательно.
Тестировать код онлайн можно на сайтах:
Опции для GCC: -O0 -Wall -Wextra -Wpedantic -fsanitize=undefined -std=c++20
.
В приведённом коде имеется 2 проблемы:
- Приходится добавлять цифры в начало массива
- Если числа разной длины, то приходится складывать цифры с разными индексами
Эти проблемы легко обойти, если хранить цифры в обратном порядке. Реализация: 1_basics_add_2.cpp.
Литература:
- https://en.wikipedia.org/wiki/Carry_(arithmetic)
- Гуровиц и Гольдштейн
- https://www.geeksforgeeks.org/sum-two-large-numbers/
Рассмотрим программу 1_basics_mul_1.cpp, в которой реализовано умножение столбиком положительных десятичных чисел, а также ввод длинных чисел в виде строк. В каждом элементе массива (тип uint32_t) мы храним по одной десятичной цифре. Лучше будет использовать не десятичную СС, а СС с основанием 109 (в этой СС не 10 цифр, а 1'000'000'000 цифр). При этом массивы с цифрами будут короче, а значит будет меньше потребление памяти и быстрее вычисления.
Почему выбрано именно base 109?
- Числа из СС с основанием 10x легко преобразовывать в десятичные строки и обратно (одна цифра длинного числа содержит ровно x десятичных цифр)
- Тип uint32_t умещает
109 − 1
, но не умещает1010 − 1
, поэтому base 1010 уже не годится
Таким образом, программа будет иметь вид: 1_basics_mul_2.cpp.
Обратите внимание, что при промежуточных вычислениях
используется тип uint64_t, который способен уместить значение base2 − 1
.
Литература:
- https://en.wikipedia.org/wiki/Multiplication_algorithm#Other_notations
- http://cppalgo.blogspot.com/2010/05/blog-post.html
- https://www.geeksforgeeks.org/multiply-large-numbers-represented-as-strings/
- https://brestprog.by/topics/longarithmetics/
Рассмотрим программу 1_basics_sub_1.cpp, в которой реализовано вычитание столбиком длинных чисел. В целом, алгоритм похож на сложение 1_basics_add_2.cpp, только в нём происходит не перенос единицы в старший разряд, а заём единицы из старшего разряда. Так как цифры хранятся в типе uint32_t, то перед вычитанием цифр производится сравнение, чтобы результатом вычисления не стало отрицательное число (и переполнение). Для избавления от лишних ветвлений воспользуемся тем фактом, что в C++ при преобразовании bool в число всегда получается 0 или 1.
uint32_t borrow = minuend_digit < subtrahend_digit;
assert(borrow == 0 || borrow == 1);
Таким образом, программа будет иметь следующий вид: 1_basics_sub_2.cpp.
Самый простой и самый медленный способ деления: из числителя (делимого) вычитаем знаменатель (делитель) до тех пор, пока остаток не станет меньше знаменателя.
Пример: 91 / 8 = 91 − 8 − 8 − 8 − 8 …
Восьмёрку удаётся вычесть 11 раз (значит неполное частное = 11) и остаётся 3, которая меньше 8.
Таким образом, для данного алгоритма также потребуется операция сравнения. Реализация: 1_basics_div.cpp. Обратите внимание, что при сравнении очень важно, чтобы у чисел не было ведущих нулей.
Недостатоком алгоритма является то, что если числитель большой, а знаменатель маленький, то число циклов будет огромным.
Литература:
- https://ru.wikipedia.org/wiki/Алгоритм_деления#Деление_путём_вычитаний
- https://en.wikipedia.org/wiki/Division_algorithm
Следующая глава: 2. Длина неполного частного