Статьи
Telegram YouTube Отзывы

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

Видеогайд

6 мая 2022

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

Содержание

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

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

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

Сама последовательность довольно проста. Она состоит из целых положительных чисел, где каждый следующий элемент равен сумме двух предыдущих. Например, 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 алгоритм всё равно работает шустро.

Рекурсивный вариант

Алгоритм поиска чисел Фибоначчи также можно реализовать с помощью рекурсии, т.е. когда метод будет вызывать сам себя.

public long getFibonacciByIndexRecursive(int n) {
    if (n == 0) {
        return 0L;
    }
    if (n == 1) {
        return 1L;
    } else {
        return getFibonacciByIndexRecursive(n - 1) + getFibonacciByIndexRecursive(n - 2);
    }
}

В любом методе с рекурсией обязательно должны быть условия остановки, проверяющие пограничные значения. Здесь мы проверяем индекс, переданный в параметре. Если он 0 или 1 – тут же возвращаем число. Иначе возвращаем результат, как вызов этого же метода с параметрами n – 1 и n – 2 с последующим их суммированием.

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

Пример на 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 11, 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.

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


Комментарии

25.10.2022 18:43 Сергей

Добавьте пример на Kotlin с использованием рекурскии.

26.10.2022 23:44 devmark

Сергей, я добавил вариант реализации алгоритма с рекурсией и указал на его нюансы.

22.02.2023 20:47 Артём

В Intellij IDEA всё получилось, но в другом компиляторе такая ошибка:
Can't compile file:
program.kt:12:24: error: cannot access 'TWO': it is private in 'BigInteger'
    var i = BigInteger.TWO
                     ^
Unable to find classes\programkt.class

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

×

devmark.ru