этот товарищ.
Не будем тянуть кота за хвост и начнём!
Задача проста как '2' + 2 = 22. Нужно всего-то посчитать 100_000 число последовательности Фибоначчи, делов-то!
Если кто-то забыл, то вот основная формула для расчёта
Мы как всегда будем хитрить и писать с некоторыми оптимизациями.
- А почему?
- А почему бы и нет?
(c) Кто-то
Но начнём сначала с написания функционала для расчёта больших чисел. Как-никак это самое главное!
Писать как всегда будем на rust, т.к. я давно что-то его не использовал, да и нравиться мне этот язык!
Давайте сначала подумаем как мы будем представлять числа в памяти... Подумали? Молодцы!
Для простоты реализации числа будем укладывать в вектор в обратном порядке. Далее вы поймёте почему именно в обратном.
Т.е. число 852_493_284_923_849_834_982 в векторе будет выглядеть как-то так:
let a = vec![2, 8, 9, 4, 3, 8, 9, 4, 8, 3, 2, 9, 4, 8, 2, 3, 9, 4, 2, 5, 8];
Для этого нам нужна некоторая структура + реализуем макрос с помощью которого будет легко задавать числа.
#[derive(Clone)]
struct BigUInt {
digit: Vec<u8>,
}
macro_rules! big_vec {
($($x:tt)*) => {
{
// соберём все термы в другой макрос
let mut value = vec![$($x)*];
// перевернём массив
value.reverse();
// и инициализируем наше большое число
BigUInt::from_vec(value)
}
}
}
Вы наверное заметили BigUInt::from_vec, которая у нас не определена. Давайте исправим эту оплошность!
impl BigUInt {
fn from_vec<T: Into<Vec<u8>>>(digit: T) -> BigUInt {
// ничего сложного, просто наш T должен реализовывать трейт Into преобразующий digit в вектор
BigUInt {
digit: digit.into(),
}
}
}
Теперь перейдём к самому интересному и напишем реализацию оператора сложения.
Всё достаточно просто реализуется в два шага:
Сначала напишем функцию для выравнивания наших больших чисел.
fn align(&mut self, n: usize) {
// получаем длину нашего вектора
let curr_size = self.digit.len();
// если меньше необходимой длины, то
if curr_size < n {
// инициализируем вектор с нулями длины `n - curr_size`
let mut tmp = vec![0; n - curr_size];
// и добавляем в конец нашего вектора
self.digit.append(&mut tmp);
}
}
Не забываем что этот метод находится в блоке impl BigUInt.
Теперь перейдём к оператору сложения
use std::ops::Add;
impl Add for BigUInt {
type Output = Self;
// не забываем про mut, т.к. функция align изменяет наши значения
fn add(mut self, mut other: Self) -> Self {
// результирующий вектор
let mut value: Vec<u8> = Vec::new();
// значение переноса
let mut carry = 0;
// выравниваем вектора по большему
let m_len = self.digit.len().max(other.digit.len());
self.align(m_len);
other.align(m_len);
// складываем значения с помощью упаковки двух итераторов
for (a, b) in self.digit.iter().zip(other.digit.iter()) {
// сумма двух цифр + перенос
carry += a + b;
// добавляем в вектор только остаток от деления
value.push(carry % 10);
// а перенос идёт дальше
carry /= 10;
}
// добавляем значение переноса, если он не ноль
if carry != 0 {
value.push(carry);
}
// Ура, готово!
BigUInt::from_vec(value)
}
}
Я знаю что это не лучший вариант, но он прост в реализации и понятен.
Вроде всё готово! Так давайте сложим два больших числа!
fn main() {
let a = big_vec![4, 5, 9, 4, 5, 7, 3, 9, 5, 3, 9, 5, 9, 4, 5, 2, 4, 4, 9, 3, 7, 4, 9];
let b = big_vec![2, 3, 1, 2, 4, 4, 0, 7, 3, 5, 0, 9, 0, 5, 5, 1];
println!("a = {}", a);
println!("b = {}", b);
let c = a + b;
println!("c = {}", c);
}
А теперь скомпилируем и запустим
$ rustc demo.rs
error[E0277]: `BigUInt` doesn't implement `std::fmt::Display`
--> demo.rs:79:24
|
79 | println!("a = {}", a);
| ^ `BigUInt` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `BigUInt`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required by `std::fmt::Display::fmt`
error[E0277]: `BigUInt` doesn't implement `std::fmt::Display`
--> demo.rs:80:24
|
80 | println!("b = {}", b);
| ^ `BigUInt` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `BigUInt`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required by `std::fmt::Display::fmt`
error[E0277]: `BigUInt` doesn't implement `std::fmt::Display`
--> demo.rs:82:24
|
82 | println!("c = {}", c);
| ^ `BigUInt` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `BigUInt`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: required by `std::fmt::Display::fmt`
error: aborting due to 3 previous errors
For more information about this error, try `rustc --explain E0277`.
Ой-ёй, кто-то забыл реализовать трейт Display для нашего типа.
use std::fmt;
impl fmt::Display for BigUInt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
self.digit
.iter()
// числа лежат в обратном порядке
.rev()
// простое преобразование из u8 в char для цифр
.map(|&x| (x + 0x30) as char)
.collect::<String>()
)
}
}
Ну, а теперь соберём и проверим с помощью python
$ rustc demo.rs
$ ./demo
a = 45945739539594524493749
b = 2312440735090551
c = 45945741852035259584300
$ python
Python 3.8.1 (default, Jan 22 2020, 06:38:00)
[GCC 9.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 45945739539594524493749
>>> b = 2312440735090551
>>> c = 45945741852035259584300
>>> a + b == c
True
Вроде всё хорошо. Давайте перейдём к основной задаче.
Напишем наивную реализацию, а потом сделаем небольшие улучшения.
fn fib(n: usize) -> BigUInt {
let mut f0 = big_vec![0];
let mut f1 = big_vec![1];
for _ in 0..n {
let f2 = f0.clone() + f1.clone();
f0 = f1.clone();
f1 = f2.clone();
}
f0
}
В наивной реализации сразу бросается в глаза множество .clone(), что не есть хорошо. Также когда мы создаём f0 и f1, то мы делаем несколько ненужных операций .reverse(), что для 0 и 1 вообще не нужно.
Так давайте оптимизировать!
// добавим вспомогательные функции в BigUInt, чтобы не использовать big_vec!
impl BigUInt {
fn zero() -> BigUInt {
BigUInt { digit: vec![0] }
}
fn one() -> BigUInt {
BigUInt { digit: vec![1] }
}
}
fn fib(n: usize) -> BigUInt {
let mut f0 = BigUInt::zero();
let mut f1 = BigUInt::one();
for _ in 0..n {
// f0 нам не нужна, т.к. дальше мы её всё равно переопределяем
let f2 = f0 + f1.clone();
// спасибо за наводку из документации num-bigint
// читай в доках, для лучшего понимания (`rustup doc --std std::mem::replace`)
f0 = replace(&mut f1, f2);
}
f0
}
Уже лучше! На этом наша реализация готова, но давайте используем сторонний крейт, где работа с большими числами сделана более оптимально.
Не будем заострять внимание на отдельных частях, а сразу "нырнём" в код, который представлен в документации крейта num-bigint
extern crate num_bigint;
extern crate num_traits;
use num_bigint::BigUint;
use num_traits::{One, Zero};
use std::mem::replace;
fn fib(n: usize) -> BigUint {
// наш 0
let mut f0: BigUint = Zero::zero();
// и 1
let mut f1: BigUint = One::one();
// аналогичный цикл
for _ in 0..n {
let f2 = f0 + &f1;
f0 = replace(&mut f1, f2);
}
f0
}
Не забудь добавить зависимости в Cargo.toml
[dependencies]
num-bigint = "*"
num-traits = "*"
Вот и всё, теперь можно перейти к тестам!
Все приложения были собраны с флагом -C opt-level=3, что соответствует 3-му уровню оптимизации кода.
На моей машине с процессором i5-8265U приложения выполняются за следующее время:
Информация для тех, кто захочет повторить:
Для более точного замера предлагаю сделать не менее 10 тестов и посчитать всякие умные метрики.
Мы же ограничились выбором минимального времени из 10 тестов для каждого из примеров.
Выводы думаю сделаете сами.
Вот так просто и незатейливо... А, это уже я писал. Короче вроде ничего сложного, а у нас уже есть простая реализация больших чисел.
Весь исходный код доступен по ссылке. Если мне будет скучно, то я добавлю реализацию ещё на паре языков или оптимизирую текущий код.
На этом сегодня всё! Не страдайте фигнёй и надеюсь скоро увидимся... услышимся... учитаемся... Короче пока!
Сегодня их не будет, т.к. я ничего не читал для реализации, а хотя...