This is a tough question to answer in a fully authoritative way -- I think different people have different ways of thinking about recursion. My way of thinking about recursion is to force myself to think in a very disciplined, abstract way about what a function is. A function is just a mapping. That's simple in principle, but easy to forget in practice, especially if you're used to thinking in imperative terms -- that is, to thinking about programs as sets of instructions.
Forget about instructions for a moment, and think about the factorial function in its most abstract form:
X Y
--------------------
0 ------> 1
1 ------> 1
2 ------> 2
3 ------> 6
4 ------> 24
5 ------> 120
6 ------> 720
...
Don't worry for now about how to calculate this. Just think about the abstract mapping. Now let's think about how to make a recursive version of this function. What do we need? Well, we need a function that creates a different mapping -- not a mapping from [1, 2, 3, ...] to factorials, but a mapping from one value of Y to the next. In other words (using lower-case now):
x y
--------------------
1 ------> 1
1 ------> 2
2 ------> 6
6 ------> 24
24 ------> 120
120 ------> 720
720 ------> 5040
...
Now let's think about how to calculate this. Immediately a problem appears: 1 maps to 1 the first time and to 2 the second. So we know we're going to have to write a special case to differentiate those two. But for the others, this is pretty simple, right? Just multiply x by its position in the list. So this means for all those parts of the mapping, we need to know just two things: x, and its position in the list:
def factorial_recurrence(x, position):
return x * position
Note that this function now has two arguments, so it's actually a slightly different function than the above:
x, p y
------------------------
1 0 ------> 1
1 1 ------> 2
2 2 ------> 6
6 3 ------> 24
24 4 ------> 120
120 5 ------> 720
720 6 ------> 5040
This shows quite clearly how we can differentiate between the two mappings from 1. Now we just have to come up with a way to get the position information. It just so happens that position is the same as the value of X. So one simple way would be to use a loop. Here we take care of X == 0 by simply setting x to 1 and starting our loop at 1 instead of 0:
def factorial(X):
x = 1
for position in range(1, X + 1):
x = factorial_recurrence(x, position)
return x
Now notice that the value of x here is being passed into factorial_recurrence, and then the result is being saved as x.
So what's really happening here is that the output of the function is being passed back into the function. Here's the big reveal:
That's recursion!
This is, in a crucial sense, already a recursive algorithm. It's just that the representation is inside-out here, and the function also incorporates position information from outside the recursive process. To see what I mean, look at this:
def even_factorial(X):
x = 1
for position in range(2, X + 1, 2):
x = factorial_recurrence(factorial_recurrence(x, position - 1), position)
return x
This gives the same result as factorial for every even value of X. (It gives the result of X - 1 for odd values of X.) And we don't have to stop there. We can do the same thing for every third value of X (breaking out the nesting for clarity):
def third_factorial(X):
x = 1
for position in range(3, X + 1, 3):
x = factorial_recurrence(
factorial_recurrence(
factorial_recurrence(
x,
position - 2
),
position - 1
),
position
)
return x
Now do the same thing for every 4th, every 5th, and so on. If you continue this process, then for any given X, you'll eventually create a function that will return nothing but 1 until you pass X, and then when you pass X, you'll get the factorial of X.
At this point the trick of recursion is simply to realize that we can automate that process of turning the loop inside out by having factorial call itself. Every time factorial is called, it simply nests another factorial_recurrence call inside the last -- unless X is 0, in which case, it returns 1, terminating the sequence of nested calls.
def factorial(X):
if X == 0:
return 1
else:
return factorial_recurrence(factorial(X - 1), X)
So this is kind of a complicated way of thinking about recursion, but its value is that it shows, very clearly, the relationship between the abstraction of recursive functions and their concrete implementation in imperative code.