GrabDuck

Рекурсия. Беглый взгляд

:

image

Ниже речь пойдёт о старушке рекурсии, которую неплохо бы представлять, понимать и применять.

Примечание: Данная небольшая статья написана для беглого ознакомления с рекурсией, некоторыми примерами её применения и опасностями.


Для начала стоит сказать, что рекурсия относится не только к программированию. Рекурсия — это общее понятие, которое может быть присуще чему угодно и встречаться в повседневной жизни, но больше всего она распространена в информатике и математике. Для программистов же умение применять рекурсию — большой плюс в коллекцию полезных навыков.


Самая большая глупость — это делать то же самое и надеяться на другой результат.

Под рекурсией понимают процесс повторения элементов самоподобным образом. Объект обладает рекурсией, если он является частью самого себя.

Частным случаем рекурсии является хвостовая рекурсия. Если любой рекурсивный вызов является последней операцией перед возвратом из функции, то это оно.


Рекурсию надо бы понять, а определение для этого подходит хуже, чем наглядные примеры. Для лучшего понимания, конечно, всё же следует прочитать определение, посмотреть на пример, снова прочитать определение и снова посмотреть на пример… Повторять, пока не придёт осознание.

Отличный пример вы можете найти тут.

Самое известное программисту применение рекурсии — задачи на вычисление чисел Фибоначчи или факториала. Давайте покажем, как это реализовать на языке C:

int fib(int n) { 
    if (n == 0) return 0; 
    if (n == 1 || n == 2) return 1; 
    return fib(n - 1) + fib(n - 2); 
} 

Можно показать, как это выглядело бы на Prolog
fib(1,1):-!.
fib(2,1):-!.
fib(N,R):- 
        N1 is N-1, fib(N1, R1),   
        N2 is N-2, fib(N2, R2),   
        R is R1 + R2.

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


Вычисление факториала. Пример хвостовой рекурсии
int factorial_rec(int n, int res) {
    if (n == 0) return res;
    return factorial_rec(n - 1, res * n);
}
int factorial(int n) {
    return factorial_rec(n, 1);
}

Ещё один опасный и интересный пример

Fork-бомба
Примечание: Рекурсивное создание процессов крайне быстро (из-за экспоненциального роста их количества) заполняет таблицу процессов, что достаточно опасно для системы.

 #include <unistd.h>
 int main() {
   while(true) fork();
 }

Reboot кнопкой после такого делать немного не приятно.

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

Самые известные фракталы:


Кривая (снежинка) Коха

image


Треугольник Серпинского

image


Дерево Пифагора

image

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


image

Проста ли рекурсия? Однозначно нет. На вид кажется, что всё просто, однако рекурсия таит в себе опасности (А иногда она просто не понятна).

Вернёмся к примеру с вычислением чисел Фибоначчи. Сразу заметим, что возвращаемым результатом функции является вызов этой же функции, а если быть точнее, то сумма результатов вызова двух функций (именно поэтому рекурсия не хвостовая). Становится понятно, что второй вызов не произойдёт, пока не завершится первый (в котором также будет вызов двух функций). Тут же пытливый ум заметит, что из рекурсивной функции должен существовать "нормальный" выход, без самовызова, иначе мы познакомимся с переполнением стека вызовов — это один из ключевых моментов, который стоит держать в голове при работе с функциями вызывающими сами себя.

Заметим, что дерево вызовов получится большим, но максимальное количество вызовов в стеке будет заметно меньше (N-1 при N > 2, соответственно).


Пример дерева вызовов для числа 6

image

Рекурсивные алгоритмы довольно-таки часто встречаются при работе с деревьями, сортировками и задачами на графах. Так что, чтобы лучше вникнуть нужна практика и для этого не плохо подходит вышеупомянутое (в частности, бинарные или общие деревья. Их реализация не так сложна, а опыт работы с рекурсией получится не плохой).

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

Для полноты картины обязательно надо упомянуть о борьбе с рекурсией.


Зачем же с ней бороться?

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


Под силу ли побороть любую рекурсию?

Однозначно да. Любой рекурсивный алгоритм можно переписать без использования рекурсии, а хвостовую рекурсию же очень легко перевести на итерацию (чем и занимаются некоторые компиляторы для оптимизации). Это также относится и к итерационным алгоритмам.

Самый известный способ — это использование стека. Здесь подробнее, для интересующихся.


Спасибо за прочтение статьи. Надеюсь, что большинство не знакомых с рекурсией получили базовое представление о ней, а от знающих людей, конечно, хочется услышать дополнения и замечания в комментариях. Не бойтесь рекурсии и не переполняйте стек!

UPD: Добавлен корректный пример хвостовой рекурсии.