Как решать рекурсивные алгоритмы
Рекурсия. Занимательные задачки
В этой статье речь пойдет о задачах на рекурсию и о том как их решать.
Кратко о рекурсии
Рекурсия достаточно распространённое явление, которое встречается не только в областях науки, но и в повседневной жизни. Например, эффект Дросте, треугольник Серпинского и т. д. Один из вариантов увидеть рекурсию – это навести Web-камеру на экран монитора компьютера, естественно, предварительно её включив. Таким образом, камера будет записывать изображение экрана компьютера, и выводить его же на этот экран, получится что-то вроде замкнутого цикла. В итоге мы будем наблюдать нечто похожее на тоннель.
В программировании рекурсия тесно связана с функциями, точнее именно благодаря функциям в программировании существует такое понятие как рекурсия или рекурсивная функция. Простыми словами, рекурсия – определение части функции (метода) через саму себя, то есть это функция, которая вызывает саму себя, непосредственно (в своём теле) или косвенно (через другую функцию).
Задачи
При изучении рекурсии наиболее эффективным для понимания рекурсии является решение задач.
Любой алгоритм, реализованный в рекурсивной форме, может быть переписан в итерационном виде и наоборот. Останется вопрос, надо ли это, и насколько это будет это эффективно.
Для обоснования можно привести такие доводы.
Для начала можно вспомнить определение рекурсии и итерации. Рекурсия — это такой способ организации обработки данных, при котором программа вызывает сама себя непосредственно, либо с помощью других программ. Итерация — это способ организации обработки данных, при котором определенные действия повторяются многократно, не приводя при этом к рекурсивным вызовам программ.
После чего можно сделать вывод, что они взаимно заменимы, но не всегда с одинаковыми затратами по ресурсам и скорости. Для обоснования можно привести такой пример: имеется функция, в которой для организации некого алгоритма имеется цикл, выполняющий последовательность действий в зависимости от текущего значения счетчика (может от него и не зависеть). Раз имеется цикл, значит, в теле повторяется последовательность действий — итерации цикла. Можно вынести операции в отдельную подпрограмму и передавать ей значение счетчика, если таковое есть. По завершению выполнения подпрограммы мы проверяем условия выполнения цикла, и если оно верно, переходим к новому вызову подпрограммы, если ложно — завершаем выполнение. Т.к. все содержание цикла мы поместили в подпрограмму, значит, условие на выполнение цикла помещено также в подпрограмму, и получить его можно через возвращающее значение функции, параметры передающееся по ссылке или указателю в подпрограмму, а также глобальные переменные. Далее легко показать, что вызов данной подпрограммы из цикла легко переделать на вызов, или не вызов (возврата значения или просто завершения работы) подпрограммы из нее самой, руководствуясь какими-либо условиями (теми, что раньше были в условии цикла). Теперь, если посмотреть на нашу абстрактную программу, она примерно выглядит как передача значений подпрограмме и их использование, которые изменит подпрограмма по завершению, т.е. мы заменили итеративный цикл на рекурсивный вызов подпрограммы для решения данного алгоритма.
Задача по приведению рекурсии к итеративному подходу симметрична.
Подводя итог, можно выразить такие мысли: для каждого подхода существует свой класс задач, который определяется по конкретным требованиям к конкретной задаче.
Более подробно с этим можно познакомиться тут
Так же как и у перебора (цикла) у рекурсии должно быть условие остановки — Базовый случай (иначе также как и цикл рекурсия будет работать вечно — infinite). Это условие и является тем случаем к которому рекурсия идет (шаг рекурсии). При каждом шаге вызывается рекурсивная функция до тех пор пока при следующем вызове не сработает базовое условие и произойдет остановка рекурсии(а точнее возврат к последнему вызову функции). Всё решение сводится к решению базового случая. В случае, когда рекурсивная функция вызывается для решения сложной задачи (не базового случая) выполняется некоторое количество рекурсивных вызовов или шагов, с целью сведения задачи к более простой. И так до тех пор пока не получим базовое решение.
Тут Базовым условием является условие когда n=1. Так как мы знаем что 1!=1 и для вычисления 1! нам ни чего не нужно. Чтобы вычислить 2! мы можем использовать 1!, т.е. 2!=1!*2. Чтобы вычислить 3! нам нужно 2!*3… Чтобы вычислить n! нам нужно (n-1)!*n. Это и является шагом рекурсии. Иными словами, чтобы получить значение факториала от числа n, достаточно умножить на n значение факториала от предыдущего числа.
В сети при обьяснении рекурсии также даются задачи нахождения чисел Фибоначчи и Ханойская башня
Рассмотрим же теперь задачи с различным уровнем сложности.
Попробуйте их решить самостоятельно используя метод описанный выше. При решении попробуйте думать рекурсивно. Какой базовый случай в задаче? Какой Шаг рекурсии или рекурсивное условие?
Поехали! Решения задач предоставлены на языке Java.
A: От 1 до n
Дано натуральное число n. Выведите все числа от 1 до n.
Задача №11. Использование рекурсивных алгоритмов.
Для того, чтобы задать рекурсию, необходимо описать:
— условие остановки рекурсии (базовый случай);
В программировании если процедура вызывает сама себя, то, по сути, это приводит к повторному выполнению содержащихся в ней инструкций, что аналогично работе цикла. Рекурсия позволяет заменить цикл и в некоторых сложных задачах делает решение более понятным, хотя часто менее эффективным.
Классическим примером рекурсивного алгоритма является описание вычисления факториала:
Рекурсивные алгоритмы вычисления одной функции
Алгоритм вычисления значения функции F(n), где n – натуральное число, задан следующими рекуррентными соотношениями:
Чему равно значение функции F(6)?
В ответе запишите только натуральное число.
Последовательно найдём значения функции от базового случая F(1) до искомого значения F(6):
Рекурсивные алгоритмы вычисления нескольких функций
Алгоритм вычисления значений функций F(n) и G(n), где n – натуральное число, задан следующими соотношениями:
G(n) = F(n–1) + 2*G(n–1), при n >=2
Чему равно значение величины F(5)/G(5)? В ответе запишите только целое число.
Последовательно найдём значения функций от базового случая F(1), G(1) до искомых значений F(5), G(5):
F(2) = F(1) – G(1) = 1 – 1 = 0;
G(2) = F(1) + 2*G(1) = 1+2 = 3;
G(3) = F(2) + 2*G(2) = 0+6 = 6;
Рекурсивные алгоритмы выполнения процедур
Ниже на пяти языках программирования записан рекурсивный алгоритм F.
Бейсик
Python
PRINT n
IF n
Выпишем последовательно все действия, которые выполнят запускаемые процедуры:
F (1) выполнит следующие действия : Вывод числа 1, F(2), F(4)
F (2) выполнит следующие действия : Вывод числа 2, F(3), F(5)
F (4) выполнит следующие действия : Вывод числа 4, F(5), F(7)
F (3) выполнит следующие действия : Вывод числа 3, F(4), F(6)
F (5) выполнит следующие действия : Вывод числа 5
F (5) выполнит следующие действия : Вывод числа 5
F (7) выполнит следующие действия : Вывод числа 7
F (4) выполнит следующие действия : Вывод числа 4, F(5), F(7)
F (6) выполнит следующие действия : Вывод числа 6
F (5) выполнит следующие действия : Вывод числа 5
F (7) выполнит следующие действия : Вывод числа 7
Просуммируем все числа, выведенные на экран: 1+2+4+3+5+5+7+4+6+5+7 = 49
Ниже на пяти языках программирования записаны две рекурсивные функции (процедуры): F и G.
Сколько символов «звёздочка» будет напечатано на экране при выполнении вызова F(11)?
Выпишем последовательно все действия, которые выполнят запускаемые процедуры:
F(11) G(10) * F(7) G(6) * F(3) G(2) * F(-1)
Всего на экране будет напечатано 3 «звездочки».
Дан рекурсивный алгоритм:
procedure F(n: integer);
begin
writeln(‘*’);
if n > 0 then begin
F(n div 2);
F(n div 2);
end;
Сколько символов «звездочка» будет напечатано на экране при выполнении вызова F(6)?
Для наглядности изобразим схему работы алгоритма в виде дерева:
Причем, распишем до конца каждое значение F(n) только один раз. Например, расписав один раз F(1), мы видим, что она напечатает в результате 5 звездочек. Т.е. F(1) = 5.
Проанализировав дерево, видим, что
F(3) = 1 + F(0) + 3*F(1) = 1 + 1 + 15 = 17
F(4) = 1 + F(1) + 3*F(2) = 1 + 5 + 3*13 = 45
F(6) = 1 + 3*F(3) + F(4) = 1 + 3*17 + 45 = 46 + 51 = 97
Ты нашел то, что искал? Поделись с друзьями!
Как работает рекурсия – объяснение в блок-схемах и видео
Представляю вашему вниманию перевод статьи Beau Carnes How Recursion Works — explained with flowcharts and a video.
«Для того чтобы понять рекурсию, надо сначала понять рекурсию»
Рекурсию порой сложно понять, особенно новичкам в программировании. Если говорить просто, то рекурсия – это функция, которая сама вызывает себя. Но давайте попробую объяснить на примере.
Представьте, что вы пытаетесь открыть дверь в спальню, а она закрыта. Ваш трехлетний сынок появляется из-за угла и говорит, что единственный ключ спрятан в коробке. Вы опаздываете на работу и Вам действительно нужно попасть в комнату и взять вашу рубашку.
Вы открываете коробку только чтобы найти… еще больше коробок. Коробки внутри коробок и вы не знаете, в какой из них Ваш ключ. Вам срочно нужна рубашка, так что вам надо придумать хороший алгоритм и найти ключ.
Есть два основных подхода в создании алгоритма для решения данной проблемы: итеративный и рекурсивный. Вот блок-схемы этих подходов:
Какой подход для Вас проще?
В первом подходе используется цикл while. Т.е. пока стопка коробок полная, хватай следующую коробку и смотри внутрь нее. Ниже немного псевдокода на Javascript, который отражает то, что происходит (Псевдокод написан как код, но больше похожий на человеческий язык).
В другом подходе используется рекурсия. Помните, рекурсия – это когда функция вызывает саму себя. Вот второй вариант в псевдокоде:
Оба подхода выполняют одно и тоже. Основный смысл в использовании рекурсивного подхода в том, что однажды поняв, вы сможете легко его читать. В действительности нет никакого выигрыша в производительности от использования рекурсии. Порой итеративный подход с циклами будет работать быстрее, но простота рекурсии иногда предпочтительнее.
Поскольку рекурсия используется во многих алгоритмах, очень важно понять как она работает. Если рекурсия до сих пор не кажется Вам простой, не беспокойтесь: Я собираюсь пройтись еще по нескольким примерам.
Граничный и рекурсивный случай
То, что Вам необходимо принять во внимание при написании рекурсивной функции – это бесконечный цикл, т.е. когда функция вызывает саму себя… и никогда не может остановиться.
Допустим, Вы хотите написать функцию подсчета. Вы можете написать ее рекурсивно на Javascript, к примеру:
Эта функция будет считать до бесконечности. Так что, если Вы вдруг запустили код с бесконечным циклом, остановите его сочетанием клавиш «Ctrl-C». (Или, работая к примеру в CodePen, это можно сделать, добавив “?turn_off_js=true” в конце URL.)
Рекурсивная функция всегда должна знать, когда ей нужно остановиться. В рекурсивной функции всегда есть два случая: рекурсивный и граничный случаи. Рекурсивный случай – когда функция вызывает саму себя, а граничный – когда функция перестает себя вызывать. Наличие граничного случая и предотвращает зацикливание.
И снова функция подсчета, только уже с граничным случаем:
То, что происходит в этой функции может и не быть абсолютно очевидным. Я поясню, что произойдет, когда вы вызовете функцию и передадите в нее цифру 5.
Сначала мы выведем цифру 5, используя команду Console.Log. Т.к. 5 не меньше или равно 1, то мы перейдем в блок else. Здесь мы снова вызовем функцию и передадим в нее цифру 4 (т.к. 5 – 1 = 4).
Мы выведем цифру 4. И снова i не меньше или равно 1, так что мы переходим в блок else и передаем цифру 3. Это продолжается, пока i не станет равным 1. И когда это случится мы выведем в консоль 1 и i станет меньше или равно 1. Наконец мы зайдем в блок с ключевым словом return и выйдем из функции.
Стек вызовов
Рекурсивные функции используют так называемый «Стек вызовов». Когда программа вызывает функцию, функция отправляется на верх стека вызовов. Это похоже на стопку книг, вы добавляете одну вещь за одни раз. Затем, когда вы готовы снять что-то обратно, вы всегда снимаете верхний элемент.
Я продемонстрирую Вам стек вызовов в действии, используя функцию подсчета факториала. Factorial(5) пишется как 5! и рассчитывается как 5! = 5*4*3*2*1. Вот рекурсивная функция для подсчета факториала числа:
Теперь, давайте посмотрим что же происходит, когда вы вызываете fact(3). Ниже приведена иллюстрация в которой шаг за шагом показано, что происходит в стеке. Самая верхняя коробка в стеке говорит Вам, что вызывать функции fact, на которой вы остановились в данный момент:
Заметили, как каждое обращение к функции fact содержит свою собственную копию x. Это очень важное условие для работы рекурсии. Вы не можете получить доступ к другой копии функции от x.
Нашли уже ключ?
Давайте кратенько вернемся к первоначальному примеру поиска ключа в коробках. Помните, что первым был итеративный подход с использованием циклов? Согласно этому подходу Вы создаете стопку коробок для поиска, поэтому всегда знаете в каких коробках вы еще не искали.
Но в рекурсивном подходе нет стопки. Так как тогда алгоритм понимает в какой коробке следует искать? Ответ: «Стопка коробок» сохраняется в стеке. Формируется стек из наполовину выполненных обращений к функции, каждое из которых содержит свой наполовину выполненный список из коробок для просмотра. Стек следит за стопкой коробок для Вас!
И так, спасибо рекурсии, Вы наконец смогли найти свой ключ и взять рубашку!
Вы также можете посмотреть мое пятиминутное видео про рекурсию. Оно должно усилить понимание, приведенных здесь концепций.
Заключение от автора
Надеюсь, что статья внесла немного больше ясности в Ваше понимание рекурсии в программировании. Основой для статьи послужил урок в моем новом видео курсе от Manning Publications под названием «Algorithms in Motion». И курс и статься написаны по замечательной книге «Grokking Algorithms», автором которой является Adit Bhargava, кем и были нарисованы все эти замечательные иллюстрации.
И наконец, чтобы действительно закрепить свои знания о рекурсии, Вы должны прочитать эту статью, как минимум, еще раз.
От себя хочу добавить, что с интересом наблюдаю за статьями и видеоуроками Beau Carnes, и надеюсь что Вам тоже понравилась статья и в особенности эти действительно замечательные иллюстрации из книги A. Bhargav «Grokking Algorithms».
Урок №107. Рекурсия и Числа Фибоначчи
Обновл. 28 Авг 2021 |
На этом уроке мы рассмотрим, что такое рекурсия в языке C++ и зачем её использовать, а также последовательность Фибоначчи и факториал целого числа.
Рекурсия
Рекурсивная функция (или просто «рекурсия») в языке C++ — это функция, которая вызывает сама себя. Например:
На уроке о стеке и куче в С++ мы узнали, что при каждом вызове функции, определенные данные помещаются в стек вызовов. Поскольку функция countOut() никогда ничего не возвращает (она просто снова вызывает countOut()), то данные этой функции никогда не вытягиваются из стека! Следовательно, в какой-то момент, память стека закончится и произойдет переполнение стека.
Условие завершения рекурсии
Рекурсивные вызовы функций работают точно так же, как и обычные вызовы функций. Однако, программа, приведенная выше, иллюстрирует наиболее важное отличие простых функций от рекурсивных: вы должны указать условие завершения рекурсии, в противном случае — функция будет выполняться «бесконечно» (фактически до тех пор, пока не закончится память в стеке вызовов).
Условие завершения рекурсии — это условие, которое, при его выполнении, остановит вызов рекурсивной функции самой себя. В этом условии обычно используется оператор if.
Вот пример функции, приведенной выше, но уже с условием завершения рекурсии (и еще с одним дополнительным выводом текста):
Когда мы запустим эту программу, то countOut() начнет выводить:
push 4
push 3
push 2
push 1
Если сейчас посмотреть на стек вызовов, то увидим следующее:
countOut(1)
countOut(2)
countOut(3)
countOut(4)
main()
Таким образом, результат выполнения программы, приведенной выше:
push 4
push 3
push 2
push 1
pop 1
pop 2
pop 3
pop 4
Стоит отметить, что push выводится в порядке убывания, а pop — в порядке возрастания. Дело в том, что push выводится до вызова рекурсивной функции, а pop выполняется (выводится) после вызова рекурсивной функции, когда все экземпляры countOut() вытягиваются из стека (это происходит в порядке, обратном тому, в котором эти экземпляры были введены в стек).
Теперь, когда мы обсудили основной механизм вызова рекурсивных функций, давайте взглянем на несколько другой тип рекурсии, который более распространен:
Рассмотреть рекурсию с первого взгляда на код не так уж и легко. Лучшим вариантом будет посмотреть, что произойдет при вызове рекурсивной функции с определенным значением. Например, посмотрим, что произойдет при вызове вышеприведенной функции с value = 4 :
sumCount(4). 4 > 1, поэтому возвращается sumCount(3) + 4
sumCount(3). 3 > 1, поэтому возвращается sumCount(2) + 3
sumCount(2). 2 > 1, поэтому возвращается sumCount(1) + 2
sumCount(1). 1 = 1, поэтому возвращается 1. Это условие завершения рекурсии
Теперь посмотрим на стек вызовов:
sumCount(1) возвращает 1
sumCount(2) возвращает sumCount(1) + 2, т.е. 1 + 2 = 3
sumCount(3) возвращает sumCount(2) + 3, т.е. 3 + 3 = 6
sumCount(4) возвращает sumCount(3) + 4, т.е. 6 + 4 = 10
На этом этапе уже легче увидеть, что мы просто добавляем числа между 1 и значением, которое предоставил caller. На практике рекомендуется указывать комментарии возле рекурсивных функций, дабы облегчить жизнь не только себе, но, возможно, и другим людям, которые будут смотреть ваш код.
Рекурсивные алгоритмы
Числа Фибоначчи
Одним из наиболее известных математических рекурсивных алгоритмов является последовательность Фибоначчи. Последовательность Фибоначчи можно увидеть даже в природе: ветвление деревьев, спираль ракушек, плоды ананаса, разворачивающийся папоротник и т.д.
Спираль Фибоначчи выглядит следующим образом:
Каждое из чисел Фибоначчи — это длина горизонтальной стороны квадрата, в которой находится данное число. Математически числа Фибоначчи определяются следующим образом:
F(n) = 0, если n = 0
1, если n = 1
f(n-1) + f(n-2), если n > 1
Следовательно, довольно просто написать рекурсивную функцию для вычисления n-го числа Фибоначчи:
ЕГЭ информатика 16 задание разбор
16-е задание: «Вычисление рекуррентных выражений»
Уровень сложности — повышенный,
Требуется использование специализированного программного обеспечения — нет,
Максимальный балл — 1,
Примерное время выполнения — 9 минут.
Проверяемые элементы содержания: Вычисление рекуррентных выражений
«Для успешного выполнения этого задания следует аккуратно произвести трассировку
предложенной рекурсивной функции»
Типичные ошибки и рекомендации по их предотвращению:
«Крайне важно отслеживать правильность возврата выполнения программы в нужную точку для
каждого рекурсивного вызова»
Объяснение темы «Рекурсивные процедуры и функции»
Для начала, разберем некоторые определения.
var x,y:integer; procedure Sum(x,y:integer); begin //3. Выводим сумму двух запрошенных чисел write(x+y); end; begin // 1. запрашиваем два числа readln(x,y); // 2. передаем запрошенные числа в процедуру Sum(x,y) end.
Подробное описание работы с процедурами можно найти, перейдя по ссылке.
procedure row(n:integer); begin if n >=1 then begin write (n, ‘ ‘); row(n-1) end; end; begin row(10); end.
Для использования рекурсии, необходимо задать:
Подробное описание работы с рекурсивными процедурами и функциями в Паскале можно найти здесь.
Решение заданий 16 ЕГЭ по информатике
Плейлист видеоразборов задания на YouTube:
Задание демонстрационного варианта 2022 года ФИПИ
Решение по рекуррентной формуле
Чему равна сумма цифр значения F(18)?
✍ Решение:
def F( n ): if n == 1: return 1 elif (n >= 2): return F(n-1)+3*G(n-1) def G( n ): if n == 1: return 1 elif (n >= 2): return F(n-1)-2*G(n-1) res = F(18) s = 0 while res > 0: s += res%10 res = res // 10 print(s)
Результат: 46
Алгоритм вычисления значения функции F(n), где n – натуральное число, задан следующими соотношениями:
Чему равно значение функции F(5)? В ответе запишите только целое число.
✍ Решение:
def F( n ): if n == 1: return 1 elif (n > 1): return F(n-1)*(n+2) print (F(5))
✎ Решение методом с конца к началу:
Результат: 840
Алгоритм вычисления значения функции F(n), где n – натуральное число, задан следующими соотношениями:
Чему равно значение функции F(6)? В ответе запишите только целое число.
✍ Решение:
✎ Решение 2. Метод решения с конца к началу:
Результат: 99
Решение данного задания 16 также можно посмотреть в видеоуроке:
Алгоритм вычисления значений функций F(n) и G(n), где n – натуральное число, задан следующими соотношениями:
Чему равно значение величины F(5)/G(5)?
В ответе запишите только целое число.
Что вернет функция. Сколько символов «звездочка». Какова сумма чисел
16_9: ЕГЭ по информатике 2020 задание 1 (Самылкина Н.Н., Синицкая И.В., Соболева В.В., «Тематические тренировочные задания»):
Что вернет функция F, если ее вызвать с аргументом 6?
function f(a:word):longword; begin if a>0 then f := f(a-1)*a; else f:=1; end;
Ответ: 720
16_3: ЕГЭ по информатике 2017 задание 16 (11) ФИПИ вариант 2 (Крылов С.С., Чуркина Т.Е.):
Ниже записаны две рекурсивные функции (процедуры): F и G.
Сколько символов «звездочка» будет напечатано на экране при выполнении вызова F(18)?