Skip to content

Commit

Permalink
Add recursion lesson
Browse files Browse the repository at this point in the history
  • Loading branch information
uellenberg committed Nov 29, 2023
1 parent eba39b4 commit ba50cab
Show file tree
Hide file tree
Showing 9 changed files with 430 additions and 2 deletions.
8 changes: 6 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"sections/40_making_websites",
"sections/50_backend_websites",
"sections/50_using_react",
"sections/60_continuing_cs",
"sections/60_typescript",
"sections/70_next_js"
],
Expand All @@ -35,12 +36,15 @@
"next": ["backend_websites", "using_react"]
},
"backend_websites": {
"next": ["typescript"],
"next": ["continuing_cs", "typescript"],
"previous": ["making_websites", "console_applications"],
"requireAll": true
},
"using_react": {
"next": ["typescript", "next_js"]
"next": ["continuing_cs", "typescript", "next_js"]
},
"continuing_cs": {
"next": []
},
"typescript": {
"next": ["next_js"]
Expand Down
178 changes: 178 additions & 0 deletions sections/60_continuing_cs/00_recursion_intro/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Recursion

Recursion is a way of structuring your code that can be incredibly useful, but only in specific circumstances.
Recursion is when a function calls itself. For computers, there's no difference between a recursive and a non-recursive
function, but it's important to understand the difference so that you can know when and how to use them.

There are a few ways to think about recursion, and it's a tricky topic, so we'll cover a couple of them.
We're going to take a look at different examples of recursion, and it's important to understand that not all of these are
good uses of recursion. We'll talk about when you should use recursion (and when you shouldn't), but it's a good idea
to think about ways to solve a problem without using recursion before reaching for it.

## A Function Calling Itself

The easiest way to think about recursion is that it's when a function calls itself. As we'll see later, there's a bit
more to it than that, but it's a good starting point.

[Click here to take a look at an example of this](./counting.js).

---

Let's take a look at a similar example. [Click here to see how we can use recursion to calculate Fibonacci Numbers](./fibonacci.js).

---

Seeing these examples, you might think that recursion is useless. Fear not!
We'll take a look at some of the real-world use-cases for recursion later.
Now that you understand the basics of what recursion is (and why it isn't always the best), let's get a bit more complicated!

## "Calling Upwards"

Normally when you write code, your functions "call downwards":
![A graph of functions only pointing downwards](https://raw.githubusercontent.com/Cratecode/intro/eba39b4607ea1d3872c782c00f92756a9e933d9f/images/Recursion-Call-Down.svg)
```js
function main() {
readData();
}

function readData() {
// ...
setup();
}

function setup() {
setupServer();
setupDatabase();
}

function setupServer() {
// ...
}

function setupDatabase() {
// ...
}
```

The idea of "calling downwards" is a little bit abstract, so let's see something that doesn't "call downwards":
![A graph of functions with a loop between some of the functions](https://raw.githubusercontent.com/Cratecode/intro/eba39b4607ea1d3872c782c00f92756a9e933d9f/images/Recursion-Call-Up.svg)
```js
function main() {
readData();
}

function readData() {
// ...
setup();
}

function setup() {
setupServer();
setupDatabase();
}

function setupServer() {
// ...
readData();
// ...
}

function setupDatabase() {
// ...
}
```

The only thing that's changed is that the `setupServer` function can call `readData`, but this has made the program immensely more
complicated. The reason for this is that `readData` can cause itself to be called again. If it calls `readData`, then
`readData` will call `setup`, which will call `setupServer`.

This is a bit more complicated than a function calling itself, but it still is recursion, just with a bit more complexity.
This type of recursion is one that you will actually see in the real-world. It's immensely helpful when representing complicated states
and behaviors. For example, if you wanted to write a program that takes in a math expression (`5 * 2 + 1`) and computes it,
you might end up needing to use this sort of recursion.

Unlike with our examples above (counting and fibonacci), this type of recursion usually isn't inefficient.
Getting to the bottom of what makes things fast is a little bit complicated, but in programs which use this type of
recursion, they're already doing so many different things that a bit of recursion won't cause any issues.
Additionally, they don't recurse that much, unlike some of our examples (like Fibonacci, which recursed trillions of times).

Usually you only want to use recursion in a select few cases:
* Your problem is much more complicated without recursion (i.e. there isn't a better way).
* You don't recurse many times.
* You're dealing with a type of problem where recursion is the best way to do it (we'll see an example of this later).

## Trees ("the most significant use-case")

Now that we've seen what recursion is and what its limits are, I want to introduce one of the most important places where
recursion is used: trees.

Trees are really similar to what we just talked about above. They're a way of organizing data so that things
point to other things, and they only point downwards. Sound familiar?

![A graph of functions only pointing downwards](https://raw.githubusercontent.com/Cratecode/intro/eba39b4607ea1d3872c782c00f92756a9e933d9f/images/Recursion-Call-Down.svg)

That first example we looked at is actually a tree. As soon as there start being loops though, it's no longer a tree.

So, our non-recursive function example is a tree, and our recursive example isn't. Even without any code or algorithms, trees
and recursion are connected!

But, let's take a look at some code. Imagine that you wanted to write a program to print out every item in the tree.

```js
function printTree(tree) {
// ...
}
```

We can start by going through every "node" at the top of the tree.
```js
function printTree(tree) {
for (const node of tree) {
// ... do something
}
}
```

The point of the program is to print out the names of the nodes, so let's do that:
```js
function printTree(tree) {
for (const node of tree) {
console.log(node.name);
}
}
```

This program will work, but it's still missing something. If we gave it our tree from above, it would only print out
`main`. That's because we're only asking it to print out nodes at the top of the tree.

A neat thing about trees is that we can can split them up into pieces. Now that we've printed `main`, let's chop it off!

![The same graph as above with the top node removed](https://raw.githubusercontent.com/Cratecode/intro/eba39b4607ea1d3872c782c00f92756a9e933d9f/images/Recursion-Call-Down-No-Main.svg)

Now, if we stick this into our function, it'll print out `readData`. As it turns out, this is exactly what we need to
do to solve the problem. This chopping off and re-running the function can all be written in code, and we'll use recursion
to do it!
```js
function printTree(tree) {
for (const node of tree) {
console.log(node.name);
printTree(node);
}

// The reason there's no base case here is that, when we get to a node
// with nothing under it, the for loop won't run, so the function won't
// call itself again.
// You don't always need a base case!
}
```

That's it! Our function will now print every item in the tree out, all on its own. This is all a bit abstract, so
[click here to try out a real-world example with trees and recursion](./files.js).

Here are some hints if you get stuck (click to reveal):
* ||LOCATION 1: You can think about a file as being your base case. There's no need to recurse further when you hit a file, just print it out.||
* ||LOCATION 2: Take a look at the code above. The problem you're working on is a bit more complicated, but the recursive part is exactly the same.||
* ||LOCATION 1: `console.log(fileName + ": " + fileData);`||
* ||LOCATION 2: `printFilesInFolder(fileData);`||

Good luck!
3 changes: 3 additions & 0 deletions sections/60_continuing_cs/00_recursion_intro/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"defaultFile": "index.js"
}
48 changes: 48 additions & 0 deletions sections/60_continuing_cs/00_recursion_intro/counting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// This is a function that counts from 1 to num.
// For example, if num is 5, then it will return
// 1 + 2 + 3 + 4 + 5 = 15.
// Instead of using a loop, it uses recursion.
function countRecursive(num) {
// This is called a base case. We'll see what that means later,
// but if you removed this (try it!), the function would call itself forever
// and crash.
if (num === 0) return 0;

// This is where the recursion comes into play.
// Notice that, each time this function runs, num is one smaller.
// Eventually, it will hit 0, and because of that if statement above,
// the function will stop calling itself.
return num + countRecursive(num - 1);
}

// Let's break down how this works. Imagine we're trying to find
// countRecursive(3).
// The computer will evaluate it like this:
// countRecursive(3) = 3 + countRecursive(3 - 1) = 3 + (2 + countRecursive(2 - 1)) = 3 + (2 + (1 + countRecursive(1 - 1)))
// = 3 + 2 + 1 + 0 = 6.

// Keep in mind that if you wanted to write this function in the real-world, you
// probably wouldn't want to use recursion.
// That's because there are easier and faster ways to do the same thing.
// Consider: a for loop.
function countFor(num) {
let counter = 0;

for (let i = 0; i <= num; i++) {
counter += i;
}

return counter;
}

// Recursion is pretty slow (we'll prove it in a bit), so it's good to look for other ways to do this.
// It's also worth noting that, for this problem, there's an even simpler (and faster) way to write it: math.
function countMath(num) {
// https://en.wikipedia.org/wiki/Arithmetic_progression#Sum
return num * (num - 1) / 2;
}

// Let's give these functions a try!
console.log(countRecursive(1000));
console.log(countFor(1000));
console.log(countMath(1000));
89 changes: 89 additions & 0 deletions sections/60_continuing_cs/00_recursion_intro/fibonacci.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Another common example of recursion is with the Fibonacci numbers.
// If you aren't familiar, the Fibonacci numbers are a list of numbers that looks like:
// 1, 1, 2, 3, 5, 8, 13, ...
//
// To figure out the next number in the list, all you have to do is add the previous two.
// For example, if we want to find the next number, we just need to add 8 + 13 = 21.
function fibonacciRecursive(num) {
// The first and second (index 0 and 1) are both 1, so we don't
// need to do any computation.
// This is our base case, and like before, prevents the function from
// calling itself forever (see what happens if you modify it!).
if (num === 0 || num === 1) {
return 1;
}

// And then, to get the next number in the sequence, just add the last two!
return fibonacciRecursive(num - 1) + fibonacciRecursive(num - 2);
}

// This feels a bit like magic, but let's take a moment to think over how the function works.
// fibonacciRecursive(3) = fibonacciRecursive(3 - 1) + fibonacciRecursive(3 - 2)
// = (fibonacciRecursive(2 - 1) + fibonacciRecursive(2 - 2)) + fibonacciRecursive(1)
// = (1 + 1) + 1
// = 3
//
// It turns out that, if you do this with any number, the computer adds ones together a bunch of times
// to get the right result.
// If you're finding this difficult to wrap your head around, try the function out with different numbers,
// and see how it's able to compute it.
//
// Unfortunately, just like before, this is super slow.
// If we tried to calculate the 100th Fibonacci number, we'd run into some problems.
// That's because that number is 354224848179261915075.
// That's a crazy number, but nothing a computer can't handle.
//
// The problem here is that the function adds ones together to get to the number.
// So, it'll calculate the number like:
// 1 + 1 + 1 + 1 + 1 + ...
// |______________________|
// |
// With 354224848179261915075 ones in the calculation.
// Computers are powerful, but not that powerful.

// Luckily, we can solve it a different way: like before, with a for loop!
function fibonacciFor(num) {
// Even though there's no recursion, we'll still use this
// if statement because it makes the code much easier to write.
if (num === 0 || num === 1) {
return 1;
}

let firstNumber = 1;
let secondNumber = 1;
// This algorithm starts at 2 because zero and one are handled by the if statement above.
for (let i = 2; i <= num; i++) {
let nextNumber = firstNumber + secondNumber;

firstNumber = secondNumber;
secondNumber = nextNumber;
}

return secondNumber;
}

// This for loop runs as many times as the number you put in it.
// That means, instead of 354,224,848,179,261,915,075 computations like before, we're down to 100.
// That's around 300,000,000,000,000,000,000% less work that the computer has to do!
// This isn't just a meaningless number either. Let's see how much slower the recursive version is.

console.time("recursive");
// We'll run it a bunch of times to get good data.
for (let i = 0; i < 200; i++) {
// 100 will take a ridiculously long time to compute, so let's use a smaller number.
fibonacciRecursive(30);
}
console.timeEnd("recursive");

console.time("for");
// We'll run it a bunch of times to get good data.
for (let i = 0; i < 200; i++) {
// 100 will take a ridiculously long time to compute, so let's use a smaller number.
fibonacciFor(30);
}
console.timeEnd("for");

// The for version takes a split second, but the recursive version takes so much time to complete that you can feel it.
// If we wanted to get even faster, there is a formula for Fibonacci numbers.
// If you're up for a challenge, try implementing it and seeing if it's faster than the for version.
// You can find the equation here: https://en.wikipedia.org/wiki/Fibonacci_sequence#Closed-form_expression.
Loading

0 comments on commit ba50cab

Please sign in to comment.