I don't know if Jack Altiere is right when he says many programmers are afraid of recursion, but if it's true I expect they fall into two categories:
Jack's short introduction is probably good for those in category one, but if you want to ever consider yourself an experienced programmer you should be trying to get into category two. This article is intended to help you get to category two as quickly as possible.
Recursive functions are easy. The only thing that you have to do to write a recursive function is to have it call itself. That's pretty easy and you may think that there isn't much reason to use it, but it can be very powerful. It turns out that there are all sorts of things that we might want to do that are easier to think of in terms of recursion. We won't go into all of them here, but if you look through your code you'll probably find that you have plenty of examples of them.
We're going to study a mathematical function that can be handled in all sorts of interesting ways — integer power calculations. I'm going to use Javascript (even though it wouldn't normally be my first choice for this sort of testing) because more people are likely to be able to run the tests themselves if I do so.
power( v, n ) = vn
For those that have forgotten your basic maths notations this means that we take some number v and multiply it by itself n times. If we take v as 4 and n as 2 we get 16 because 4 × 4 = 16¹ [1We're only going to consider positive integers for both v and n here, but the equation does of course work for any numbers.].
We also need to be aware of a couple of special rules. Anything multiplied by itself no times is one. This seems counter-intuitive, but it makes all the maths work out. Another rule is that anything multiplied by itself once is just the same as itself. We can write these as:
power( v, 0 ) = 1
power( v, 1 ) = v
We should put these together to get a complete definition:
power( v, 0 ) = 1
power( v, 1 ) = v
power( v, n ) = vn
Let's take a look at an extended example whilst we remember that our notation means we are multiplying a number by itself² [2There is of course a shortcut that works for any positive values of n and not just integer ones, but this article is about recursion and not logarithms]:
n = 0 ≔ 1
n = 1 ≔ 4 = 4
n = 2 ≔ 4 × 4 = 16
n = 3 ≔ 4 × 4 × 4 = 64
n = 4 ≔ 4 × 4 × 4 × 4 = 256
n = 5 ≔ 4 × 4 × 4 × 4 × 4 = 1024
There is a rather striking pattern here which we can express quite simply in an improved program³ [3I'm calling it a program because if we were using a pure functional programming language then this would be the program. The syntax would change depending on which programming language we were using, but the structure of the program would be essentially the same]:
power( v, 0 ) = 1
power( v, 1 ) = v
power( v, n ) = v × power( v, n - 1 )
It is this last part of our definition that makes it recursive. We have defined power()
in terms of itself and that's all that a recursive function is — a function that calls itself. We can write this in Javascript pretty simply too:
function power_recurse( value, power ) { if ( power == 0 ) return 1; else if ( power == 1 ) return value; else return value * power_recurse( value, power - 1 ); }
The three parts to the if
statement correspond perfectly to the three parts of our definition of power()
. The first two are called base cases because they do not involve any extra call to power_recurse()
. The last is the recursive call because, well I'm sure you can work it out.
There's a couple of things that we should notice about this:
For every recursive function that you ever write these two items must always work together, although the interaction isn't always so obvious. If you want to see an example where finding the base case is hard take a quick look near the end of this article.
for
loopIf we re-examine the pattern we can see that there is another solution that we shouldn't overlook too. Here is the pattern again:
n = 0 ≔ 1
n = 1 ≔ 4 = 4
n = 2 ≔ 4 × 4 = 16
n = 3 ≔ 4 × 4 × 4 = 64
n = 4 ≔ 4 × 4 × 4 × 4 = 256
n = 5 ≔ 4 × 4 × 4 × 4 × 4 = 1024
We can see that we're just multiplying n times. Writing this in Javascript is a breeze:
function power_for( value, power ) { var result = 1; for ( var i = 1; i <= power; i++ ) result *= value; return result; }
The base cases here are a little more subtle to spot and when you go through how it works you will notice that the second base case is missing altogether. We don't need it and skipping it makes the function much easier to work with.
The other thing to notice is that whereas the recursive version was a single statement, this one uses three⁵ [5Of course it depends on how you count statements, but the point is that the recursive function contains one block, the if
statement. This version contains three different items: the variable declaration; the for
loop; and finally the return statement.].
To start with there doesn't seem to be much to choose between them. Functional purists would argue that the recursive function is more elegant, whilst many other programmers would probably argue that the for
loop more clearly expresses how the calculation is done.
It turns out though that there is a good reason to prefer the for
loop over the recursive function in Javascript. Below is a table of execution times (n is the power that we are requesting be calculated). I've had to repeat the calculations many thousands of times in order to be able to measure the time with any sort of accuracy⁶ [6I've also abridged the results. For a complete table see the end of the article.]. Even so there is a lot of noise in the data, but the pattern is still clear enough.
power_recurse | power_for | |
---|---|---|
0 | 235ms | 282ms |
1 | 203ms | 343ms |
2 | 515ms | 360ms |
3 | 547ms | 375ms |
10 | 2078ms | 485ms |
20 | 4375ms | 703ms |
30 | 6672ms | 875ms |
100 | 22875ms | 3407ms |
The first question is why did I choose to plot the times against n and not v? The answer is that n is the control variable. That is, the execution pattern of both functions depends on n and not v. Try working out how they would run with a few different values for v and n and you will see that it doesn't matter what v is, the number of times the recursive function calls itself or the number of times the for
is executed never changes. Both change dramatically with any change in n⁷ [7A full explanation is beyond the scope of this article, but will be addressed in another article about algorithmic complexity (which sounds more complex than it is).].
The second question is why are the times so different? To answer that we need to think about what the Javascript interpreter⁸ [8I'm going to call it an interpreter as that's what it logically is even if your platform features a just-in-time compiler for Javascript.] is doing.
For power_recurse()
, each time the function is called it must copy the two variables value
and power
. When we request a power
of zero then this function call overhead is about the same for both versions and because of the way we have structured the recursive function (with two base cases) then this is the same for a power
of one. But once we ask for a power
of two then there are twice as many function calls in the recursive version as compared to thefor
loop.
So, although both versions are doing essentially the same amount of work in the same way the extra work in the function call overhead makes the for
loop much faster⁹ [9I've ducked something to do with my results here. If you stop to think about the implementation of Javascript and the use of big floats you'll realise that most of the actual returns from the functions we've written are going to be NaN or some other floating point error value as they'll overflow. We could have used some large integer add-on for Javascript, but what we are interested here is the call graphs and memory usage of the different implementation and for that it doesn't really matter that they overflow — each implementation overflows at more or less the same place (differences down to rounding errors) and we check that for numbers that are in our normal range we get the same results.]. Now, interesting as this is there are two reasons for this result:
What this means is that although the recursive function incurs an overhead in the function calls and this takes longer this is not the reason why we prefer for
loops!
The memory that your computer program uses comes in two flavours and they behave quite differently (actually there are three if we include the memory that the program code sits in, but let's ignore that as it isn't important for this discussion).
The first, and the biggest, is the heap. This is where nearly all of the data that your programs use live. The stack on the other hand is small and is used for remembering where to return to after function calls, the function call parameters and the function's local variables. I'm sure you can see where this is leading.
The for
loop version is only ever going to use one function call no matter how big the power we ask for is. The recursive version on the other hand has to remember function return points and variables on each call. For a power of one hundred that's one hundred times as much memory! If the power is big enough we can get Javascript to run out of memory using a recursive function which the for
loop could never do.
And this is the reason why we normally prefer to write a loop rather than a recursive function when we can.
Even if the Javascript interpreter uses its heap for the Javascript stack the principle still holds — it just means that the power needed to blow the program up has to be bigger¹⁰ [10Although if the interpreter executed the recursive Javascript call recursively in it's own execution (which it almost certainly doesn't) then you could still exhaust its stack very quickly.]. On my machine power_recurse()
exhausts the stack somewhere between n = 1,000 and n = 10,000. These are not high numbers. Even if the stack frame for each function call was one kilobyte (and it's probably actually a few hundredths of that size) then this represents only between one and ten megabytes.
For programming languages like C, C++ and Java though the stack is normally tiny in comparison to the heap. My computer at the moment has a gigabyte of RAM and a further 1½ gigabytes of swap space. My C++ compiler though has a default stack size of only one megabyte (which is still a lot larger than I was expecting), but still represents over three orders of magnitude.
This memory usage overhead of recursive functions is the real reason why recursive functions are often replaced with loops wherever possible. There are two circumstances under which recursive functions have to be very carefully controlled:
Of course recursive functions aren't all doom and gloom and there are good reasons to use them anyway. In many circumstances recursive functions are the simplest way to express an algorithm and for many recursive functions we can see that we have to pay the extra memory overhead anyway.
What we've seen so far is a special type of recursion, one where only a single recursive call is made. The way that they are used is very almost tail recursion, and some compilers will be able to turn these functions into proper tail recursive forms in order to use a technique called tail call optimisation. This isn't the place to go into this in any more detail, but the outcome is that a good compiler is able to convert these into a for
loop.
There is however another type of algorithm that is very easy (almost trivial) to implement recursively but is extremely hard to write as a loop and even when we do manage it we find that the memory overhead is about the same.
I'm going to stick to the example we've been using so far in order to show the form of the recursive calls even though a bit of even more clever maths will enable us to turn the algorithm into a loop.
Let's look at our pattern of multiplication again.
n = 0 ≔ 1
n = 1 ≔ 4 = 4
n = 2 ≔ 4 × 4 = 16
n = 3 ≔ 4 × 4 × 4 = 64
n = 4 ≔ 4 × 4 × 4 × 4 = 256
n = 5 ≔ 4 × 4 × 4 × 4 × 4 = 1024
If you look carefully you can see that we can split each of these in two. Let's start at n = 2 and go from there:
n = 0 ≔ 1
n = 1 ≔ 4 = 4
n = 2 ≔ 4 × 4 = 16 ≔ ( n = 1 ≔ 4 = 4 ) × ( n = 1 ≔ 4 = 4 )
n = 3 ≔ 4 × 4 × 4 = 64 ≔ ( n = 1 ≔ 4 = 4 ) × ( n = 2 ≔ 4 × 4 = 16 )
n = 4 ≔ 4 × 4 × 4 × 4 = 256 ≔ ( n = 2 ≔ 4 × 4 = 16 ) × ( n = 2 ≔ 4 × 4 = 16 )
n = 5 ≔ 4 × 4 × 4 × 4 × 4 = 1024 ≔ ( n = 2 ≔ 4 × 4 = 16 ) × ( n = 3 ≔ 4 × 4 × 4 = 64 )
Each of the powers can be expressed as powers of about half half their size. Can we make use of this in our program in any way? It turns out that this is pretty easy to do with our functional version, especially if we first put in a slight twist:
n = 0 ≔ 1
n = 1 ≔ 4 = 4
n = 2 ≔ 4 × 4 = 16 ≔ ( n = 1 ≔ 4 = 4 ) × ( n = 1 ≔ 4 = 4 )
n = 3 ≔ 4 × 4 × 4 = 64 ≔ ( n = 1 ≔ 4 = 4 ) × ( n = 1 ≔ 4 = 4 ) × 4
n = 4 ≔ 4 × 4 × 4 × 4 = 256 ≔ ( n = 2 ≔ 4 × 4 = 16 ) × ( n = 2 ≔ 4 × 4 = 16 )
n = 5 ≔ 4 × 4 × 4 × 4 × 4 = 1024 ≔ ( n = 2 ≔ 4 × 4 = 16 ) × ( n = 2 ≔ 4 × 4 = 16 ) × 4
What we've done is to split it into symetric calls of half the power with odd versions taking an extra factor of × 4. Written functionally this gives us:
power( v, 0 ) = 1
power( v, 1 ) = v
if ( modulus( n, 2 ) = 0 ) power( v, n ) = power( v, n / 2 ) × power( v, n / 2 )
if ( modulus( n, 2 ) = 1 ) power( v, n ) = power( v, n / 2 ) × power( v, n / 2 ) × v
I'm assuming that there is a modulus function that performs the standard mathematical operation and that the division by two is an integer division so it throws away the fraction.
Can we implement this in Javascript? Actually it's pretty easy to do so recursively. Again this is the natural way to turn our insight into working software and we end up with this new version:
function power_recurse_quicker( value, power ) { if ( power == 0 ) return 1; else if ( power == 1 ) return value; else { var r = power_recurse_quicker( value, Math.floor( power / 2 ) ); if ( power %2 ) return value * r * r; else return r * r; } }
Let's see what this does. The two base conditions are identical to our earlier version so that's pretty easy. As in the functional re-write above the only change is in our implementation for larger powers. We do our recursive call to calculate the answer for half the power and then use this, maybe with an extra factor of our value (if the power is odd) to calculate the requested result.
Does it work out any faster? If we think about what it will do now then it will recurse a lot less. If we start at n = 64 it will go to n = 32, then n = 16, n = 8, n = 4, n = 2 and finally hit the base case at n = 1. The other recursive version would go from n = 64 to n = 63 then n = 62 etc. until it also hit the base case at n = 1. The for
loop would work up from n = 1 to n = 2 and then n = 3 etc. until it got to n = 64, but both of these would need to do 64 calculations to work out a sixty fourth power. This new version only does seven calls and six multiplications!
Let's look to see how that translates into the timings.
power_recurse | power_recurse_quicker | power_for | |
---|---|---|---|
0 | 235ms | 203ms | 282ms |
1 | 187ms | 203ms | 343ms |
2 | 469ms | 515ms | 360ms |
3 | 547ms | 641ms | 375ms |
10 | 2078ms | 1453ms | 485ms |
20 | 4375ms | 1562ms | 703ms |
30 | 6672ms | 1547ms | 875ms |
100 | 22875ms | 2297ms | 3407ms |
We can see that it is slow at the lower values (they're all pretty quick on the base cases). By the time we get to n = 10 though it is beating the speed of the old recursive version and by the time we get to n = 100 it is about ⅓ faster than the for
and a whopping ten times faster than the old recursive function¹¹ [11The fact that the for
loop can compete for such high values of n shows us just how high the function call overhead is in Javascript. It is doing more than ten times the number of calculations and is only slightly slower.].
I have a procedural version of this algorithm that I'm saving for another article. Have a think about how you might implement it. A special prize if you manage to come up with a non-recursive implementation of this algorithm that doesn't require any extra storage — it is not easy¹² [12I tried for several whole days to come up with something and didn't manage it. Despite first off wondering if it wasn't possible Jules' very clever algorithm in the forums does solve it.] (and using logarithms doesn't count — it's of this algorithm).
Recursion is a very powerful tool and it often enables us to spot algorithms that we wouldn't otherwise see. We should however be very careful of using it in production software, especially where the parameters for the recursion ultimately derive from user input, or any other non-trusted source.
And finally here is a non-obvious recursive program. Try to work out what it does and how it does it. There is a clue in the the function name.
function gcd( value1, value2 ) { if ( value2 == 0 ) return value1; else return gcd( value2, value1 % value2 ); }
The download file (see top of article) includes this as gcd.js
which will show you a table of results to help give you a clue.
power_recurse | power_recurse_quicker | power_for | power_builtin | |
---|---|---|---|---|
0 | 235ms | 203ms | 282ms | 281ms |
1 | 187ms | 203ms | 343ms | 500ms |
2 | 469ms | 515ms | 360ms | 391ms |
3 | 547ms | 641ms | 375ms | 484ms |
4 | 844ms | 750ms | 422ms | 484ms |
5 | 1031ms | 859ms | 406ms | 500ms |
6 | 1125ms | 1079ms | 469ms | 438ms |
7 | 1203ms | 906ms | 437ms | 437ms |
8 | 1562ms | 1437ms | 500ms | 485ms |
9 | 1594ms | 1485ms | 484ms | 500ms |
10 | 2078ms | 1453ms | 485ms | 468ms |
20 | 4375ms | 1562ms | 703ms | 485ms |
30 | 6672ms | 1547ms | 875ms | 484ms |
40 | 8375ms | 1860ms | 1219ms | 453ms |
50 | 10125ms | 1968ms | 1593ms | 516ms |
60 | 11906ms | 1985ms | 1750ms | 453ms |
70 | 15047ms | 2031ms | 2000ms | 500ms |
80 | 17172ms | 2109ms | 2704ms | 391ms |
90 | 19469ms | 2078ms | 3093ms | 453ms |
100 | 22875ms | 2297ms | 3407ms | 484ms |
200 | 2422ms | 6421ms | 531ms | |
300 | 3203ms | 10000ms | 344ms | |
400 | 3922ms | 14313ms | 344ms | |
500 | 4110ms | 18281ms | 312ms | |
600 | 4781ms | 23250ms | 313ms | |
700 | 4859ms | 27844ms | 406ms | |
800 | 4360ms | 32859ms | 391ms | |
900 | 4453ms | 34672ms | 531ms | |
1000 | 4453ms | 39421ms | 344ms | |
2000 | 5047ms | 359ms | ||
3000 | 4812ms | 453ms | ||
4000 | 5516ms | 375ms | ||
5000 | 5344ms | 344ms | ||
6000 | 5937ms | 594ms | ||
7000 | 5578ms | 484ms | ||
8000 | 5828ms | 391ms | ||
9000 | 5313ms | 328ms | ||
10000 | 5672ms | 422ms |
power_recurse()
gets too slow to bother capturing more data. In any case it blows the stack somewhere between n = 1,000 and n = 10,000.power_for()
is starting to get too slow to capture results after about n = 1,000.Math.pow()
for comparison. Notice that it doesn't get slower for large n. There are good reasons for this and they're called logarithms.