Почему рекурсия — это все-таки зло? И как не нужно писать программки на собеседовании в Google?

16 мая 2011 г.

Недавно столкнулся с интересной ситуацией, связанной с тем в какие неприятности можно попасть, написав совсем простой, маленький код, использующий рекурсию.

Классическим примером рекурсии является определение чисел Фибоначчи. N-е число Фибоначчи определяется как сумма (N-1)-го и (N-2)-го числа Фибоначчи. Исключение составляют первые два числа Фибоначчи — по определению первое равно 0, а второе равно 1.
F[0] = 0,
F[1] = 1,
F[n] = F[n-2]+F[n-1], для n >= 2.

Итак, два следующих алгоритма (код написан на языке Java) для нахождения n-го числа Фибоначчи являются очевидными. Первый из них рекурсивный и второй нерекурсивный.

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

long nonrecursivefib(int n){
 if (n <= 0) return 0L;
 else if(n == 1) return 1L;
 else {
 long value0 = 0L;
 long value1 = 1L;
 long value2 = 1L;
 for (int i = 0; i < n-1; i++){
 value2 = value0+value1;
 value0 = value1;
 value1 = value2;
 }
 return value2;
 }
 }


Предположим, что индекс n не превышает 161 004 — это максимальное число Фибоначчи, помещающееся в 63 старших бита (не считая 0-го знакового) типа long и равное 7 829 159 639 441 890 064.

Далее если вы попробуете посчитать 5-е число Фибоначчи:

void main(String[] args) {
 System.out.println(recursivefib(5));
 System.out.println(nonrecursivefib(5));
 }

или 8-е

void main(String[] args) {
 System.out.println(recursivefib(8));
 System.out.println(nonrecursivefib(8));
 }

или даже 13-е

void main(String[] args) {
 System.out.println(recursivefib(13));
 System.out.println(nonrecursivefib(13));
 }

то скорее всего не увидите разницы.

Но если вы попробуете посчитать 43-е число Фибоначчи:

void main(String[] args) {
 System.out.println(recursivefib(43));
 System.out.println(nonrecursivefib(43));
 }

то наверное что-то заподозрите, а когда вы попробуете посчитать 65-е число Фибоначчи:

void main(String[] args) {
 System.out.println(recursivefib(65));
 System.out.println(nonrecursivefib(65));
 }
 

то поймете в чем проблема, и что проблема не в переполнении стека, а программа просто зависла и зависла так где-то лет на 200. Мы в своей собственной жизни не увидим 65-е число Фибоначчи, его увидят наши далекие пра пра пра… правнуки.

Что происходит?

Оказывается, что если, очевидно, порядок сложности второго нерекурсивного алгоритма равен O(n), где n-номер искомого числа Фибоначчи, то порядок сложности рекурсивного алгоритма, неочевидно, равен O(2^n) два в степени n.

Почему в рекурсивном алгоритме выполняется порядка 2^n операций — вам предлагается разобраться самостоятельно.

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

Кстати, есть и более быстрые громоздкие алгоритмы для вычисления чисел Фибоначчи, имеющие порядок сложности O(log2(n)).

И последнее, что хочется сказать, так это то, что никогда! никогда! не нужно писать такие «эффективные» рекурсивные программки на собеседовании в Google.

Теги: рубрика Java
  • Похожие статьи
  • Предыдущие из рубрики