Рекурсивные алгоритмы очень тесно связаны с математической индукцией .Возможно, изучение одного из них поможет вам лучше понять другое.
При использовании рекурсии необходимо помнить два ключевых принципа:
- Базовый случай
- Индуктивный шаг
Индуктивный шаг часто является самым сложным, потому что он предполагает , что все, на что он опирается, уже вычислено правильно.Сделать этот прыжок веры может быть сложно (по крайней мере, мне потребовалось некоторое время, чтобы освоить его), но это только потому, что у нас есть предварительных условий для наших функций;эти предварительные условия (в этом случае n
является неотрицательным целым числом) должны быть указаны так, чтобы индуктивный шаг и базовый случай были всегда истинными .
Базовый случай такжеиногда сложно: скажем, вы знаете, что факториал N!
равен N * (N-1)!
, но как именно вы справляетесь с первым шагом на лестнице?(В данном случае это просто, определите 0! := 1
. Это явное определение предоставляет вам способ прекратить рекурсивное применение вашего индуктивного шага.)
Вы можете увидеть свой типСпецификация и шаблоны защиты в этой функции обеспечивают предварительные условия, которые гарантируют, что индуктивный шаг можно будет использовать снова и снова, пока он не достигнет базового случая, n == 0
.Если предварительные условия не могут быть выполнены, рекурсивное применение индуктивного шага не сможет достичь базового варианта, и ваши вычисления никогда не прекратятся.(Ну, это было бы, когда у него не хватало памяти.:)
Один сложный фактор, особенно с функциональными языками программирования, это очень сильное желание переписать все «простые» рекурсивные функции, как у вас здесь, с вариантами, которые используют Tail Calls или Tail Recursion.
Поскольку эта функция вызывает сама себя, а затем выполняет другую операцию с результатом, вы можете построить цепочку вызовов следующим образом:
fac 3 3 * fac 2
fac 2 2 * fac 1
fac 1 1 * fac 0
fac 0 1
fac 1 1
fac 2 2
fac 3 6
Этот глубокий стек вызовов занимает память;но компилятор, который замечает, что функция не меняет никакого состояния после выполнения рекурсивного вызова, может оптимизировать рекурсивные вызовы.Такие функции обычно передают аргумент аккумулятор .У такого же накопителя есть очень хороший пример: Хвостовая рекурсия в Haskell
factorial 1 c = c
factorial k c = factorial (k-1) (c*k)
Это очень сложное изменение :) означает, что предыдущая цепочка вызовов превращается в следующее:
fac 3 1 fac 2 3
fac 2 3 fac 1 6
fac 1 6 6
(Вложенность предназначена только для показа; система времени выполнения фактически не будет хранить сведения о выполнении в стеке.)
Это выполняется в постоянной памяти независимо от значения n
и, таким образом, эта оптимизация может преобразовать «невозможные» алгоритмы в «возможные» алгоритмы.Вы увидите, что этот вид техники широко используется в функциональном программировании, так же, как вы часто видели бы char *
в программировании на С или yield
часто в программировании на Ruby.