6 мая 2022
Тэги: Java, Kotlin, алгоритмы, головоломки.
На собеседованиях по алгоритмам любят давать такую задачку, как нахождение чисел из последовательности Фибоначчи. Эта числовая последовательность названа в честь Леонардо Пизанского - известного математика Средневековья.
Сама последовательность довольно проста. Она состоит из целых положительных чисел, где каждый следующий элемент равен сумме двух предыдущих. Например, 2 = 1 + 1, 3 = 2 + 1, 5 = 3 + 2.
Последовательность начинается с 0 и 1:
Иногда в этой последовательности принято пропускать 0 и начинать её с двух единиц, однако официально она начинается именно с 0.
Эта последовательность описывает правило «золотого сечения», когда мы делим нечто целое на две неравные части, где целая часть так же пропорциональна бОльшей, как и бОльшая к меньшей.
Считается, что это правило также часто встречается в природе. Например, в спиралях раковины моллюска или в спирали Млечного Пути.
Давайте напишем метод, который будет возвращать число Фибоначчи по его индексу в этой последовательности. Числа в последовательности растут очень быстро, поэтому значения будем хранить в типе long.
Алгорим тут довольно простой: заводим первые два числа последовательности как n0 и n1. Затем в цикле, начиная с индекса 2, вычисляем новое значение как их сумму. Это новое значение становится n1, а то, что было n1 - становится n0. По достижении необходимого индекса выходим из цикла и возвращаем результат.
Обратите внимание на обработку пограничных случаев. Если индекс равен 0 - сразу возвращаем первый элемент последовательности, т.е. 0. Отрицательный индекс недопустим - кидаем стандартное исключение IndexOutOfBoundsException.
Также кидаем исключение, если индекс больше чем 92 - тут происходит переполнение типа long. При переполнении Java ошибку не кидает, но число при этом становится некорректным (например, отрицательным). Это связано со сдвигом и потерей части разрядов в двоичном представлении числа.
Теперь можем вызвать наш метод и посмотреть число Фибоначчи с индексом, например, 42.
В качестве альтернативы есть также и рекурсивный алгоритм. Но он будет заведомо медленнее, чем рассмотренный выше. Кроме того, для больших чисел можно довольно быстро поймать StackOverflowException. Поэтому я не вижу смысла его рассматривать.
Ну а данная реализация работает довольно шустро и её единственный недостаток - это поддержка только первых 92 членов последовательности. Но что, если мы хотим вычислить элемент с индексом 100, 1000 или даже 1 000 000 ?
На помощь нам придёт класс BigInteger. Он довольно громоздкий с точки зрения скорости вычисления и с точки зрения написания кода, но при этом поддерживает сколь угодно большие числа. Поэтому заменим long на BigInteger. Желаемый номер элемента также будем передавать в метод как BigInteger.
Обратите внимание, что тут мы уже не можем использовать числа типа 0 и 1. Вместо этого пишем константы BigInteger.ZERO и BigInteger.ONE. Сравнение чисел также усложняется - используем метод compareTo(). Для сложения вместо плюса пишем метод add(). Поскольку BigInteger поддерживает очень большие числа, то проверку на максимальный индекс убираем.
Теперь мы можем найти элемент с порядковым номером 1 000 000. Обратите внимание, что BigInteger можно инициализировать через текстовое представление числа.
Найденный результат представляет собой столь большое число, что оно не помещается на экран консоли! Даже на 20 экранов... Однако и это число на моём ноуте было вычислено всего-то за 15 секунд.
Согласитесь, вряд ли может потребоваться вычислять столь большие значения, поэтому можно считать, что даже с BigInteger алгоритм всё равно работает шустро.
В дополнение приведу этот же метод на Kotlin, чтобы продемонстрировать лаконичность его синтаксиса.
Здесь всё ровно то же самое, только мы заменили цикл for на while. А также вернули обратно привычные нам операторы сравнения и инкремента. Теперь вызовем этот метод:
Время вычисления не увеличилось по сравнению с 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.