Pull to refresh

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

Reading time3 min
Views35K

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: Добавлен корректный пример хвостовой рекурсии.

Tags:
Hubs:
Total votes 29: ↑18 and ↓11+7
Comments34

Articles