From ba50cabc2db05f07be65ad3002c1ff3a75f5bf1f Mon Sep 17 00:00:00 2001 From: uellenberg Date: Tue, 28 Nov 2023 21:23:01 -0800 Subject: [PATCH] Add recursion lesson --- manifest.json | 8 +- .../00_recursion_intro/README.md | 178 ++++++++++++++++++ .../00_recursion_intro/config.json | 3 + .../00_recursion_intro/counting.js | 48 +++++ .../00_recursion_intro/fibonacci.js | 89 +++++++++ .../00_recursion_intro/files.js | 75 ++++++++ .../00_recursion_intro/manifest.json | 9 + sections/60_continuing_cs/manifest.json | 12 ++ templates/editable_runnable/config.json | 10 + 9 files changed, 430 insertions(+), 2 deletions(-) create mode 100644 sections/60_continuing_cs/00_recursion_intro/README.md create mode 100644 sections/60_continuing_cs/00_recursion_intro/config.json create mode 100644 sections/60_continuing_cs/00_recursion_intro/counting.js create mode 100644 sections/60_continuing_cs/00_recursion_intro/fibonacci.js create mode 100644 sections/60_continuing_cs/00_recursion_intro/files.js create mode 100644 sections/60_continuing_cs/00_recursion_intro/manifest.json create mode 100644 sections/60_continuing_cs/manifest.json create mode 100644 templates/editable_runnable/config.json diff --git a/manifest.json b/manifest.json index 54a415b..c08bc44 100644 --- a/manifest.json +++ b/manifest.json @@ -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" ], @@ -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"] diff --git a/sections/60_continuing_cs/00_recursion_intro/README.md b/sections/60_continuing_cs/00_recursion_intro/README.md new file mode 100644 index 0000000..3836cec --- /dev/null +++ b/sections/60_continuing_cs/00_recursion_intro/README.md @@ -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! \ No newline at end of file diff --git a/sections/60_continuing_cs/00_recursion_intro/config.json b/sections/60_continuing_cs/00_recursion_intro/config.json new file mode 100644 index 0000000..01cc740 --- /dev/null +++ b/sections/60_continuing_cs/00_recursion_intro/config.json @@ -0,0 +1,3 @@ +{ + "defaultFile": "index.js" +} diff --git a/sections/60_continuing_cs/00_recursion_intro/counting.js b/sections/60_continuing_cs/00_recursion_intro/counting.js new file mode 100644 index 0000000..83adee0 --- /dev/null +++ b/sections/60_continuing_cs/00_recursion_intro/counting.js @@ -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)); \ No newline at end of file diff --git a/sections/60_continuing_cs/00_recursion_intro/fibonacci.js b/sections/60_continuing_cs/00_recursion_intro/fibonacci.js new file mode 100644 index 0000000..38d3767 --- /dev/null +++ b/sections/60_continuing_cs/00_recursion_intro/fibonacci.js @@ -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. \ No newline at end of file diff --git a/sections/60_continuing_cs/00_recursion_intro/files.js b/sections/60_continuing_cs/00_recursion_intro/files.js new file mode 100644 index 0000000..bf7cfb2 --- /dev/null +++ b/sections/60_continuing_cs/00_recursion_intro/files.js @@ -0,0 +1,75 @@ +// This represents files on a computer. +// It's stored as a JavaScript Object. +// If you haven't seen one of these before, it's a key -> value store. +// That means that, if I have an object that looks like { "a": "b" }, +// and I give it the key "a", it will give me the value "b". +// We can also stick objects into objects, like we do here. +// +// If we wanted to get `secretfile.txt` (`/home/me/Documents/secretfile.txt`), +// we'd need to ask `myFiles` for the value tied to `home`. +// That would give us another object, which we can ask for the value tied to `me`. +// Eventually, we'll get the contents of `secretfile.txt`. +// This can be written in JavaScript as `myFiles["home"]["me"]["Documents"]["secretfile.txt"]`. +const myFiles = { + "home": { + "me": { + "Documents": { + "secretfile.txt": "My secret", + "favoritenumber.txt": "1" + }, + "Desktop": { + "poem.txt": "Current Draft: roses are red, violets are blue" + } + } + }, + "logs": { + "systemlog.txt": "No errors detected!" + } +}; + +// Now, take a moment to think about how you might write a program that prints out every file. +// If we just had a list of files, then we could use a for loop, but the way the data is structured makes +// this really tricky. + +// The answer, as you probably guessed, is recursion! +// This is one of the cases where recursion is exceptionally useful. +// It helps us write programs that can navigate tricky data structures. +// Do you have any idea how to use recursion here? + +// Here's some code to get you started. +// Give it a try, and if you get stuck, that's alright! +// This is a really tricky topic, so there are hints included in the lesson (click the blacked out text to reveal them). +function printFilesInFolder(filesObject) { + // This is a for loop going through every key in `filesObject`. + // It will look at every file/folder in the object. + for (const fileName in filesObject) { + const fileData = filesObject[fileName]; + + // Don't worry about what this is doing. + // This if statement is checking whether we're looking at a file + // or a folder. + if (typeof(fileData) === "string") { + // It's a file! + + // LOCATION 1 + // What should we do here? + // Remember, this function is meant to print out info + // about the file. + // `fileName` is the name of the file, and `fileData` is the text + // stored inside it. + } else { + // It's a folder! + + // LOCATION 2 + // What should we do here? + // Remember, `fileData` is a folder object, + // and this function prints all the files + // contained in a folder object. + } + } +} + +printFilesInFolder(myFiles); + +// By the way, the source code for this website actually uses a piece of code super similar to this to +// deal with files, as well as lessons. \ No newline at end of file diff --git a/sections/60_continuing_cs/00_recursion_intro/manifest.json b/sections/60_continuing_cs/00_recursion_intro/manifest.json new file mode 100644 index 0000000..05b3de9 --- /dev/null +++ b/sections/60_continuing_cs/00_recursion_intro/manifest.json @@ -0,0 +1,9 @@ +{ + "type": "lesson", + "id": "les_recursion_intro", + "extends": "editable_runnable", + "name": "Recursion Intro", + "unit" : "main_intro", + "spec": "A series of files demonstrating recursion.", + "class": "tutorial" +} diff --git a/sections/60_continuing_cs/manifest.json b/sections/60_continuing_cs/manifest.json new file mode 100644 index 0000000..1f157b3 --- /dev/null +++ b/sections/60_continuing_cs/manifest.json @@ -0,0 +1,12 @@ +{ + "__comment": "This is a unit that deals with more advanced CS concepts.", + "type": "unit", + "id": "continuing_cs", + "name": "Continuing Computer Science", + "upload": ["00_recursion_intro"], + "lessons": { + "les_recursion_intro": { + "next": [] + } + } +} diff --git a/templates/editable_runnable/config.json b/templates/editable_runnable/config.json new file mode 100644 index 0000000..bf625af --- /dev/null +++ b/templates/editable_runnable/config.json @@ -0,0 +1,10 @@ +{ + "root": "/", + "outputView": "console", + "outputViews": ["console"], + "run": { + "command": { + "value": "node $CRATECODE_FILE" + } + } +}