Факториал через рекурсию c

Факториал через рекурсию c

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

Рекурсия

Важной особенностью языка JavaScript является то, что функция может вызывать не только другие функции, но и сама себя. Такие функции называются рекурсивными (recursive function). Применение рекурсивных функций, во многих случаях, позволяет писать компактный код вместо сложных вложенных циклов.

Расчет факториалов

В качестве одного из примеров можно привести расчет факториалов. Обозначается факториал восклицательным знаком "!" и вычисляется следующим образом:

Так, в соответствии с этим определением, мы имеем 5! = 5*4*3*2*1, т.е. 120 . Напомним также, что значение 0! определяется равным 1 , а факториалы отрицательных чисел не определены.

В следующем примере показано, как подсчитать факториалы итерационно, т. е. с помощью цикла while , в котором вычисляется результат:

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

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

Для того, чтобы окончательно разорвать замкнутый круг дополним рекурсивное определение N! = N * (N-1)! еще одним, которое будет служить для остановки процесса:

Попробуем теперь вычислить значение 5!, несколько раз применив правило N! = N * (N-1)! и однократно правило 1! = 1 :

Вместо вычисления значения с помощью цикла можно просто снова вызвать функцию factorial , передав ей очередное меньшее значение. Рекурсия останавливается, когда значение становится равным 1:

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

А теперь давайте разберём как же работает наша функция factorial() .

Когда factorial() вызывается с аргументом 1, функция возвращает 1. В противном случае она возвращает произведение num * factorial(num — 1) . Для вычисления этого значения factorial() вызывается с num — 1. Это происходит, пока num не станет равно 1.

Например, при передаче числа 5, у нас образуется следующая цепочка вызовов:

Ничего не умножается, пока мы спускаемся к базовому случаю factorial(1) . Затем мы начинаем подниматься обратно, по одному шагу.

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

Общее количество вложенных вызовов называют глубиной рекурсии. В случае с factorial() , всего будет num вызовов. Максимальная глубина рекурсии в браузерах ограничена 10 000 рекурсивных вызовов.

Читайте также:  Кодирование текста в двоичный код

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

Определение чисел Фибоначчи

Рассмотрим другой пример – определение чисел Фибоначчи.

Числа Фибоначчи – это ряд чисел, в котором каждое последующее число равно сумме двух предыдущих: первые два числа равны 1, затем 2(1+1), затем 3(1+2), 5(2+3) и так далее 1, 1, 2, 3, 5, 8, 13 и т.д.

Если напрямую написать решение этой задачи, переписав рекурсию в код, то получим очень простую реализацию, повторяющую математическое определение:

Вернёмся к функциям и изучим их более подробно.

Нашей первой темой будет рекурсия.

Если вы не новичок в программировании, то, возможно, уже знакомы с рекурсией и можете пропустить эту главу.

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

В процессе выполнения задачи в теле функции могут быть вызваны другие функции для выполнения подзадач. Частный случай подвызова – когда функция вызывает сама себя. Это как раз и называется рекурсией.

Два способа мышления

В качестве первого примера напишем функцию pow(x, n) , которая возводит x в натуральную степень n . Иначе говоря, умножает x на само себя n раз.

Рассмотрим два способа её реализации.

Итеративный способ: цикл for :

Рекурсивный способ: упрощение задачи и вызов функцией самой себя:

Обратите внимание, что рекурсивный вариант отличается принципиально.

Когда функция pow(x, n) вызывается, исполнение делится на две ветви:

  1. Если n == 1 , тогда всё просто. Эта ветвь называется базой рекурсии, потому что сразу же приводит к очевидному результату: pow(x, 1) равно x .
  2. Мы можем представить pow(x, n) в виде: x * pow(x, n — 1) . Что в математике записывается как: x n = x * x n-1 . Эта ветвь – шаг рекурсии: мы сводим задачу к более простому действию (умножение на x ) и более простой аналогичной задаче ( pow с меньшим n ). Последующие шаги упрощают задачу всё больше и больше, пока n не достигает 1 .

Говорят, что функция pow рекурсивно вызывает саму себя до n == 1 .

Например, рекурсивный вариант вычисления pow(2, 4) состоит из шагов:

  1. pow(2, 4) = 2 * pow(2, 3)
  2. pow(2, 3) = 2 * pow(2, 2)
  3. pow(2, 2) = 2 * pow(2, 1)
  4. pow(2, 1) = 2

Итак, рекурсию используют, когда вычисление функции можно свести к её более простому вызову, а его – к ещё более простому и так далее, пока значение не станет очевидно.

Рекурсивное решение задачи обычно короче, чем итеративное.

Используя условный оператор ? вместо if , мы можем переписать pow(x, n) , делая код функции более лаконичным, но всё ещё легко читаемым:

Общее количество вложенных вызовов (включая первый) называют глубиной рекурсии. В нашем случае она будет равна ровно n .

Максимальная глубина рекурсии ограничена движком JavaScript. Точно можно рассчитывать на 10000 вложенных вызовов, некоторые интерпретаторы допускают и больше, но для большинства из них 100000 вызовов – за пределами возможностей. Существуют автоматические оптимизации, помогающие избежать переполнения стека вызовов («оптимизация хвостовой рекурсии»), но они ещё не поддерживаются везде и работают только для простых случаев.

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

Контекст выполнения, стек

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

Читайте также:  Как перемножить два вектора

Информация о процессе выполнения запущенной функции хранится в её контексте выполнения (execution context).

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

Один вызов функции имеет ровно один контекст выполнения, связанный с ним.

Когда функция производит вложенный вызов, происходит следующее:

  • Выполнение текущей функции приостанавливается.
  • Контекст выполнения, связанный с ней, запоминается в специальной структуре данных – стеке контекстов выполнения.
  • Выполняются вложенные вызовы, для каждого из которых создаётся свой контекст выполнения.
  • После их завершения старый контекст достаётся из стека, и выполнение внешней функции возобновляется с того места, где она была остановлена.

Разберёмся с контекстами более подробно на примере вызова функции pow(2, 3) .

pow(2, 3)

В начале вызова pow(2, 3) контекст выполнения будет хранить переменные: x = 2, n = 3 , выполнение находится на первой строке функции.

Можно схематически изобразить это так:

Это только начало выполнения функции. Условие n == 1 ложно, поэтому выполнение идёт во вторую ветку if :

Значения переменных те же самые, но выполнение функции перешло к другой строке, актуальный контекст:

Чтобы вычислить выражение x * pow(x, n — 1) , требуется произвести запуск pow с новыми аргументами pow(2, 2) .

pow(2, 2)

Для выполнения вложенного вызова JavaScript запоминает текущий контекст выполнения в стеке контекстов выполнения.

Здесь мы вызываем ту же функцию pow , однако это абсолютно неважно. Для любых функций процесс одинаков:

  1. Текущий контекст «запоминается» на вершине стека.
  2. Создаётся новый контекст для вложенного вызова.
  3. Когда выполнение вложенного вызова заканчивается – контекст предыдущего вызова восстанавливается, и выполнение соответствующей функции продолжается.

Вид контекста в начале выполнения вложенного вызова pow(2, 2) :

Новый контекст выполнения находится на вершине стека (и выделен жирным), а предыдущие запомненные контексты – под ним.

Когда выполнение подвызова закончится, можно будет легко вернуться назад, потому что контекст сохраняет как переменные, так и точное место кода, в котором он остановился. Слово «строка» на рисунках условно, на самом деле запоминается более точное место в цепочке команд.

pow(2, 1)

Процесс повторяется: производится новый вызов в строке 5 , теперь с аргументами x=2 , n=1 .

Создаётся новый контекст выполнения, предыдущий контекст добавляется в стек:

Теперь в стеке два старых контекста и один текущий для pow(2, 1) .

Выход

При выполнении pow(2, 1) , в отличие от предыдущих запусков, условие n == 1 истинно, поэтому выполняется первая ветка условия if :

Вложенных вызовов больше нет, поэтому функция завершается, возвращая 2 .

Когда функция заканчивается, контекст её выполнения больше не нужен, поэтому он удаляется из памяти, а из стека восстанавливается предыдущий:

Возобновляется обработка вызова pow(2, 2) . Имея результат pow(2, 1) , он может закончить свою работу x * pow(x, n — 1) , вернув 4 .

Восстанавливается контекст предыдущего вызова:

Самый внешний вызов заканчивает свою работу, его результат: pow(2, 3) = 8 .

Глубина рекурсии в данном случае составила 3.

Как видно из иллюстраций выше, глубина рекурсии равна максимальному числу контекстов, одновременно хранимых в стеке.

Обратим внимание на требования к памяти. Рекурсия приводит к хранению всех данных для неоконченных внешних вызовов в стеке, и в данном случае это приводит к тому, что возведение в степень n хранит в памяти n различных контекстов.

Реализация возведения в степень через цикл гораздо более экономна:

Итеративный вариант функции pow использует один контекст, в котором будут последовательно меняться значения i и result . При этом объём затрачиваемой памяти небольшой, фиксированный и не зависит от n .

Читайте также:  Ноутбуки в пределах 30000

Любая рекурсия может быть переделана в цикл. Как правило, вариант с циклом будет эффективнее.

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

Часто код с использованием рекурсии более короткий, лёгкий для понимания и поддержки. Оптимизация требуется не везде, как правило, нам важен хороший код, поэтому она и используется.

Рекурсивные обходы

Другим отличным применением рекурсии является рекурсивный обход.

Представьте, у нас есть компания. Структура персонала может быть представлена как объект:

Другими словами, в компании есть отделы.

Отдел может состоять из массива работников. Например, в отделе sales работают 2 сотрудника: Джон и Алиса.

Или отдел может быть разделён на подотделы, например, отдел development состоит из подотделов: sites и internals . В каждом подотделе есть свой персонал.

Также возможно, что при росте подотдела он делится на подразделения (или команды).

Например, подотдел sites в будущем может быть разделён на команды siteA и siteB . И потенциально они могут быть разделены ещё. Этого нет на картинке, просто нужно иметь это в виду.

Теперь, допустим, нам нужна функция для получения суммы всех зарплат. Как мы можем это сделать?

Итеративный подход не прост, потому что структура довольно сложная. Первая идея заключается в том, чтобы сделать цикл for поверх объекта company с вложенным циклом над отделами 1-го уровня вложенности. Но затем нам нужно больше вложенных циклов для итераций над сотрудниками отделов второго уровня, таких как sites … А затем ещё один цикл по отделам 3-го уровня, которые могут появиться в будущем? Если мы поместим в код 3-4 вложенных цикла для обхода одного объекта, то это будет довольно некрасиво.

Давайте попробуем рекурсию.

Как мы видим, когда наша функция получает отдел для подсчёта суммы зарплат, есть два возможных случая:

  1. Либо это «простой» отдел с массивом – тогда мы сможем суммировать зарплаты в простом цикле.
  2. Или это объект с N подотделами – тогда мы можем сделать N рекурсивных вызовов, чтобы получить сумму для каждого из подотделов, и объединить результаты.

Случай (1), когда мы получили массив, является базой рекурсии, тривиальным случаем.

Случай (2), при получении объекта, является шагом рекурсии. Сложная задача разделяется на подзадачи для подотделов. Они могут, в свою очередь, снова разделиться на подотделы, но рано или поздно это разделение закончится, и решение сведётся к случаю (1).

Задача такова: ввожу число — программа должна посчитать её факториал. Хочется это сделать и рекурсивно и итеративно

Вот как я это понимаю итеративно. Но что то не работает.

2 ответа 2

Вам следует вынести рекурсивное и итеративное вычисление факториала в отдельные функции.

Соответствующие программы могут, например, выглядеть следующим образом.

Для рекурсивного вычисления

Для итеративного вычисления

Вы можете использовать эти программы как шаблон для написания собственной программы.

Вы должны иметь в виду, что даже размер типа unsigned long long int ограничен, а потому не в состоянии вмещать вычисляемый факториал для всех заданных пользователем значений.

Об ограничениях на верхние значения для факториалов для целочисленных типов в C++ я написал в сообщении по следующему адресу

Если вы хотите контролировать верхний предел вычисляемого значения, то вы можете, например, выбрасывать исключение. Для этого вам понадобится включить заголовок
и использовать стандартный класс std::numeric_limits , чтобы получить максимальное значение в том или ином целочисленном типе. Например,

Ссылка на основную публикацию
Удаление последнего элемента списка
Введение. Основные операции О дносвязный список – структура данных, в которой каждый элемент (узел) хранит информацию, а также ссылку на...
Телефон самсунг с хорошей камерой недорогой
Если вы ищете лучший телефон Samsung, тогда рейтинг поможет разобраться в их различиях. Посмотрите какой смартфон лучшие купить из всех...
Телефон перестал заряжаться быстрой зарядкой
Наверняка многие сталкивались с тем, что смартфон ни с того ни с сего перестаёт заряжаться. Другая распространённая беда — слишком...
Удаление дубликатов фотографий на русском бесплатно
Здравствуйте Уважаемый Друг. У каждого из нас на компьютере хранится большое количество различных фотографий изображений и тому подобных картинок. Парой...
Adblock detector