Introduction to Recursion Harry Jain March 2020 Contents 1 Introduction 1.1 Recursion Basics . . . . 1.2 Aspects of Recursion . 1.3 Benefits of Recursion . 1.4 Downsides of Recursion . . . . 1 1 1 1 1 2 Recursive Problems 2.1 The Fibonacci Sequence . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 2 3 Divide and Conquer Algorithms 3.1 Merge Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . CS50 Notes: Web Track 1 Harry Jain Introduction Quote 1.1: Computer Science Humor “To understand recursion, you must understand recursion.” While recursion is one of the most important and most common problem solving techniques in computer science, it is unfortunately one of the most difficult to understand. Correspondingly, the above quote may seem nonsensical to a recursion novice, but is really quite funny to an experienced “recurser” (or at least I think so). However, after grasping a few basic concepts and doing a bit of practice, recursion becomes a powerful and indispensable tool. 1.1 Recursion Basics In short, recursion is the idea of breaking up a large problem into smaller problems of the same type. Once reduced to a certain size, many problems become trivial, e.g. sorting a list when there are only two (or even one) item(s) in it. Then, by combining the solutions to these sub-problems, we are able to solve our initial problem more clearly and concisely. 1.2 Aspects of Recursion In terms of code, there are two main aspects of a typical recursive solution: • Recursive function: this is a function that solves the problem by breaking it into smaller sub-problems and recursively calling itself to solve these sub-problems • Base case(s): the recursive function must contain at least one base case that determines when the recursion stops (otherwise it would get stuck in an infinite loop) 1.3 Benefits of Recursion While a bit opaque at first, the many benefits of recursion have made it a long-lasting and prevalent problem-solving technique. For example, recursion can • Reduce time complexity (as seen in Merge Sort below) • Make code easier to write and understand • Naturally fit many common problems in computer science and graph theory due to its inherent connection to trees 1.4 Downsides of Recursion While recursion is a widely-used and very powerful technique, it does have several downsides, including • Recursive “stack” uses more memory than other techniques • Recursion can be slow for some problems or if it is implemented incorrectly Page 1 Harry Jain CS50 Notes: Web Track 2 Recursive Problems Now, let’s consider what sorts of problems we can solve by recursion. Mirroring the basic aspects of recursion, these problems need to have the following attributes: • Able to be broken down into identical sub-problems: this condition guarantees that the basic form of recursion is viable • Easy to solve smaller sub-problems: this condition guarantees the existence of a base case, i.e. the fact that we can easily sort lists of length 1 or 2 2.1 The Fibonacci Sequence Now, let’s consider perhaps the most famous recursive problem, the Fibonacci sequence. Doing a bit of arthimetic review, consider the following geometric series: a1 = 2, a2 = 4, a3 = 8, . . . We can define this series one of two ways: 1. Explicit: in this case, we can find any term an using a closed-form expression, i.e. an = 2n 2. Recursive: in this case, we define each term based on the previous terms, i.e. an = 2 × an−1 While many sequences like this can be written in both explicit and recursive forms, some can only be defined recursively. One such example is the Fibonacci sequence, which is defined as f0 = 0, f1 = 1, f2 = 1, f3 = 2, f4 = 3, f5 = 5, f6 = 8, . . . (fn = fn−1 + fn−2 ) There is no simple explicit formula for the Fibonacci sequence, so it must be defined by the recursive expression in parentheses above, i.e. the sum of the previous two numbers of the sequence. Outlining the aforementioned aspects of recursion, we have • Recursive function: fn = fn−1 + fn−2 • Base case(s): if n = 0, then fn = 0 and if n = 1, fn = 1 This structure easily translates into code, which is outlined in the Python example below. 1 2 3 4 5 6 7 8 9 10 11 # Define the recursive fib function def fib(n): # Base case of 0th item: return 0 if n == 0: return 0 # Base case of 1st item: return 1 elif n == 1: return 1 # Recursive step: add the previous two Fibonacci numbers else: return fib(n - 1) + fib(n - 2) 12 13 14 15 # Print out the first 10 Fibonacci numbers for i in range(10): print(fib(i), end = " ") 16 17 print() Page 2 Harry Jain CS50 Notes: Web Track 3 Divide and Conquer Algorithms Formalizing our previous discussion of recursive problems, we will take a look at a common class of algorithms, called “Divide and Conquer.” These algorithms apply to the same kind of problems as described above, i.e. those that can be broken down into smaller problems of the same type. In terms of structure, these algorithms have three steps (from which they took their name): • Divide: Break the problem into two or more problems of the same kind • And: Solve each problem either recursively or using the base case • Conquer: Combine the results of the sub-problems to get the solution to the original problem Even with our previous recursive examples, this may seem a bit abstract. Thus, we will look at a common example in Merge Sort, one of the more efficient sorting algorithms. 3.1 Merge Sort To give a concrete example, let’s consider implementing Merge Sort, which sorts a list of length n using the following recursive Divide and Conquer form • Divide: Split the list into two sub-lists of size n 2 • And: Sort each sub-list either recursively or using the base case (for a list of length 1, return that element) • Conquer: Merge the groups together by progressively adding the smallest (or largest) item from either sub-list to the main list Once again, while requiring a slightly more complex implementation, this structure fits rather neatly into code, as outlined in the C program below. 1 2 #include <stdio.h> #include <string.h> 3 4 5 void mergesort(int nums[], int l, int r); void merge(int nums[], int l, int m, int r); 6 7 8 9 10 int main() { int nums[] = {1023, 37, 1, 2, 37, 23, 5, 2, 4, 2, 13, 522, 92, 23}; int num_count = sizeof(nums) / sizeof(nums[0]); 11 mergesort(nums, 0, num_count - 1); 12 13 for (int i = 0; i < num_count; i++) { printf("%i ", nums[i]); } 14 15 16 17 18 printf("\n"); return 0; 19 20 21 } 22 23 24 // // Sort the items in nums at indicies between l and r, inclusive Page 3 CS50 Notes: Web Track 25 26 27 28 29 30 31 Harry Jain void mergesort(int nums[], int l, int r) { // Base case when there is only one element if (l < r) { // Calculate middle element int m = (l + r) / 2; 32 // Recursively call mergesort on the left and right half of the list mergesort(nums, l, m); mergesort(nums, m + 1, r); 33 34 35 36 // Merge the sorted left and right lists merge(nums, l, m, r); 37 38 } 39 40 } 41 42 43 44 45 46 47 48 // Merge two sorted portions of nums: from l to m, inclusive, and from m + 1 to r, //inclusive void merge(int nums[], int l, int m, int r) { // Calculate lengths of left and right subsets to merge int llen = m - l + 1; int rlen = r - m; 49 50 51 52 // Declare the left and right subsets int lefts[llen]; int rights[rlen]; 53 54 55 56 57 58 59 60 61 62 // Initialize the values of the left and right subsets for (int i = 0; i < llen; i++) { lefts[i] = nums[i + l]; } for (int j = 0; j < rlen; j++) { rights[j] = nums[j + m + 1]; } 63 64 65 66 // Declare the number of elements used from each subset and set it equal to 0 int lindex = 0; int rindex = 0; 67 68 69 70 71 72 73 74 75 76 // Loop through each index of nums within our range for (int k = l; k <= r; k++) { // If there are unmerged items in both subsets, choose the larger one if (lindex < llen && rindex < rlen) { if (lefts[lindex] >= rights[rindex]) { nums[k] = lefts[lindex]; Page 4 CS50 Notes: Web Track lindex++; } else { nums[k] = rights[rindex]; rindex++; } 77 78 79 80 81 82 83 } // If the right subset has been exhausted and not the left, // place the rest of the left ones else if (lindex < llen) { nums[k] = lefts[lindex]; lindex++; } 84 85 86 87 88 89 90 91 } 92 93 Harry Jain } Page 5