Статьи
YouTube-канал

Алгоритм чисел Фибоначчи на Java и Kotlin

Видеогайд

6 мая 2022

Тэги: Java, Kotlin, алгоритмы, головоломки.

Содержание

  1. Немного теории
  2. Простая реализация на Java
  3. Поддержка больших чисел
  4. Пример на Kotlin
  5. Итоги

Немного теории

На собеседованиях по алгоритмам любят давать такую задачку, как нахождение чисел из последовательности Фибоначчи. Эта числовая последовательность названа в честь Леонардо Пизанского - известного математика Средневековья.

Сама последовательность довольно проста. Она состоит из целых положительных чисел, где каждый следующий элемент равен сумме двух предыдущих. Например, 2 = 1 + 1, 3 = 2 + 1, 5 = 3 + 2.

Последовательность начинается с 0 и 1:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144 ...

Иногда в этой последовательности принято пропускать 0 и начинать её с двух единиц, однако официально она начинается именно с 0.

Эта последовательность описывает правило «золотого сечения», когда мы делим нечто целое на две неравные части, где целая часть так же пропорциональна бОльшей, как и бОльшая к меньшей.

Считается, что это правило также часто встречается в природе. Например, в спиралях раковины моллюска или в спирали Млечного Пути.

Простая реализация на Java

Давайте напишем метод, который будет возвращать число Фибоначчи по его индексу в этой последовательности. Числа в последовательности растут очень быстро, поэтому значения будем хранить в типе long.

public long getFibonacciByIndex(int index) {
    if (index == 0) {
        return 0; // первый элемент последовательности
    }
    if (index < 0 || index > 92) {
        // при индексе 93 происходит переполнение типа long
        throw new IndexOutOfBoundsException(index);
    }
    var n0 = 0L;
    var n1 = 1L;
    for (int i = 2; i <= index; i++) {
        var n2 = n0 + n1;
        n0 = n1;
        n1 = n2;
    }
    return n1;
}

Алгорим тут довольно простой: заводим первые два числа последовательности как n0 и n1. Затем в цикле, начиная с индекса 2, вычисляем новое значение как их сумму. Это новое значение становится n1, а то, что было n1 - становится n0. По достижении необходимого индекса выходим из цикла и возвращаем результат.

Обратите внимание на обработку пограничных случаев. Если индекс равен 0 - сразу возвращаем первый элемент последовательности, т.е. 0. Отрицательный индекс недопустим - кидаем стандартное исключение IndexOutOfBoundsException.

Также кидаем исключение, если индекс больше чем 92 - тут происходит переполнение типа long. При переполнении Java ошибку не кидает, но число при этом становится некорректным (например, отрицательным). Это связано со сдвигом и потерей части разрядов в двоичном представлении числа.

Теперь можем вызвать наш метод и посмотреть число Фибоначчи с индексом, например, 42.

public class Fibonacci {
    public static void main(String[] args) {
        var fibonacci = new Fibonacci();
        System.out.println(fibonacci.getFibonacciByIndex(42)); // выведет 267914296
    }
    // ...
}

В качестве альтернативы есть также и рекурсивный алгоритм. Но он будет заведомо медленнее, чем рассмотренный выше. Кроме того, для больших чисел можно довольно быстро поймать StackOverflowException. Поэтому я не вижу смысла его рассматривать.

Ну а данная реализация работает довольно шустро и её единственный недостаток - это поддержка только первых 92 членов последовательности. Но что, если мы хотим вычислить элемент с индексом 100, 1000 или даже 1 000 000 ?

Поддержка больших чисел

На помощь нам придёт класс BigInteger. Он довольно громоздкий с точки зрения скорости вычисления и с точки зрения написания кода, но при этом поддерживает сколь угодно большие числа. Поэтому заменим long на BigInteger. Желаемый номер элемента также будем передавать в метод как BigInteger.

public BigInteger getFibonacciByIndexInfinite(BigInteger index) {
    if (index.compareTo(BigInteger.ZERO) == 0) {
        return BigInteger.ZERO;
    }
    if (index.compareTo(BigInteger.ZERO) < 0) {
        throw new IndexOutOfBoundsException(index.toString());
    }
    var n0 = BigInteger.ZERO;
    var n1 = BigInteger.ONE;
    for (var i = BigInteger.TWO; i.compareTo(index) <= 0; i = i.add(BigInteger.ONE)) {
        var n2 = n0.add(n1);
        n0 = n1;
        n1 = n2;
    }
    return n1;
}

Обратите внимание, что тут мы уже не можем использовать числа типа 0 и 1. Вместо этого пишем константы BigInteger.ZERO и BigInteger.ONE. Сравнение чисел также усложняется - используем метод compareTo(). Для сложения вместо плюса пишем метод add(). Поскольку BigInteger поддерживает очень большие числа, то проверку на максимальный индекс убираем.

Теперь мы можем найти элемент с порядковым номером 1 000 000. Обратите внимание, что BigInteger можно инициализировать через текстовое представление числа.

System.out.println(fibonacci.getFibonacciByIndexInfinite(new BigInteger("1000000")));
// найденное число состоит из 208988 цифр!

Найденный результат представляет собой столь большое число, что оно не помещается на экран консоли! Даже на 20 экранов... Однако и это число на моём ноуте было вычислено всего-то за 15 секунд.

Согласитесь, вряд ли может потребоваться вычислять столь большие значения, поэтому можно считать, что даже с BigInteger алгоритм всё равно работает шустро.

Пример на Kotlin

В дополнение приведу этот же метод на Kotlin, чтобы продемонстрировать лаконичность его синтаксиса.

fun getFibonacciByIndexInfinite(index: BigInteger): BigInteger {
    if (index == BigInteger.ZERO) {
        return BigInteger.ZERO
    }
    if (index < BigInteger.ZERO) {
        throw IndexOutOfBoundsException(index.toString())
    }
    var n0 = BigInteger.ZERO
    var n1 = BigInteger.ONE
    var i = BigInteger.TWO
    while (i <= index) {
        val n2 = n0 + n1
        n0 = n1
        n1 = n2
        i++
    }
    return n1
}

Здесь всё ровно то же самое, только мы заменили цикл for на while. А также вернули обратно привычные нам операторы сравнения и инкремента. Теперь вызовем этот метод:

fun main() {
    println(Fibonacci().getFibonacciByIndexInfinite(BigInteger("1000000")))
}

Время вычисления не увеличилось по сравнению с Java-версией, поскольку компилятор Kotlin вызывает те же самые compareTo() и add(), генерируя почти такой же байт-код.

Итоги

Мы рассмотрели несколько реализаций нахождения числа Фибоначчи по его индексу. Наиболее простая реализация на Java использует примитивный тип long. Он работает быстро, но уже на 93-м элементе последовательности происходит переполнение.

Чтобы вычислять числа с бОльшим индексом, мы использовали класс BigInteger, который хоть и значительно медленнее примитива, но всё равно довольно шустро вычисляет огромные числа.

Наконец, мы рассмотрели реализацию того же самого метода на Kotlin. Благодаря его лаконичному синтаксису метод выглядит менее громоздко и при этом не теряет в производительности.


Облако тэгов

Kotlin, Java, Java 16, Java 11, Java 10, Java 9, Java 8, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.

Последние статьи


Комментарии

Добавить комментарий

×

devmark.ru