"Recursion" Introductory C Programming for ECE at University of Toronto
Watch on YouTubeVideo summary
Recursion is introduced as a fundamental concept in programming where a function calls itself to solve a problem by breaking it down into smaller, similar sub-problems. To implement recursion correctly, two essential components are required: a base case and a recursive step. The base case represents a simple scenario with a known solution that stops the function from calling itself indefinitely, preventing an infinite loop or stack overflow. The recursive step reduces the problem to a smaller version of itself, relying on the assumption that the function works correctly for these smaller inputs until it eventually reaches the base case. Without both elements, a recursive function would fail to terminate, effectively creating an endless cycle similar to reading a dictionary definition that only points back to itself.
The video illustrates these concepts using Fibonacci numbers and exponentiation as primary examples. In the Fibonacci sequence, calculating the nth number involves summing the two preceding numbers, which naturally leads to multiple recursive calls for each step. Similarly, computing exponents can be done recursively by multiplying the base by the result of the function called with an exponent reduced by one. While these recursive solutions are conceptually elegant and often easier to understand mathematically, they can be inefficient in practice because every function call consumes memory on the stack to store local variables like arguments and return addresses. If the recursion depth becomes too large, such as calculating a very high power or a deep Fibonacci sequence, the program will eventually run out of memory, resulting in a "stack overflow" error.
To address performance issues, the lecture discusses techniques like tail recursion and memoization. Tail recursion is a specific form where the recursive call is the last operation performed, allowing compilers to optimize the code into an efficient loop that uses constant memory instead of growing stack space. Another optimization strategy mentioned is memoization, which involves caching previously computed results so they do not need to be recalculated repeatedly, significantly speeding up algorithms like the Fibonacci function. Although modern compilers can often optimize tail-recursive functions automatically, understanding these mechanisms is crucial for writing efficient code and avoiding common pitfalls associated with deep recursion. Ultimately, while loops are often preferred for simple iterative tasks like calculating powers or factorials, mastering recursion is vital for solving complex problems where a recursive approach offers a more natural and readable solution.
Read the full video transcript
welcome back to 105 I'm re-recording
this uh because I forgot to hit the
stream button so let's get into it so
this is probably one of the toughest
things to get your mind around being Rec
recursion so might have heard of
recursion before or in math a recurrence
relation but if you haven't a recursive
function is just a function that calls
itself so in order to actually solve a
problem recursively we need two things
so we need a base case so that is a
simple solution that we know and then we
need a recursive step and the idea
behind this is we don't have to solve
the whole problem we can assume the
solution already works we just have to
put the solution in terms of a smaller
version of itself and then through the
magic of computers well we can have that
try and make the problem smaller and
smaller and smaller and smaller until it
gets to our base case and then it will
go ahead and solve our problem for us so
if you went and looked in the dictionary
for the definition of recursion you
might see something hilarious like this
where the definition of recursion just
says see
recursion and this would not solve any
problem because well if you were going
to read it you see what's the definition
of recursion well it's recursion what's
the definition of recursion it's
recursion so so this kind of looks like
an infinite Loop and well we can get
like the same types of things happen
whenever we do not have our two rules
coming into effect where you know our
computer would just essentially run
forever because it's basically an
infinite loop it's just more of a
roundabout way to get into an infinite
Loop so you may have seen Fibonacci
numbers before they're also an example
of recursion
so they are in math a recurrence
relation which
means that they Define it like this they
say that the value of the zeroth
Fibonacci number is simply just a zero
then the value of the first Fibonacci
number is just one and then they give
you the recurrent relation so to
calculate the nth Fibonacci number it is
the nus oneth Fibonacci number plus the
nus
2 Fibonacci number as long as n is
greater than zero so we're just assuming
positive integers so we could also write
a function to compute F ofn and what
that would look like is well if We're
translating into C we know that we are
Computing the nth Fibonacci number and
the type of n would be an integer and
well we could go ahead
and see that the condition to use the
recurrence relation is if n is greater
than one so we kind of know we probably
want to do some type of if statement so
for recursive problems
typically once you're starting out to
it's easier to divide it into the base
case and the recursive step so in this
example to solve the calculating the nth
Fibonacci number our base case
could be assuming that n is going to be
zero or above not no negative values our
base case would be if n is less than two
then just return n so that covers
actually both of our base cases so if n
is equal to one well one is less than
two and then based off the definition
should be one in this case we just
return n which is equal to one and also
if we're calculating the zeroth
Fibonacci number which should have the
value zero well 0 is also less than two
then we could just return n which is
currently zero so we would get zero so
if you wanted to if you could write two
different if statements so you could
have two base cases so if n is equal to
zero return zero or if n is equal or
else if n is equal to one then return
one and remember you should use double
equals not in equals because you might
get into trouble for that and then the
other part of the solution here is the
recursive step so if it's not the base
case then it's the recursive step so we
could just do else and then we just
write out the recurrence relation and
just kind of translate it to C so we
return just FIB of
nus1 n plus FIB of n minus 2 and we will
revisit this problem a bit later in the
lecture too so let's start with a bit of
an easier problem first so we could also
calculate exponents using recursion as
well so assuming that we have n that
can't be negative if we want to compute
B to the power of n well that's the same
thing as B * B * B * B just n times but
if we want to think about this
recursively the two things we need are
the base case and also the recursive
step which is solving the problem in
terms of a smaller version of
itself so well our base case could be
there's a few possibilities here but one
of our base cases could be if the
exponent or n is equal to zero then we
know that the result is just one so that
could be our base case so if we see n is
equal to zero we should just return one
we could also have a base case of when n
is equal to 1
we could just return B but in general
you kind of want to have your base case
follow like the worst case scenario so
if we assume n is going to be zero or
positive the worst case scenario is that
well in terms of our correctness would
be that n is greater or equal to zero so
our base case could be B to the 0 equals
1 and then in terms of our recursive
step so to calculate B to the power of n
in terms of B to the power of something
well our recursive step could be that b
to the power of n is equal to B * b ^ of
nus1 and nus1 well that's an instance of
the same problem it's just a smaller
problem so we just take our number B and
then multiply it by B to the
nus1 so if we we were to write that out
in C looks like the same thing as a
Fibonacci number so we would create a
function called
exponent which would take two arguments
now so we have a b and an N which are
integers and then we could write out an
if statement for our base case so in
this case our base case is n is equal to
zero we could just return the value one
and then for our recursive step well we
just return B * exponent B so we don't
change the base at all and then n minus
one so we can think of another way to
calculate the exponent just using
something like a wall Loop or for Loop
but in some scenarios we're just getting
practice writing
recursive uh recursive Solutions because
some problems lend themselves to this
much more than others so we can go ahead
and see
that here is my program if I implement
it I went ahead and
also used the version of C or of the
version of main that takes some
arguments that we type in just because
I'm saving myself from writing scan F
and then having to enter hit enter two
times to use this program so here I just
check if there are three arguments so
the first is going to be the name of the
program and then I'm assuming the next
are two numbers so I'll just assume that
the user is nice I will use our
a2i function that we discussed in the
strings function lecture that will go
ahead and give convert that string to an
integer and then I will just call
exponent with
BN that I get from the first argument
and the second argument and just display
the results now if I go ahead and run
this I might see that you know 2 to ^ 4
well that should give me what I expect
and give me the result 16 and now if I
do 2 the^ 5 I get 32 and this seems to
actually work as I
expect so now let's evaluate what
actually happens when we are calling to
to the exponent of four and this is
where my recording catches up so let's
switch to that
there we go oops so if we hit or sorry
if we do two to the 4 well we would call
exponent 24 that would go into our
recursive case so it would go into that
lse statement and it would do return
well 2 *
exponent 2 to the power of three so
before that function returns that called
2 to 4 it has to actually compute 2 to
the 3 so we would do another function
call so this is another function call so
I put it here on the stack to indicate
that the other one is still active it's
waiting for this one to complete so
right here in the gray text that is what
we're trying to compute and every box is
a new function call that would have its
own local variables in this case it
would be B is 2 and N is 3 so to
evaluate exponent 2 to the 3 that would
go into the recursive case so we would
get 2 * exponent 2 to 2 okay well now we
have to figure out what exponent 2 the
two is that is another function call so
we call exponent 2 to the two then that
gets into the recursive case again so we
get 2 * exponent 2 to the one then it
gets kind of boring so then we do have
another function call with exponent or 2
to the 1 and then well that is 2 *
exponent 2 to the 1 so we would call
that and then mercifully this is our
base case so in this case n is equal to
zero so it would just get a one so it
would return one so the function that
called it gets a copy of one
and this whole thing kind of unravels
itself so then it would return and then
in our 2 to the 1 function we would
calculate 2 * 1 which is just two and
then that is the result for 2 to the
power of 1 so that would get returned
for this function
call then we have 2 * 2 so that would be
four so that's 2 ^ of two and then it
would get returned here and then all
kind of unravels right so now
here that four is returned it gets a
copy of it so then we would do 2 * 4
which is equal to 8 which is thankfully
to the^ 3 and then here well we would
return 8 so then we would get 2 * 8 and
then we'd get finally our final answer
16 so lots of steps it would be in that
case what five function calls but we got
the answer
in this case a for Loop would have
probably been less confusing but is
there any questions about how this
worked because it's going to get weird
once we get Fibonacci
going so we're all all okay until it
gets slightly more
strange okay so let's go ahead and run
it make sure it works so here is that
exponent code so here I just have my
base case and my recursive step step
oops and then if I go ahead and run it I
could do to the power 4 something like
that I get 16 seems to work to the^ 5
32
but if we implemented this with a loop I
could probably do like one to the power
of I don't know some large
number and that would be fine it would
just might take some time to execute
right well let's see what happens here
so if I run
that I get a segmentation
fall
crap well I didn't even use malachow in
the blue hell do I have a segmentation
fault so one of your steps might be okay
well I'll use valren and then I'll see
says okay I have no memory leaks I
didn't allocate anything so what
happened so you try and scroll up and
then you get a bunch of random text
that's probably not so readable the only
thing that you have any hope of reading
is this thing called a stack Overflow so
we know that kind of what a stack is
it's where all the local variables are
and in this case a stack Overflow means
basically it ran out of memory so this
is a very common thing you can encounter
once you do recursive function functions
because if there's too much recursion we
essentially just run out of memory so I
did what to the power of like a million
or something like that so that would
mean that you know a million called
9 called
999998 called that called that called
that called that and well every function
that is active has a b and an N at least
so it has two integers so I essentially
just ran out of memory because every
function call needs its own variables so
that is what a stack Overflow means
basically just I ran out of
memory
so again like I said every time we call
a function that function gets its own
copy of the variables in C they store
local variables on the stack so if
there's many active functions at once so
there I did to the power of a lot so if
I was to draw that on on the slide I
would run out of room because it would
just call itself over and over and over
again until it actually ran out of
memory so running out of memory for
local variables called a stack Overflow
that is the name of the helpful website
that you can get questions and answer to
for programming questions um so they
named it that because well this is a
common enough error that is really hard
to go go it means you have screwed
something up because usually people get
to recursion and then things start
screwing up and then most programmers
see stack Overflow a lot when they are
starting out so common cause for stack
Overflow more likely than not is
infinite recursion so it's similar to an
infinite Loop except in an infinite Loop
just takes a lot of time it doesn't
actually need more memory each time you
go through the loop for recursive
functions it needs more meory memory
essentially every iteration of the loop
if we were thinking about loops so we
can actually likely run out of memory
before we run out of
time
so this is far beyond the scope of the
course so don't worry about if this part
doesn't make any sense but there are
like certain forms of recursion that are
more that can be optimized more easily
so we can rewrite some functions to use
what's called tail recursion so it's a
special recursive function that just has
a single recursive call in the return
statement you do not need to practice
actually rewriting this this is mostly
for performance so if we modify exponent
well we could create an exponent that
takes three
variables so before we just returned a
number well like in our recursive case
we did like two times the recursive
function so if we want to be tail
recursive we can't do that so we create
another variable here that's local to
the function called accumulator that
just keeps track of the current value so
in our base case instead of returning
one we just return whatever the final
value we have is so it would do
like uh 2 to
the 2 * 2 * 2 * 2 if we did 2 The Power
four then eventually it would be 16 and
then we just return 16 at the end and
this is our recursive case so we just do
the calculation with the accumulator so
we just multiply B by it and otherwise
it looks the same as before so if we
want to keep exponent looking the same
well we have the base and the n and we
can just return exponent written in this
tail recursive manner by giving the
accumulator one and then giving B at the
end and the only reason why I show this
to you is because compilers can optimize
this function so if it's recursive a lot
of people don't like writing recursive
functions because well it wastes a lot
of memory and you can get into the stack
Overflow issue where if you just wrote a
while loop or for Loop or something like
that that would work better and you
wouldn't have that memory issue so it's
actually guaranteed that compilers and
you'll learn this in like fourth year
whenever you get to compilers but it's
guaranteed that compilers will optimize
this away so it will optimize away the
recursive fall call and just convert
that whole function into essentially a
while loop so it would convert it to
this for you so just start off in x
equals accumulator so in our case we set
it equal to one and then it would just
go while n is not equal to zero and just
do the recursive step over and over
again
so now our codee doesn't have a stack
Overflow doesn't have that issue because
we're not actually doing recursive
function calls we let the compiler
optimize it away for us and in this
version maybe you can argue that the
recursive one was more readable so you'd
rather have it than this but for
calculating an exponent probably just a
while loop is going to be much much much
easier to do but it's good to think of
things recursively because well some
solutions lend itself better to
recursion than
others so let's rewind a bit and see
what happens when we call FIB of four so
this one is a bit different than the
exponent where in our recursive step we
return FIB n minus 2 plus FIB or FIB n
minus 1 plus FIB n minus 2 so we do two
function calls here so if we actually
want to evaluate something like FIB of
four turns out it's going to be a lot of
work to understand how your computer
actually executes this code so gets a
bit ugly so if we evaluate FIB of
four well we call FIB of four and again
I'll just write in the gray here what
the function is supposed to be Computing
so it's supposed to be Computing the
fourth Fibonacci number so in order to
compute FIB of four well that's the
recursive step right it should return
FIB 3 plus FIB
2 so we have to do these function calls
so c will go ahead and it does function
calls in left to right order don't have
to know that for the course but in order
to figure out how this actually works on
your computer it's important to know
what happens first so we will do this
function call for FIB of three so we'll
do that function call it'll get its own
copy of N and we do FIB of three well
that goes into the recursive step so it
would return FIB of 2 + 1
right then okay now we need to call FIB
of two okay so we call fib FIB of two
FIB of two is equal to FIB of 1 plus FIB
of 0 mercifully when we call FIB of one
that is thankfully our base case right
so it would immediately return one so
that whole thing would evaluate to one
and then it would return and give us the
value one back for in this case FIB of
one so it returns and now oh no well we
still have to find out what FIB of0 is
right so now we have to figure out what
FIB of0 is so that is another function
call to figure out what FIB of Zer is
that mercifully is another base case so
that just gets the value zero so it
returns and then we can finally figure
out what the second Fibonacci number is
so it's uh 1 + 0 so it would figure out
that that is one okay great so that was
figuring out what FIB of two was so that
would return the value
one okay well now we have to do the
other half of it now we have to do FIB
of
one okay well that's another function
call so we call FIB of one mercifully
that is another base case so that just
returns
one and then well that returns one so
then we can figure out what the third
Fibonacci number is so it's just 1 + 1
so that's
two so now that returns right so that is
fib of three so that
returns oh God but now we have to figure
out what the second Fibonacci number is
so that is another function call so this
whole thing kind of goes again so we
have FIB of two okay well that's a
function call oh that's our recursive
case so we would return FIB of one plus
FIB of
zero okay so then FIB of one that's
another function call mercifully that's
our base case so that's just a
one so then that would return okay yay
so now we have to figure out what FIB of
zero is again so FIB of zero okay that's
another function call that goes to the
base case verely we know it's a
zero then it would
return and now FIB of two is done so we
get one then that returns now we can
finally figure out the fourth Fibonacci
number which is 2 + 1 which is
three
yay wasn't that
fun
so any questions about how that works
other than it seems like it takes a long
time to figure it
out
so let's see so here is that
implementation and just as a review for
the last one instead of just asking and
doing scan F I'm just using that other
version of main that takes another
argument that I can type to it just
because I don't feel like hitting enter
two times so I check that there's two
argum ments and then use aot to convert
the first one to an INT and I assume I'm
being nice to it I'm just going to print
the result of calling Fibonacci on
whatever that number is so if I call FIB
of four it was a lot of work for us to
figure out all the function calls and
you know how they work and who calls
what and what values and when do they
stop but computers are fast so it just
gets three quite readily and works so if
we did computed 10 we get 55 turns out
it grows pretty fast 20 we get 6,000
something like that seems to be pretty
fast computers are quick but if we get
40 you might have noticed that took a
little bit of
time maybe
44 44 took a bit of time it was a bit
slow and also for this one we could just
write it in a for Loop to and it would
be way faster and way easier to actually
compute but again we're just kind of
practicing uh practicing recursion even
though doesn't really make sense this
would be a lot easier to just do in a
for
Loop so any questions so far about
evaluating that yeah is there any way to
know how much slower will get depending
on how
many yeah so the question is do we know
how much slower it will be depending on
how many steps it will get so you can
kind of guess it's going to get
like in this case almost two times
slower every time because well if I have
to compute 44 that means it has to
compute
403 and then
42 and then if I have to compute the
next one well then I have to compute 44
which is going to take as much as what I
ran before plus a 43 so not quite two
times slower every step but gets up
there so if I do like
45 probably pretty
slow yeah oh
crap so oops so if I do 44 One
Mississippi two Mississippi takes a bit
of time if I do 45 One Mississippi two
Mississippi three Mississippi four
Mississippi
okay so took about twice as long because
essentially has to calculate everything
it had to do before and then do it all
again for the next number and then you
might imagine this might be like
exponential so I have to compute one
side and then the other side and then I
have to do it again and again and again
and it's kind of a pain so that leads us
to our next thing which is we can
signif whoops wrong thing leads us to
the next point of again so this part
again is beyond the scope of the course
for this course just writing recursive
functions is good enough for us we'll
get some more practice as it goes on but
just so you know you can significantly
speed this up without
actually changing it from being
recursive with something called
memorization which is just a fancy word
for caching and CA caching is just a
fancy word for just saving values that
you don't so you don't have to recompute
them so even with FIB of calculating FIB
of four well we had to calculate like
FIB of two a lot of times and we had to
calculate FIB of three once and FIB of
two a bunch of times and then a lot of
well we didn't have to calculate the
value for FIB of one or FIB of zero but
we could change our PR program a little
bit so we don't have to go ahead and
recompute them over and over again so
one optimization we could do that you
could probably implement right now but
you don't have to for this course is
just I could just remember some old
value in an array and then if I've
already calculated that value before I
can reuse it again instead of
recomputing it
so this one took
I almost counted like five Mississippi
it was really really really really
really really slow so I wrote another
version of it that
just uh that just saves the value that
is computed already so it just reuses it
over and over again instead of doing the
whole recursive nonsense over and over
again and if I run that oops that is the
same
function so if I run my optimized
version boom look how fast that
was so there are some techniques you
might employ that some problems will
have to be recursive and they might be
slow and this is one technique to speed
it up again just something to think
about don't have to do that in the
context of this course at all but it
will come up you know probably in your
next programming
course
so recursive functions
are basically just another tool some
problems are easier to think about
recursively especially if you're like
more of a matthy person that likes
proofs and all that stuff was never me
so it took me a bit to figure out this
recursive thing but some problems
believe it or not are actually easier to
solve
recursively so it's good to get some
practice trying to get yourself into
that
mindset so typically recursive functions
are going to take more space to execute
tail recursive functions can be
optimized again don't have to know that
for this course but in your career that
might be a good thing to know and try to
write for so it's important to practice
so you can go ahead identify these types
of problems and be able to solve them
and the main two things are given a
problem you probably want to identify a
base case so something you know a value
for given one of the inputs and then you
also want to create the recursive step
so you want to solve the problem in
terms of
itself sorry you want to solve the
problem in terms of a smaller version of
itself so typically it'll be you do one
thing like you just take one step and
solve that so let's think about that
that for more practice can we write a
recursive function to calculate
factorials so sit think about it for a
few minutes and then we will try and
come up with the code ourselves together
so we're trying to calculate factorials
and like for an example well
the like four factorial would be 4 * 3 *
2 *
1
so take a minute think about how you
would solve that and write that as a
recursive function so these are the
types of things that would be on exams
and stuff like that like for this
course most of the problems you'll see
you can probably solve with like a wall
Loop or something like that because well
you have to be able to write them during
an exam and recursive stuff takes
practice so generally you'll probably
get some question on the file will be
like solve this problem with a recursive
function all right I'll give you a few
minutes for that and then we'll try and
think about what the steps would
be
all
right
yeah
I can't hear you
sorry okay so I'll take you first so you
you you will start half of it so the
first thing we need is a base case that
has been answered up there so our base
case would probably be
well if we're Computing
like n less than two so if we're
Computing the factorial we assume no
negative numbers if we're Computing one
factorial or zero factorial well they're
defined as just being the value one so
that is our base case so I'll get
someone else for the recursive step so
that is one thing we need so the other
thing we need is our recursive step so
anyone with the idea of what we should
do in the recursive step so how do we
solve factorial
in terms of itself
yeah yeah so my recursive step I could
write in an else here or return doesn't
really matter so in terms of solving in
terms of itself well let's assume the
number is four so in order to calculate
four factorial well that's just the same
as doing 4 * 3 factorial so if we wrote
that in terms of C function calls our
recursive step would be to return n *
nus1 factorial which would just be
factorial of n minus
one everyone agree with
that so turns out this one might be
easier to think about than doing all the
for Loop and everything like that so if
we do I don't know 4 factorial 24 10
factorial some large number seems to
work
cool that was faster than I expected any
questions about
that yeah
oh so the questions can I do a recursive
macro
yeah I'm not sure if the c pro
pre-processor lets you define recursive
macros I'm actually not sure I don't
think so it's fairly straightforward and
simple and that would probably
complicate it so I don't think you can
but can do recursive functions no
problemo
so any questions at all about
factorial all right wow I outdid myself
so here's the solution for you so same
thing we developed so important thing so
we will get a much much much much more
complicated example in the next lecture
I even have fun toy props that I can
bring in but for this one just remember
a recursive function just a function
that calls itself in order to think
about the problem well your solution
needs two things so you need a base case
so which is basically a simple solution
you know so something you know how to do
that you don't have to think very hard
about and then the other one is your
recursive step that you'll have to
develop which just solves the problem in
terms of a smaller version of itself so
the idea here is since it's a smaller
version of itself you're making progress
each step you take eventually you will
hit the base case and then you can just
let your computer solve it for you
so yeah we're really 13 minutes ahead
all right cool I like it all right so
I'll we'll just stick around for the
remaining 13 minutes but just remember
pulling for you we're all in this
together