5. Оптимизировать вычисление числа Фибоначчи
Условие задачи:
Дана рекурсивная реализация функции Фибоначчи:
int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
Необходимо провести рефакторинг:
устранить повторные вычисления;
улучшить производительность;
корректно обработать отрицательные значения
n;оценить временную и пространственную сложность решения.
Последовательность Фибоначчи:
n: 0, 1, 2, 3, 4, 5, 6, ...
f: 0, 1, 1, 2, 3, 5, 8, ...
Спойлеры к решению
Подсказки
💡 Например, при вычислении
fib(5) значение fib(3) будет вычислено несколько раз.💡 Если рекурсия не является требованием, проще всего заменить её итерацией.
💡 Для вычисления очередного числа достаточно хранить только два предыдущих значения.
Решение
Предпочтительный вариант — итеративное решение:
public static int fib(int n) {
if (n < 0) {
throw new IllegalArgumentException(
"n должен быть неотрицательным"
);
}
if (n <= 1) {
return n;
}
int previous = 0;
int current = 1;
for (int i = 2; i <= n; i++) {
int next = previous + current;
previous = current;
current = next;
}
return current;
}
Например:
System.out.println(fib(0)); // 0
System.out.println(fib(1)); // 1
System.out.println(fib(5)); // 5
System.out.println(fib(10)); // 55
В исходном варианте:
return fib(n - 1) + fib(n - 2);
строится дерево рекурсивных вызовов, в котором одни и те же значения вычисляются повторно.
Например:
fib(5)
├── fib(4)
│ ├── fib(3)
│ └── fib(2)
└── fib(3)
fib(3) уже вычисляется как минимум дважды, а с ростом n число повторных вызовов быстро увеличивается.
Временная сложность такой рекурсии экспоненциальная — O(φⁿ), где φ ≈ 1.618. Её также часто упрощённо оценивают сверху как O(2ⁿ).
Итеративный вариант выполняет один проход:
for (int i = 2; i <= n; i++)
и хранит только два предыдущих значения.
Сложность после рефакторинга:
время —
O(n);дополнительная память —
O(1).
Если требуется сохранить рекурсивный подход, можно использовать мемоизацию:
public static int fib(int n) {
if (n < 0) {
throw new IllegalArgumentException(
"n должен быть неотрицательным"
);
}
Integer[] cache = new Integer[n + 1];
return fib(n, cache);
}
private static int fib(int n, Integer[] cache) {
if (n <= 1) {
return n;
}
if (cache[n] != null) {
return cache[n];
}
cache[n] = fib(n - 1, cache)
+ fib(n - 2, cache);
return cache[n];
}
У варианта с мемоизацией:
время —
O(n);память —
O(n)на кэш и стек рекурсии.
Для обычной задачи итеративная версия проще и экономнее по памяти.
Важно также учитывать диапазон типа int: F(46) ещё помещается в int, а F(47) уже приводит к переполнению. Для больших значений следует использовать long, а затем при необходимости BigInteger.