Recursion in Programming - Full Course

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

Happy cake day !

👍︎︎ 1 👤︎︎ u/Bot_Testing_Reddit 📅︎︎ Jul 19 2021 🗫︎ replies
Captions
When learning about recursion, it can seem  like you're always going back to the beginning.   In this course, the simple engineer will help  you understand recursion using animations,   thought processes, and more. Hey, guys, and  welcome to another video brought to you by   the simple engineer. In today's video, we are  going to delve deep into the depths of recursion,   and strengthen your algorithmic mental model  around this programming paradigm will do so by   looking at a variety of different examples  and animations. So let's get right into it.   The first question that we have to answer about  recursion is what even is recursion? And I think   the best way to talk about it is through this  analogy, and let's imagine that you're sitting in   line waiting to withdraw money out of an ATM. And  for the sake of this example, let's assume that   this right here is you and you have this question  this underlying question. And you want to know   how many people are standing in front of you in  this line. And this kind of brute force, iterative   version of you says, Well, what I can do is I  could step out of line, I could run to the front,   and I can maybe, maybe count one by one. And maybe  I store these counts in some auxilary, you know,   notebook that I have some external variable that  you can imagine, and then you run down this line,   and you get back and you say, Okay, I got my  answer. But I did a lot of work. And we want   to kind of switch this paradigm, we want to think,  how can I be lazy? What's the least amount of work   that I can do, and sort of break this problem down  in some sort of sub structure? And so what I do   well, I tap on this girl's shoulder in front of  me, and I asked her a very simple question, I say,   hey, what number are you? And she turns around  and looks at me and says, you know, I'm sorry,   I don't really know. But what I'll do is I'll ask  the woman in front of me. And what happens is,   this process kind of continues, and it continues  up until we hit some stopping condition. And this   condition that we get stopped at is when we hit  this woman, and she taps on this guy's shoulder.   And he says, she says, hey, what number are you in  line? And he gives this response. And he's number   one. And this is interesting, right? Because this  is kind of the stopping condition to this sort of   problem that we were solving. And what she does,  if she takes that number, and she says, Well,   if he was number one, then I'm basically  one plus one, because I count myself.   And this guy says, Well, if she was number  two, that I'm one plus whatever she was,   and this idea unravels backwards to the original  asker. And this woman basically says to the guy,   hey, there were, you know, 10 people  in front of me, I count myself.   And now I'm at 11. And it turns out, that we can  model this problem with this simple blueprint,   we ask the first question, what's the least  amount of work that I can do? How do I break   this problem down into some sub problem?  And the second question that I have is,   when would the process complete? Like, what's  my stopping condition. And in this case,   it's when we hit the first person in line, which  is the gentleman withdrawing money from the ATM,   it just so happens that we can actually write  this problem in a very simplified, minimal,   beautiful piece of code. And the function that  we have is just this get my position in line. And   we have this kind of abstraction of a person  that we have. And notice the return values in   integer. And, again, if we think back to those  two questions that we have for this blueprint,   we say if the next person in line is null,  then we must be the first person in line.   Right? That's kind of this base case, imagine  nobody is in line, well, then you're probably   the first person in line. Now, if we don't satisfy  this conditional, then what do we do, we do just   a little bit of work, we say, I'm going to count  myself as somebody that contributes to the number   of people in line. And I'm going to add this  kind of recursive call, and I'm calling myself.   And this is the interesting property  of recursion, we're calling our self.   But the parameter that we condition on actually  further progresses us to the problem that we're   trying to solve. And so I'm not I'm not passing  the same person object into the function again,   I'm actually passing a person that progresses me a  little bit closer to the question that I'm asking,   which is, how many people are in front of me. And  this is really all recursion is about is how can   I take some large problem and break it down into  a bunch of subproblems such that each invocation   of my method gets me a little bit closer to the  problem that I'm trying to solve? Let's think   about another example. Let's take back to the the  school days where you're writing a bunch of essays   and typically this process, at least for me, is  you know, you writing So you submitted to your   professor and he says, Hey, you know, that essay  was terrible, I want you to go make revisions.   And the essay gets passed back to the student.  And this process can actually continue over and   over and over again, you write an essay, you  make revisions, you submit it, it gets denied,   and you rinse and repeat. And you do this  until the professor says, Okay, that was good,   I'm going to put it into my briefcase, and start  grading it. And it just so happens, this, again,   is this kind of recursive strategy that can be  developed. If we look at a simple piece of code.   And what are we doing, we're really, we look  at an essay, we revise the essay, we read it,   we get feedback on it, we apply changes to it.  And then notice, we just call ourselves again,   right. And there's a single base case  kind of hidden in here. And we do it   until the essay is complete. And notice  that each invocation is the same essay   object. But what we've done is we've inched  our way a little bit closer to that goal,   that goal of no longer needing revisions  on the essay. So the whole process again,   just to re emphasize is that we do a little bit of  work on each invocation of our method call Intel,   we hit some base case, some stopping condition  that says, hey, you no longer need to continue.   So again, like what is recursion, recursion is  nothing more than a method that calls itself   and to be more precise, it's usually a method  that maybe returns a value, maybe it doesn't.   But it's conditioned on some parameter, such that  when you hit some conditional at some point in   time, you can actually stop recursing, some base  case, right. And we consider this piece the base   case, this is the stopping condition, such that  we no longer grow the number of recursive calls   that we're storing in memory. And this piece down  here, this is the recursive call. This is where I,   you know, I do some unit of work, some small sub  problem that inches me or progresses me closer   to the goal or question I'm trying to  answer. Before we dive into the technical   intricacies of recursion, it's important  that we discuss some of the trade offs.   Why would we want to use recursion? And  why would we want to avoid recursion.   And I think there are valid examples for both.  And it really comes down to the situation.   The first pro that I'll give is that it really  bridges the gap between elegance and complexity,   we will look at a variety of different  problems where we're traversing   complex data structures like trees and graphs.  And it really boils down to three or four lines of   code. And that is vastly different than doing  something like that, in this kind of imperative   approach, where we're, we're looking at things  with a lot of loops and a lot of variables. And   that can get really messy really quickly, with  data structures that are inherently recursive.   Now, on the downside, you know, adding a bunch of   method calls on the call stack incurs some  CPU overhead, right, there is some slowness,   with calling methods in your code compared  to iterating through a loop. And that, again,   is another trade off you need to make it can be a  time or space trade off that you need to consider.   Now, I touched on this briefly, but again, like  recursion can reduce the need for complex loops,   and auxiliary data structures. recursion has  this sort of implicit stack, which is a data   structure commonly used in a lot of algorithms.  And so having that sort of implicit stack and   kind of self manage looping construct, it's  given to you as a part of recursive calls,   you can exploit that property to really simplify  your code and focus on the problem you're solving.   Now, the con for that is as you're  growing the amount of method invocations   in your computer memory, you can actually run out  of memory. And we'll look at a lot of examples as   to why this is, and we'll, we'll start to explore  this, this idea of a Stack Overflow exception. And   this is where we start to run out of  the pre allocated buffer of memory   that we have for our program, which can  actually cause your program to crash.   There are a lot of pros when it comes to recursion  when talking about optimization, reducing time   complexity. And that's what this idea of memos  zation, and we'll look at some examples of   memorization and caching, to help speed up  redundant calls. And that's a beautiful property   of recursion. Now, the con again, just like any  piece of code, is that if recursion is overused,   you can get into this sort of habit where  you start to develop really complex code,   if it's not well instructed,  and you want to make sure that   and what we'll get a lot better at this  as we go through this This tutorial   is when you look at problems, you want to  kind of ask yourself, Is this a good use case?   For recursion? Can I really break it down into  subproblems that make sense for recursion?   If you cannot, then you may get into this con  where you have unnecessarily complex code.   Now, the final thing, as I had  mentioned before, is recursion   works really well for recursively defined  data structures, JSON objects, trees, graphs,   things that allow you to basically focus on  one tiny unit of the data structure at a time.   And it just so happens we'll look at a bunch of  different examples as to why this holds true. But   this is just a small set of pros and cons, things  to consider when thinking about using recursion.   This brings us to a topic that I think is  often overlooked when teaching recursion,   which I think is one of the fundamental  concepts that you really need to grasp   to understand what people  call quote unquote, magical   about recursion. It's just magic with how  it works. And after we look at this example,   we'll start to realize how logical recursion is  and why it's not necessarily such a magical thing.   Let's imagine that you go to work one day, and  the first thing that you want to do is check your   email. And whilst checking your email, you get  interrupted by your boss, and your boss says, Hey,   I need you to go attend some meeting. And  you have to actually deviate from your task.   So you don't finish checking your email, you get  interrupted. And before you can check your email,   now you need to attend this meeting. Now, let's  say your boss, well walk into this meeting,   your boss interrupts you again. And he  says, Hey, our investors are visiting,   I would love for you to go to this board meeting  and impress them with all your knowledge. And   so again, we'll walk into this meeting, you get  interrupted, you have to go impress the investors.   Right, that's your next task. And so before you  can attend the meeting, before you can check   your email, you need to impress these investors.  And finally, your boss, he interrupts you again.   And he says, Hey, you know, I'm sorry. But I need  you to go help Jake with his code, his code is   failing. And we need to push to production. So now  you have to basically avoid the investor meeting,   avoid the initial meeting, you can't check your  email until you help j code. Once you help him,   this thing kind of gets popped off your to do  list, you know, you go to the board meeting,   you attend this original meeting. And  then finally, you can check your email.   And you may be asking, like, why is this ever at  all relevant to recursion. And it just so happens   that this sort of idea is exactly how the call  stack works when we're talking about invoking   methods within our programs. Let's take a  look at this simple program here. Notice   we have three method calls. And they're kind of  chained together. The first one returns a string,   but it has a dependency on B, B returns  a string, but it has a dependency on C.   And c just returns a string. So nothing fancy at  all. Now, the call stack is going to be this sort   of abstraction that our operating system leverages  to store method invocations within our program.   It allows us to understand what memory addresses  we return data to, and it stores local variable   information, like what are the parameters  that were passed into me. So if we execute a,   the first thing that goes on to the call stack  is this sort of idea known as a stack frame.   And it basically says, Hey, I want to call  Hello, and I want to concatenate the result of B.   But in order for me to concatenate the result of  B, I need to actually now call B. So that pushes B   onto the call stack. Now I'm in the same scenario,  where I want to now return from B, but I have a   dependency on C. So now I call C and put that  on the call stack. And notice this is the same   sort of process that we looked at in the  previous analogy. I cannot return or pop   these things off the stack until I kind of go in  the order in which these things were called. So   I returned friends, right. And now friends  replaces that see invocation. And so now   this has been fully evaluated. And so now since  B is fully evaluated, I can return that value   to the B method invocation. And now that  B stack frame gets popped off the stack.   And it's only at this point that I have now  evaluated that entire chain of method calls.   So now I can return this string  value. And get my expected output.   Now you may be asking, like why still? Why  is this relevant to recursive thinking.   And let's look at an example. Let's look at a  call stack when we call these recursive calls,   right? This is just a program that  calls itself and it'll execute forever,   right? So the first time I invoke a will now  I need to invoke a, but it calls a again.   And it just keeps happening, right?  There's no stopping condition. And finally,   there's going to be this point, this point  in time where I try and invoke a again,   and I get a, I get an error, and  that error is a Stack Overflow.   And this exception happens when we exceed the pre  allocated buffer of memory that our program has.   Right, we basically have run out of  memory, we've exceeded the stack,   and our invocations have overflowed,  and we can no longer handle it.   And this is the whole thing with recursion, why  we need a base case, we need to return a value.   So just like we saw previously, for methods  that aren't recursive, we noticed that frames   still grow and grow and grow. And the only  way for those frames to shrink in size   is for them to return some value for them to  stop invoking methods. And the same holds true   with recursion. The only difference is, we have  some sort of base case something that says, hey,   this is the one thing that I want to condition  on to avoid us from further recursing.   I want to start off the first technical component  of this presentation looking at recursion with   strings. And this is going to give us a really  good idea of manipulating input parameters on   the call stack recursively. And the first problem  I want to look at is this idea of string reversal.   And so what we do is we have an input string  like the symbol engineer. And the idea is the   output would be the input in the reverse order,  right? And so the question is, how do we how do   we build a really concise recursive function that  gives us something like this. And as we look,   as we look at the skeleton structure for this  code, we of course, are going to have some sort of   input, right? So the input is going to be some  string, and the output is going to be the reverse   version of that. And so we always ask kind of  these two questions. The first is, what is the   base case? And this is really asking, when can I  no longer continue within my algorithm? And the   next line of code is going to be all about what's  the smallest amount of work I can contribute?   So in this case, it's going to be basically  between each invocation, what's the small   unit that I can actually modify or manipulate  to progress a little bit closer to the goal?   So let's look at the first question. The first  question is saying, When can I no longer continue?   And I think I think when you think about this sort  of scenario, there are two schools of thought.   And when I when I like to construct base cases,  I typically think if I were to just pass in a   very small input, like what is the smallest  input that I could just pass in to start   to this function, where I would need to basically  immediately return. And there could be two schools   of thought with this approach, right? And the  two schools of thought could be, well, one letter   reversed is itself, right. And that could be a  really, really good base case for string reversal.   But we want to be even lazier. Like what is the  laziest, like the least amount of work that I   could even consider thinking about. And that would  be the empty string, the empty string reversed,   is again, just the empty string. And so if I  passed in the empty string to this function,   and it would only make sense  to get the empty string back.   And so if we, if we modify the code, and we  look at this, we just have a simple base case   where we evaluate if the input string is the empty  string, then let's just return the empty string.   But now we need to consider how do I even  get to that point? What's the smallest amount   of work that I can contribute? Right? And  that's the question that we're asking here.   I need to do something that whittles down the  decision space within each recursive call.   And so we kind of asked this question, what's the  smallest unit that I can deal with in a string? A   string is just a bunch of characters, right?  And so maybe I can modify a single character.   And this is where we get to this question. Let  me pick a single character out of this string.   And maybe where I position it will allow it to be  concatenated from the call stack in the reverse   order. And it turns out when we when we write  this code, we get a recursive call where we,   we take that first character from the input string  and we concatenate After the recursive call,   and you may be looking at this and you say, Okay,  well, the input parameter has changed. And it's   changed because we've actually shrunken down the  decision space, we've shrunk in that input string,   because our entire goal is to get closer to that  base case, right. And so we've taken everything   directly after the first character to the end.  And the idea is that if we do this enough times,   we can shrink our search space on each invocation  and get the goal, which is the reverse string.   So this first piece is all is all focused on  shrinking the problem space. And the second piece   is reflecting really the work that we're doing to  contribute closer to the goal. And I think it's   worthwhile to analyze what is happening on the  call stack. So let's say that we pass in Hello.   And we don't hit the base case, on line two, we  immediately go to line six, which is basically   the recursive invocation with the shrunken down  substring. And then we concatenate the H. And just   as we had discussed with call stacks, we can't pop  this off the call stack until that recursive call   has also completed, which adds an  additional stack frame to the call stack.   And you notice, again, we don't hit the base  case, and we shrink down that input string,   but we concatenate the first character. And we  keep doing this. And it's nice, because I don't   need to keep track of the H, I don't need to  keep track of the E. It's all self managed for   me in the stack frame on the call stack. And so  I invoke reverse string again. And now I get Oh.   And now watches this gets fairly  interesting, because as I pass in,   oh, I get to a point where my input string has  been whittled down to just the empty string.   And it just so happens that for this use  case, this is the base case. So I return   the empty string. So that reverse  string gets evaluated the empty string   from this base case, and I end up just  returning the empty string plus Oh.   And now this recursive call gets evaluated. And  now I add L. And this just becomes o l, right.   And so now I can return this. And  this gets popped off the call stack.   And now you can see that I'm basically returning  these values to the stack frame that preceded me.   And popping things off the call stack.  This function gets evaluated in ADS II.   So I return it and pop this off the call  stack. This function gets evaluated to o Ll E.   So I return it, it gets evaluated. And then  this gets popped off the call stack. And as   you can see, this is the goal, right? This is the  reverse string. And it's the power of the call   stack the power of these recursive calls that  allow us to return values back down the stack.   And this is a very good property that  we can exploit when using recursion.   Let's look at palindromes, palindromes are  these unique words, where we can basically   spell the same word forward and backward.  Let's look at this word. The mechanical   way that we kind of analyze if a word is a  palindrome or not, is we look at both ends.   And we basically say okay, do these letters match?  Yes, they do. So we shrink in Word, we say do   these letters match? Yes, they do. So we shrink in  Word. And now this is just one character. And it   proves that we've we've matched successfully.  So this indeed would be a palindrome.   Now let's look at a snippet of code to see how we  could think about something like this recursively.   Now, obviously, we're going to have a  Boolean function, because it's either Yes,   it's a palindrome or No, it's not. And  we're going to evaluate some input string.   And of course, the first thing we always consider  is what is this base case, the thing that stops us   from recursing? Now, think back to what  I had said in the previous example,   I always like to consider my base cases, what  is the smallest input that I could just pass   into this function? And it turns out very  often not very often, for string recursion,   you can typically whittle down your search space  and evaluate the input length. So if you passed   in a palindrome that was of size zero, then  that's a palindrome, right? Because there's   there's nothing that proves it's not a palindrome.  There are no characters to compare against.   And in the same vein, if we pass in just a single  character, with a single character, both forward   and backward is the same character. So that's also  a palindrome. So these are really good use cases,   or base cases to evaluate this conditional,  which is whether or not a string is a palindrome.   But let's continue we need to  consider the small amount of work   that we can do. And in the animation that we just  looked at, you notice that we had two pointers,   one on the left and one on the right, and we  were comparing those strings, those characters,   we're saying they need to be the same. If they're  not the same at any point in time, then we've   violated the property of a palindrome. And if we  violate the property of palindrome, then we just   go to this false. This is kind of this fallback  base case. So at any point in the algorithm, if   the characters don't match up at their respective  opposite indices, then we can terminate,   we return false and the false gets propagated  through the call stack. And the initial color   function would return the false. But if that's not  the case, then we get to this interesting thing,   where we call this recursive call, and it whittles  down or sub our substring. So let's look at the   call stack to understand what's happening. We  pass in race car as an input string. And this   of course, is a palindrome you can see race  car forward and backwards, it's just race car.   And the first thing we do is we evaluate  this, this conditional this base case,   is the length zero or one? Well, it's definitely  not. So we continue. And now we compare the   first character and the last character for  this input string. And if they're equal,   then we just call ourselves again,  but we shrink that input string.   And since these since R and R are equal, we  add another stack frame to the call stack.   And we've shrunken down our input string,  we again, look at the base case, we don't   satisfy it. So we compare the first and the last  character. And we notice in this in this case,   they're also equal. So we call ourselves again,  right, and we don't pop the stack frame off the   call stack, because we still have more work  to do. And so we shrink down our input string,   we still haven't satisfied the  base case. And our characters   at the first and last position are still the  same. So we do just a little bit more work.   And now we're at the last character. And  we've already come to this conclusion that   if the input length is zero or  one, then we just return true.   So for this one particular subproblem, this one  particular element, this is indeed a palindrome.   So what do we do, we say, yes, this was a  palindrome and we pop it off the call stack.   And since we return true, and all of these other  conditionals have been satisfied, that we can just   propagate through the true Boolean down the call  stack. So we return true here and pop this off.   We go to the next stack frame and return  true here and pop this off. And now we   get to the final stack frame, which evaluates  to true. And this gets popped off the stack.   Now I want to look at recursion with numbers.  And the thing that you'll start to realize as   we go through these sorts of problems, is that  the same blueprint holds true for all of them.   And the first problem I want to look  at is this idea of converting decimal   base 10 values to binary, which is a base two  number format, ones and zeros. So the question is,   how can we convert a number like  25 into its binary counterpart.   And it turns out, there's a very mechanical way  of doing this. So let's take the number 233.   And the formula here, as we'll come to find  is we can do a division by two and this is   basically doing the floor operator. So it gets  us an integer instead of a floating point value.   And we get we get an output, which is 116. And  we have a remainder, and that remainder is one.   And the mechanical process that we can  follow to convert decimal to binary is   we take the results of this operation,  which is 116. And we divide that by two.   And we just keep evaluating, and this is 58,  remainder zero, and then we take 58. And we   divide that by two, and this is 29, remainder  zero. And we basically just keep taking the   result in the output and dividing it by two. And  notice we're keeping track of the remainders here.   And the interesting property by doing these  operations gets us to a point where we can   take all of these remainders. So notice, we've  gotten to this base case, which is zero. And that   kind of halts this progression. So now we're  done. And if we take all of these remainders,   in the order that they were basically pushed onto  the stack, we can evaluate them and this is the   result in binary string for 233. And the question  is, how can we Okay, this this mechanical process,   you know, very formulaic? How can we  convert it into some recursive operation?   Let's take a look at some of this code. Now,  notice, we said the first thing is we took 233   divided by two, we got some output The remainder  was one. And so there are actually many ways   of doing this problem. And we're going to keep  it really simple. So notice we're dealing with   strings. As the result outputs, we're going  to be concatenating strings basically.   And so what we consider here is we we think  about that base case, right? If I hit zero,   then there's no longer reason for me to  divide further, right, I return my result.   Now, if I have not hit zero yet, it means that I  still have more division to do. And so the first   thing that we do is we get that remainder. So  when we look at 233, divided by two, we want to   store that remainder because that represents  one of the binary digits that we care about,   basically tells us if the value is  even or odd. And this is the binary,   this is the remainder that will contribute to  the result. And so that's why we store this   in the result. And notice, we just prepend this  to the result. And now what I do is I again, just   call find binary and I shrink my problem space  by half. And I just propagate the result through.   Now let's let's actually look at the call  stack. By coding this. And seeing what it   looks like I want to take the code that we  were just looking at, but look at it from   the perspective of the call stack and understand  how these stack frames are building up over time.   Now, there are many ways to code this fine binary  function. And a lot of people do it returning an   integer. And that's completely fine. And if you  want to do it that way, you can actually follow   along with any modification of this and analyze  the call stack with me. So as we run in debug,   the call stack is going to show up down here. So  these are all the stack frames that we can watch   grow. And the variables on the current stack  frame will show up under here in local. And so   as we dive into this function, you notice that  this is the first stack frame for find binary.   And in order for this to complete,  we need to return some value.   We haven't hit our base case yet. So we jump into  our work. And we just make another recursive call.   So the first stack frame can't pop off yet,  because we're going to invoke ourselves again.   And once we do that, you notice  that we add another stack frame.   And also notice that the input parameters have  changed. We've shrunken down our decision space,   and we've appended the first digit to our binary  result. We haven't hit our base case yet. And we   continue. And again, we do a recursive operation.  And our decision space keeps shrinking. And this   is the same mechanical process that we saw in the  animation, we divide by two and log the remainder,   we divide by two and log the remainder. And we  do this until we hit some sort of base condition.   Right. And as we shrink down our problem  space, notice we get to this point where   the decimal is one. And that still is  not our base case. So we go through,   we get the result. And this binary string  is looking pretty good. In on the final   recursive call, you notice the decimal is  now zero. And this is a really good case now,   because now we can just return. So we  come in here and we return this value.   And this value comes in and says okay, find binary  for that invocation returned this result. And so   now we're just saying, okay, continue. And notice  as I step through, the stack frames have grown.   But now they should shrink off the call stack. So  I step through. And notice they've all shrunken   down, so all of them have returned, they've all  returned a value. And now once I step over here,   we noticed that the binary  value has been evaluated,   you can come into this debug  console and look at it.   And this is the resulting binary string. And so  it's good to look at how the call stack is working   to understand. Okay, how many stack frames do we  build up to get to our result? And how do they   unravel once we hit our base case here, we've just  straight up propagated the result down through   all of the stack frames, and all of them just  return the same thing. And we'll look at a lot of   different examples where all the stack frames kind  of work together. And they wait for the result to   do a little bit more work. And so there's a lot  of different versions of this. The next problem   that I want to look at with numbers is the sum of  natural numbers. And the idea of this problem is   you take an input number like 10, and then you sum  all the values up to 10. And what we do here is we   actually just add them all up, and we get some  output and the output in this case would be 55.   And the question is how can we build some  succinct function that Does this recursively   Now let's look at a little snippet of code and try  to understand what's happening behind the scenes.   So recursive summation, again, we take in an input  value. And the first question we need to ask is,   what is the smallest input value that I could  pass in. So if I pass in one, for example,   the sum of one to one is just itself. One,  right. And so that's a good, that's a good place   for the base case. And again, keep in mind, we  are simplifying these functions. So you know,   edge cases, we're not really focused on that.  Right, now we're focused on the core goal,   which is building a succinct function. So  if I pass in one, I would return one. Now,   if I pass in a larger number, like 55, I still  have a lot of work to do, I need to add up   all the proceeding numbers up to 55 from one. And  so what I can do is I can take whatever value I am   currently. And I can add it to myself again,  but subtracting one from that numbers, right.   So it shrinks me closer to this base case. Let's  again, look at some code and understand how the   call stack is working with this sort of code. So  I've taken the same code from the slide. And now   we want to look at the call stack and understand  what's happening as we make these recursive calls.   The first thing that we'll look at is the number  five, so we want to add all values from one to   five. Let's put a breakpoint here and debug into  the call stack. So as I dive into this operation,   we first evaluate the base case, which hasn't  been hit yet. And so now what I want to do is   I want to take five, and I want to add it to  another recursive call, but that recursion   is shrinking down the input space again. And  so as I dive in, notice this input number   change. So I make the recursive call, and  the input number has shrunken down to four,   and another stack frame has been added to the  call stack. And so we keep evaluating has the   conditional been hit? No, it has not. So I  come in here and it shrinks down to three.   And I keep doing this, and I come in here. I  come in here. And now the input number is one   has the base case been hit yet? Yes, it has. And  so if I dive into this, you notice that we return   one here. And this one returns this  value to the stack frame right below it.   And so notice that as I continue, that stack  frame gets popped off. And you can see that the   invocation of this method returned one here. And  so now what I'm doing is I'm taking two plus one,   and I'm returning this value and  this value is going to continuously   unravel through these stack frames. So  notice, all of these get popped off.   And here's the final invocation, the input  number is 10 here. And as we continue,   this is evaluating for recursive summation of 10.   And so now again, for 10, we get down to a base  case. And that base case is one. And if we look,   so I'll put a breakpoint here. And we continue, we  can see that we have the results of 15 and 55. And   these get printed out. And so again, this is just  an example to show as we're looking at the stack   frames, how a stack frame can return a value to  its previous stack frame to finish the operation.   And that's the key here for recursion. And we'll  keep doing this for a lot of the problems that   we look at to get familiarized with how the call  stack is working, and how the stack frames grow.   I want to talk about divide and conquer  algorithms, which are really great demonstrations   of recursion. And the blueprint that we think  about for dividing conquer holds through so   we you know, still divide a large  problem into several small problems.   But divide and conquer is all about we divide  them into subproblems. We independently solve   them. And then we actually merge the  results to solve some holistic problem,   right? We merge them together and say, Hey,  this is the solution. And they are typically   recursive. And let's look at the first one,  which is binary search. And the entire purpose   of binary search is we look at a sorted list of  numbers. And the key point here is they're sorted.   And let's call this array. And the first  thing that we say is we say alright,   I'm going to start with the left and right  index, so the far left index is zero and the   far right index is the length of the array minus  one. And the whole purpose of binary search is to   find a value we want to find a value in this  array. So we know that zero is less than the   length of the array. So we can Have not hit  our base case and we calculate the midpoint.   So the midpoint is the left and right index added  together and divided by two. And so we check, we   say, okay, is that midpoint? The answer that we're  looking for? So that's another sort of base case,   have we hit or found the number 10? Here? And the  answer is no. So we ask one of two questions, the   first question that we ask is, is the number that  we're looking for on the left half of the array,   or is the number that we're looking  for on the right half of the array.   And remember, we just found the midpoint, so  we're considering everything to the left and   the midpoint and everything to the right of the  midpoint because the input data is sorted. Now,   if it's on the left half, which is what we're  looking at here, then we completely discard   the right half. And notice, the way that we do  that is we change the bounds of the problem.   So the left half stays the same. So our  starting point on the left stays the same,   but we only consider up to the midpoint minus one.  And so that is our ability to shrink our decision   space and completely discard the right half in  each recursion, or each recursive invocation.   Now, if we get to this other evaluation, this  is basically saying the value we're looking for   is on the right half. And what this allows us to  do is we only consider our starting point to be   the midpoint plus one all the way to the  end. So we completely discard the left half   for the rest of this problem. And  this indeed, is also a recursive call.   So look at the first stack frame in the  call stack to the right, we start with zero,   our upper bound index for the right variable is  nine. And the number we're searching for is 10.   So here's our midpoint, this is  three. And we asked this question,   Is this the value that we're looking for?  Is this 10? And the answer is no, it's not.   So what we can do is we can discard the  entire left half of the search space because   10 is greater than the midpoint. And it turns out,  we add another stack frame to the call stack and   notice how the parameters have changed. Now for  the left, the starting bound, which is the left   is five, which is the index where four is,  and the upper bound is nine, which is the   original length of the array that we're working  with. And again, we calculate the midpoint,   the midpoint on this sub array is nine, and we  say is 10, greater than nine or less than nine.   And of course, we know 10 is greater than nine.  And so what we can do is completely discard   the left half of that sub array and continue. And  we add another stack frame. And the stack frame,   the start bound is eight in the upper bound is  nine. And we again, recalculate the midpoint.   And the midpoint at this point actually turns  out on line seven, we've hit this base case,   the base case here is that we've found  the number that we're looking for. And   this is the solution. And so what  we can do is just return this value.   But we're not done yet, because we need to still  consider the call stack. And so this value is 10.   And so what we do from this stack frame is we  return 10. And in this case, we returning the   index that 10 is app. So 10 is at the eighth index  of the array, and this gets popped off the stack.   And now this binary search invocation has been  completed, and it gets eight. So it just returns   eight. And this gets popped off the stack. And  now finally, we return eight. And we're complete.   And the final stack frame gets popped off the  stack. And that's how binary search works. Let's   look at Fibonacci, a classical, mathematical and  computer science problem that often demonstrates   the power of recursion. And we're going to  be looking at the non optimized version here.   And we'll add some optimizations at the end. And  let's look at the mathematical expression. First,   we're basically saying that for some input in at  the index n is going to be made up of the sum of   the two values at the two indices that precede it.  Let's look at the Wikipedia page for Fibonacci.   And this is the sequence the Fibonacci sequence.  And if we take one of these numbers like 55,   it's the sum of the two numbers that  preceded so 34 and 21. And if we look at 34,   it's the sum of 21 and 13. And if we look at 13,  it's the sum of eight and five, etc. And this,   this formula holds true for all values of one to  infinity. And so this is the Fibonacci sequence.   So if we look at this expression again, now we  have these base cases in pink, and basically says   that for the values of at index zero and one,  the base cases are zero and one was Effectively,   and we can just return at that point. So that  would be where we stopped that recursion.   And the piece of yellow is just saying that this,  this holds true for all values of one to infinity.   So let's let's kind of look at this and  understand why it differs from the previous.   Now we're dividing and conquering, right, we're  dividing this problem, we have fib of n minus   one that needs to happen, we add it to fib of  n minus two. And these are two recursive calls.   And as we think back how the call stack works,  we know that the recursion on the left needs   to complete before we even considering, consider  starting the recursive call on the right. So fib   of n minus one could have a ton of different  calls that need to complete before we even do   the plus operator to fib of n minus two.  So let's look at how this would animate.   So we want to find the Fibonacci of five. And like  I had said, we do not evaluate the right hand side   of the expression yet, because we need to satisfy  that first recursive call first. So this gets us   F, fib of five minus one, which is four. And  this gets us four minus one, which is three.   And this process continues F of two. And remember  our base case, f of one is just one, so we would   return from here. And now to can evaluate the  right hand side of its recursive operation.   Remember, we're calling the same function over  and over again. So now we've hit a base case,   we've evaluated the left hand side, which is  fib of n minus two, or fib of n minus one. And   now we can do fib of n minus two for this value,  which is f of zero. This again is a base case.   So we can return from here. Now, the sum of these  two values return and get passed up to F of three.   And now F of three can evaluate its recursive  call on the right hand side of that plus operator.   And again, we do f of one, that's a base  case, so we just return it. Right? Now this,   these F of two, f of one get added together to  return from F of three, and this gets passed to f   of four. And now F of four can evaluate the right  hand side of its expression, which is f of n minus   two. So here we get F of two. And again, this  recursive property holds true, this is a base   case, we return it, we evaluate the right hand  side, this is a base case, we return it. And   now this gets propagated back up to F of four. And  these values get propagated back up to F of five.   And it's from here. Now, this is the very first,  you know call of the function, we can now evaluate   the right hand side of that plus operator. And  again, we get down to f of three, we get down   to f of two, and then this is f of one. And  this is a base case. So we return it and this   is f of zero, this is a base case, we return  it. And notice like we're doing the same thing   over and over again, this gets returned. Now we  go to the right hand side, this gets returned,   this gets passed up. And now we can add these  two values. And that's how we find the Fibonacci.   Now, we'll look at optimizations for this later.  But I just want to point out one thing like as we   evaluate this, notice, we have f of three here,  we have f of three here. This is redundant,   right? All of the nodes below F of three are the  exact same. And so it seems extremely wasteful,   that we're recalculating these values. And so as  we'll come to realize, there are optimizations   that we can consider to avoid recalculating  something that we've already done the work for.   So if you look, we have f of two, f two and  f of two. And this is three nodes. And you   can imagine for really large numbers,  it turns out for Fibonacci, you know,   most modern day computers cannot run Fibonacci  for this function at a very high input. And that's   because the recursive calls are so intensive. So  we have to look at some optimization techniques   known as memoization. Merge Sort is this poster  child of divide and conquer when explaining this   in a lot of computer science classes. And the  idea is that we take in a bunch of unsorted   values, like the following. And the idea is  that we divide this array in such a way that we   keep dividing. And then we merge up the sorted  results to sort this array in ascending order.   And it kind of looks like this. So we split the  array in half and we say, Alright, I'm going   to focus on the left hand side. And remember,  before I even do the right hand side, I need to   finish the left hand side first, right? That's the  order in which recursion is going to operate here.   And this process just holds through. So this is  the left hand array and what do I do I split this   in half. And now I decompose this into two parts.  But again, I first have to focus on this half. And   then I can focus on this half. So I look at, you  know, I split these, and I look at foreign one.   And the base case here is that you  can't really soar just one value.   And so I can stop splitting when I hit  just one value. And I compare these two,   and I merge and sort them together  just by a simple comparison operator.   Now I can look at the right hand side, and I  basically split these. So I have one integer here,   and I take two and zero, and two and zero gets  split even further. And again, I'm down to this   case where I just have digits, and so zero to  get compared, they get merged up. Now three has   a linear time comparison, again, zero and two,  and these get merged together. And now we take   one and 402, and three, these get merged together.  And now I've solved the left half of this array.   But remember, now I need to do the right half,  we're evaluating in the order that recursion   would consider this. So when we split this, I get  the right half of the array. And I do the same   thing on this side. So I take this array, split  it, I get negative one and seven. This gets split,   I compare them. And it just so happens, they were  already in sorted order. So these get put back in   the same spot. Now I take the right half of the  array, right, and I split this even further.   And so when this gets split, I  have 10, nine and 20 gets split,   nine and 20 are individual digits. And when I  compare them, they are already in sorted order.   And then I compare all the digits here,  and they get merged back in sorted order.   And then again, finally I compare these digits,  and they get merged back in sorted order.   And this brings us to the final merge,  remember divide and conquer is all about   dividing your problem into subproblems.  And then merging the results together.   So what we've done is we've just recursively  merged all the results together from the   recursive sub calls. And this is the final merge.  And just to emphasize how this comparison works   to ensure that these things get put back  in sorted order, we do a linear comparison.   And what that means is we basically have two  pointers, starting with the left hand side   and we say alright, which number is smaller, and  we know negative one is smaller, so we put this   in its spot and increment the pointer. And then we  compare these two values will zero smaller here.   So we put that in spot and increment the pointer.  And we keep doing this as we compare values.   And notice here, we've actually run out of  values in the left hand array. And since   we already know the right hand array is sorted,  we just placed these back in the positions they   belong. And as we look at the resultant array,  we noticed that Merge Sort has sorted the input   completely. And so this right here is the sort of  solution. And now the question is, how do we build   something like this? How do we devise a recursive  algorithm that could construct a sorting,   you know, solution to a problem like this,  we're gonna look at some code to do that.   So we have a blank slate here. And we want to  consider how Merge Sort would work to satisfy the   properties that we looked at. Now, the first thing  I want to do is build the recursive call. And so   remember, Merge Sort takes in an array, and it  sorts it. And we're going to do this in place. So   I'm going to build a function that's public, and  static. And it's just going to be void. So we're   going to modify the original input array. And what  I'm going to take in this is going to be called   merge sort. And it's going to take in just a one  dimensional integer array, and we'll call it data.   And it will have a start, and an end, which will  represent the indices that we're working with.   Now, for merge sort, this is going to be a  recursive call, we need to consider the base   cases. Remember, we work from the start  to the end. And if those values overlap,   then we've hit our base case. So we asked  this question, if start is less than end,   and we can continue doing work. But if the  start passes, whatever the end pointer is,   then there's no more work to continue.  We've already sorted the data.   And so remember, we're all about taking the  array and splitting it in two halves, we want to   divide the problem into two halves and solve them  independently. And so the first thing that we can   do is we can calculate the midpoint. And so what  we do is we say in mid is going to be basically   whatever the start index is of the array plus  the end index, and we divide that by two,   and that's going to be the  midpoint that we're working with.   And what do we want to do? Here, well, what  we want to do is we want to divide the array   in two parts. And so it would make sense for us to  just call merge sort. We're dealing with the same   data array. But our bounds are what has changed.  And so the start is again, for the first half   going to start at the same position, but  the end is going to end at the midpoint.   And that's going to be the left sub half.  Now, if we think about the right sub half,   the start is going to change, right? The start  is going to be whatever the midpoint is plus one,   all the way to the end. And this is how we can  consider splitting this array into two different   sub halves. But the question is, how do we how  do we merge this data, right, what we're doing   is we're just continuously dividing and dividing  and dividing, but I want to be able to merge the   data in sorted format. So let's build a function  called merge. And what merge is going to do,   it's going to take the original data array, and  it's going to start taking the start the midpoint   in the ending index. And remember that  last animation that we just looked at   does this kind of linear time comparison  to replace the values in the correct spot   when it's looking at the two sorted sub arrays, so  I build a function here, it's going to be public,   static void. And I'm just going to call this  merge. And again, it's going to take in an   integer array called data, a starting  point, a midpoint and an end point.   And now remember, we basically need to merge these  values, but I don't want to modify the input array   yet. So I'm going to build a temporary array. So  it would make sense to build a temporary array   to avoid modifying, can't type to avoid modifying  the original contents. And so in order for me to   do that, I can just build a simple temporary  array here. And it will be a new int array.   And the size is just going to be dependent on the  indices, right, so I have n minus start, plus one.   So this is going to build me a pre  allocated buffer of memory to hold   all the data for the sub arrays that I'm  dealing with, from the start and end index.   And now what I'm going to do is I'm just  going to copy the values. So I want to I   don't want to lose a reference to the start or  midpoint. So I'm going to say int i equals start,   we'll say j is equal to mid plus one, and k is  equal to zero. and k is going to be this kind   of tracker variable that we use to keep track  of the values that we put into this temporary.   So let's continue. Now recall back when we were  merging the data together in that final sub call,   that's a good way to kind of realize how this is  working. So we're basically doing a linear time   comparison, at the values in the left array, and  the pointer on the right array, and we're saying   which one is smaller, whichever one is smaller  is going to be placed first in this temporary.   And so I'm basically saying, while i is less  than or equal to mid, which is going to be   the left sub array. And we want to say,  well, j is less than or equal to the end,   then we can continue. And what is this  saying? What this is saying is while both   of the sub arrays have values, then  try and merge them in sorted order.   Right, and that's what we want to do. So  we're starting with I, which is the left   sub array up to the midpoint, and then j,  which starts from the midpoint to the end,   is going to be the right sub array. And  we just want to compare these values.   And so we ask a question, we say, Alright, well,  if data sub i is less than or equal to data sub j,   then we know what we know that the value in the  left sub array is less than whatever the value   is in the right sub array. So what we can do is  we can say, Alright, in this temporary buffer,   I'm going to put in at the index k data  sub i. So the smaller value gets put   next in the temporary. And now what I want  to do is I want to increment i, since I've   already placed it in the array, and I want to  increment K, so I don't override this value.   Now if this condition doesn't hold true,  then what I can say is well, okay, so   that would imply the opposite. So I would just  say temp of K is going to equal data sub J.   And then what we would do is we would say k plus  plus, and j plus plus. And you can also get fancy,   you can come in here and do something like k  plus plus here. And you can do something like   j plus plus here, it's both, it's going to be the  same thing. So if you want to simplify your code,   that way, you can do the post increment  operator to reduce the amount of code.   And this handles basically the comparison between  the sub arrays. But remember the conditional here,   the conditional says, we only do this while both  values have values to compare against. And the   example that we looked at, we actually ran out  of data in the left sub array. And we just had to   just blindly place all the data in the right sub  array into the original array. And so we need to   satisfy that case here. And in order to do that,  we can just say, while i is less than or equal to   the midpoint, so while there's basically data  to traverse, still, we're just going to place   it into the position of the temporary array  where it belongs. This would be data, so I,   and again, we would do k plus plus n i  plus plus. And that handles exhausting   the left sub array, if the right sub  array has run out of values, so we say,   add the rest of the values from the left sub  array into the result. Now on the opposite side,   if the left sub array has run out of values,  but we still have data in the right sub array,   then we just need to have the reverse conditional  and we say, while j is less than or equal to end,   then we're just gonna say temp  sub k is equal to data sub j.   And then again, we just increment K. And  we increment J. And this is the same thing,   we're adding the rest of the values from  the right sub array into the result.   You may ask, Are we done yet? And we're almost  done. But we need to remember we built this   temporary array, and this is void. And so we  haven't really done any work yet. And so the   question we need to ask is, how do we modify the  original data array in memory. Now, we don't want   to modify the entire array, we only want to modify  the subsection that we're dealing with right now,   in whatever recursive call that we're in. And  so this brings us to the copy phase, how do   we copy this data over from temp into the right  positions of the data, which is the original data.   And it turns out, we can just have a simple  for loop where we say i is equal to start.   And while i is less than end, right, so we're only  taking the subsection that we're dealing with.   So from start to end, right, which could be  any, any bounds at any point of the recursion.   So while I is equal to start n is less than or  equal to end, we're just going to increment i.   And in each iteration, we're going to load  up the original data array at the index i   to be equal to whatever's in the temporary  array at the value of i minus start.   So if the data array that we're dealing with, if  the start is 12, then what we would do is we would   say, all right, well, I equals 12. So 12 minus  star is zero. So we're going to load data set 12,   with whatever's at temp sub zero. And this  is the copy phase. This is how we actually   override values at each sub array.  Let's actually run some input here,   I'm going to have a new input array, we'll say,  you know, data equals new int. And we'll just load   it with some values, we'll say negative five,  you know, 2010 320. And what I want to do is   I want to sort this. So I'm going to call merge  sort. And I'm going to give it the data array,   the start is going to be the zeroeth index. And  the end is going to be the length minus one.   And so this should sort the array in place.  And so what I can do is I can put a breakpoint   to look at the array once it's been  sorted. And then we'll look at the   call stack to see what actually  is happening behind the scenes.   So I'm going to debug and we'll notice I  get here and merge sort has completed. And   what I can do is I can look at this stack frame  and look at the data. And you notice that we   have in the zeroeth index, negative 5023 10 and  20. And so this is the output in sorted order.   But this doesn't really do us any good unless  we understand what's happening at the level of   the call stack. So let's take a look at that.  By put a breakpoint in the merge sort function.   Let's kind of look and see what's happening. If  I dive in here, the first stack frame gets added   to the call stack for merge sort. And I basically  ask is start less than end? And the answer is no,   we still have work to do. And so when I go in  here, I calculate the midpoint. And I say I want   to divide this array into two sub halves. And the  midpoint here is two. And so what I do is I say,   Alright, I'm going to divide everything from  zero to two into its own set of sub arrays,   and then I'll handle the right half. And remember,  I can't even go to the right half until the left   half has completed. So I dive into this. And  now I'm looking at everything from zero to two.   And I have to break this up even further. And  so now I'm at this position where the midpoint   is one. And again, I grow the call stack,  and I'm still only on the left hand side.   And I calculate the midpoint again, and I  go again. And now we don't satisfy this,   right, because zero and zero are equal. And  so we actually pop this off the call stack.   And now we return to the next recursive  call. And the next recursive call says,   Okay, now I want to handle  the right hand side of this,   right. And when I dive into this, I grow the call  stack again. And again, we pop off, right, we   don't satisfy this base case, and we kind of just  continue this operation. And so now I've reached   the basically the base case on the far left hand  side of the first left sub half of this array.   And what it's asking me to do is it's asking  me to now merge these values in sorted order.   So if I dive in here, and we look at the merge,  let's analyze the start the mid and the end.   Remember, I'm only dealing  with basically two values here.   And so if I go in here, we have a temporary  array, and it's only of size two. And that's   because my base case here for merge sort is only  really comparing two values. And so we go in here,   and we say, all right, which one's smaller, the  left value, or the right value. And I come in   here, and I say, Okay, well, here, the left value  is smaller. So I load up K, I load up temp, and I   put in negative five. And then I have another  while loop. And remember, because I ran out of   values to compare in the left sub array, I need  to basically take all the values in the right sub   array and put them in the positions they belong.  So I come down here and I load up the values,   increment the counters. And now we're  done, because we've hit this base case.   So now, I have this sorted sub array, negative  five and 20. And what do I need to do I need to   put these in the original spot, right? If we look  at these values, I have, you know, I, J and K.   What does that mean? Well, it means that  from the position that I'm dealing with,   in the original array, I need to override those  values. So that's what this replacement is   going to do. So coming here, I replaced value  one, I replace value two. And now I'm done.   And that is the process that we go through  for a single iteration of merge sort. And   this process will basically continue as  we are dealing with bigger and bigger sub   arrays as we recurse back from the bottom  up, right, and now we're merging again.   And notice, like, since we did the left sub half,  which was just two values, now we're dealing with   the right sub half, right. And now we have three  values, and the right sub half of another sub   problem. And so we just continue comparing and  going up and going up. Now, we won't go through   every stack frame. In this call. There are quite  a few as we saw in the animation. But I encourage   you to rewatch that animation because that is  the order that the call stack will be evaluated.   And that's the order in which things will recurs  back up from the subproblems to get merged into   some overall solution. And that's one of  the key components of divide and conquer.   linkless are really common data structures used  to store data that is not necessarily contiguously   stored in memory. And it turns out, you can do a  lot of cool recursion on these sorts of things.   We're going to look at link list reversal.  And we'll look at an animation to walk through   this code. Now the idea is that we have some  sort of linked list. Let's say we have values   in sorted order like this. And we'll just have a  bunch of pointers. And the idea is that the head   node changes from one pointing to two pointing to  345. Instead, we want six to point toward five,   five, to point toward four, and so on. And when  we look at this code, this reversal code, we asked   this question, all right, if the head node is  null, right, if we pass in a null value, or the   next value from the head is null, then we just  return the head. And this kind of brings us back   to that idea of considering base cases. What's the  smallest thing I could pass in? If I passed in?   No, then I can just return null. And that would  be a reversed list. Right? And in the same vein,   if I passed in just a single node, where there was  no next node, then I could just return head again.   And those are good base cases for this solution.  But the question is, how do I even start the   reversal? What's the unit of work I need to  do and that's what we're going to look at.   So we start with one, the head node that we  pass in on the first iteration is one. And we   don't hit the base case. And so on line three, we  see that we call reverse list head dot next. And   that immediately pushes us to the next node.  And we have one stack frame on the call stack.   Now we're looking at two and this, this is not  know either, and it also doesn't have a pointer   pointing to null. And so we execute line three,  again at another stack frame on the call stack.   And that gets us to three. And this process  continues all the way until we get to six.   And when six gets passed in as a parameter,  we say is it null? And the answer's no,   but the next value is null. And so what  do we return, what we do is we return   head, which is just six, and six gets returned  as the node to the previous value. So this is   the base case. And we return this to five. And  so now P has been evaluated to the number six.   And what we're saying since since the number  five is the current head node, we're saying   five dot next dot next, equals five. And it turns  out that what that means is we're basically just   saying, Okay, well, what I want you to do  is take six and have it point back to five,   because head next to six, and head dot next, which  is six dot next, is going to be pointing to five.   And if that doesn't make sense, I encourage  you to really slow down and read this code.   And think five is the head node. And we want  to say the next pointers, next position just   points back to ourself. Now the problem here  is that five is is also pointing to six.   And if we were to trace this, this is this would  be a cyclic dependency. And this would never end   right. And so what we can do is we can just say  fives dot next, can point to nothing, right? This   is no, and so that gets dropped off. And what we  return is we return six, because we want six to be   the new head. And so six will just get propagated  down the call stack to the original color to be   the new head node. And that's why we return p  here. And so now we return and we get to four.   And when we look at four, we know that P is  six. And what we're asking here is we're saying   fours, next note is five, and I want five  2.4. And so again, we do the same thing,   we take five, eight points to four. And to avoid  the cyclic dependency, we drop this pointer here,   this connection. And what do we return?  What will you turn six, because again,   six gets propagated through, and six is always  going to be P in this case. And so now I return.   And I know six is P, but head dot next is four.   And so I want for 2.23. So I say head dot next dot  next equals three. So I draw a pointer. And again,   I dropped the cyclic dependency and I get rid of  this connection. And now I return, P is still six,   I return to two and I say two's next value, which  is three, I want three dot next to point to two.   So we do this, I dropped the pointer and I return.  And then finally we do this again. I dropped   the pointer and now we're done. So let's let's  take the opportunity to look at the call stack   and really understand how this is  working in a little bit more detail.   In order to save some time I've written some  code. The first piece of code I want to look   at is this idea of a node. Now this node  is just an abstraction that holds a value   and a pointer to The next node. And so this is  the easiest way to build a singly linked list.   Now, I also made a method called print link list.  And this will just print all the values. And this   is a verification that we can use to ensure  that we've reversed the linked list correctly.   And the final thing I want to look at is  the actual code. So this is the same code   that was on the slide. And I want to look  at the call stack as we debug through this.   So I have a singly linked list here 12345, just  as we looked at, and they're all linked together.   And what we want to do is we want  to actually see how the reversal   is working. So if I run and debug this,  we're going to look at the call stack.   So the first thing I hit is this call. And you'll  notice in the stack frame, as I dive into this   function, this is the initial stack frame that  gets put onto the call stack. And ask myself,   is the node that I'm looking at, which is  one, is it No, and the answer is no. is the   next value? No, the answer's no, it's  two, right. And we can see that here.   So I continue through. And I just call reverse  list again, which is a recursive call. And as   a dive into this, I add another stack frame to  the call stack. And now you see my values too.   And my next note is still not  know. So I continue through.   And I add another recursive call, which  adds another stack to the stack frame   to the call stack. And I continue. And  this process is going to keep continuing.   Now notice here, the value is five, but my next  value is actually null. And that satisfies this   base case here. So if we continue, you notice  I just return null here. And this stops the   recursion. So notice this stack frame should get  popped off. So as I continue through, that stack   frame gets removed. And now I'm looking at four.  And it actually shows me what the result of this   recursive call was for P. So as I step over,  we can look at p and see that P is just five.   And so I'm basically saying the node dot  next, so let's evaluate that. So node dot next   has a value of five. And I'm basically saying I  want five two point. So I want five dot next 2.2,   whatever my current value is, which  is four, right? So we know no dot Val   is four. So I'm changing the pointers  here. So as I step over, now I have four   pointing to know as I execute this, so we step  over one more. So as I look@no.val.no dot next,   there should be no which it is. And now the value  P which was five should be pointing to for now.   So P dot next, dot Val is four. Right.  And so the whole idea here is that now   we return the very last note again,  and we pop this off the call stack.   And the node that we're looking at at this point  is three. And right now three is pointing to four,   right? So if we look at no dot Val, which is  three, we can say no dot next dot Val, and this   is four. But this is wrong, right? Because we  want for it to be pointing to three, not three   pointing to four. And so that's what this line  is doing. I'm saying I want four to point back   to three. And so I do that, and then I drop  the connection. And so now if I evaluate this,   and we look at p, we can actually see the progress  that we've made five points to four, and four   points to three. Okay, so we're not done yet. But  we're making progress. And as we return from here,   we pop off the call stack. And we just keep doing  this process. We return we pop up the call stack,   we come appear we return and we pop up the call  stack. And now we're back to the original call.   And remember how we propagated the very  last value throughout all these calls. So   as we look at the reverse value, reverse dot  Val, it's five and this is our new head node,   which was the goal of this problem. And so if we  come in we look at reverse, we say okay, we have   five the next value is for the next value. There's  three the next value, there's two and an x value.   There's one, and then we're complete. And so this  is how the call stack works for a linked list   reversal. A really fun problem to consider with  linked lists is how do you merge two sorted linked   lists recursively. We're going to look at how the  call stack works for this Let's analyze this small   snippet of code and see if we can conceptualize  what's happening on the call stack. We take in   two head nodes. One was a list of values, and the  other is a singly linked list of values as well.   And we asked this question, the first question  is, what's the smallest input I could pass in?   And that's a good consideration for our base case,  the same formula that we've been applying. If I   pass in a head node for a, that's no, then I can  just return b, because B would be merged with no,   which would just be itself. And in the same  vein, if b is null, and I merge that with a,   then I can just return a. And if they're both  No, then no merge with no is also just the null   value. So this holds true. And that's the base  case. But let's consider the other side of things.   And this is kind of a similar  comparison that we saw with merge sort.   Right now we're considering, Okay, first of  all, base case. But second of all, what's the   unit of work that we need to do the recursion.  And I think it'll help if we look at two lists.   So we have two sorted lists of values. And  we want to go through this recursive call.   And the first thing that we do is we  add a stack frame to the call stack.   And it basically says, the head nodes that I have  are one and four, so neither of them are null.   And what I want to do is I want to say,  all right, which which value is smaller,   the value in the first node or the  value in the second node A or B,   A's data is one and B's data is four. So that  first conditional is the one that will be   recursive upon. And I basically say one dot next  is going to be pointing to sorted sorted merge.   And node A is going to be incremented by one  value, so I add another stacker into the call   stack. And so now I pass in eight. And now he is  getting compared with four. And we still haven't   hit our base case, right. And so I look at the  L statement. And I compare eight and four and   four is obviously smaller. And so this is  going to be the next node that I consider.   And I just continue this process.  So now eight and 11 gets compared,   instance eight is smaller, and I pass in 22. And  I add another stack frame. And now 22, and 11,   get compared. And since 11 is smaller, I pass in  the node right after 11. And these get compared.   And notice in the calls, the recursion needs to  complete, but I'm actually setting these results   as the next value. And so as we compare  22 and 1616 is smaller, so I pass in 20.   And now 20 is smaller. But it's interesting  because there is no node after 20.   And so now I'm at this point where I have  no. And now we consider the base case,   if we have a null value, which is B in this case,  then I just returned a. And if I return a then I'm   just returning the reference to the node of 22. So  here's the base case. And what I can do is I can   just simply return 22 here, and  this gets popped off the call stack.   And this is where the unravelling starts  to begin. So when I return 22 here,   I actually change 20s dot next pointer, right,  because I'm saying 20 dot next, is equal the   result of sorted merge. And since sorted, merge  returned 22, I can now modify this pointer.   And from here, I return a. And when  I return a I'm just returning 20.   And this gets popped off the call stack.   And now, you know we're looking at 16. And we're  saying what should sixteens next pointer be? Well,   16 should point to 20. But it already is pointing  to 20. So we don't really need to do anything.   And so I returned from here 16. And  this gets popped off the call stack.   And again in the same in the same case,  11 is already pointing to 16. So we don't   need to modify anything. But we return 11 from  here and this gets popped off the call stack.   And now we have this question eight was pointing  to 22. But since we returned to 11, we can now   say that eight is going to point to 11. And that  means we modify the pointer, which we've done.   Right, and now we return eight from  this stack frame, a smaller value,   and this gets popped off the call stack.  Now we compare eight and four four was   pointing to 11. But since eight got  returned from the recursive call,   and we take four and we point it to eight now we  return the smaller value From the stack frame.   And what do we do here? Well, what happens  here is we pop this off the call stack,   and four is the smaller value. And so now what  we want is we want one to point to the four node.   And this is our final stack frame. And from  here, we just return one, which is the head node,   which is the smallest value from the two sorted  lists. And from here, this gets popped off the   stack frame. And if we follow these edges, or  these pointers, we noticed one goes to four goes   to eight goes to 11, to 16, to 20, to 22, to  40. And this ends up being our sorted list.   And even for this, we can take a second to  look at the code and understand the call stack.   Alright, so I've gone ahead and I've actually  built a couple linkless. So we have one here.   So one, 513 14, and 550. And we have another  completely independent link lists to 15 130   203 50. And the idea here is how do we take  these and merge them in their sorted order.   And so we take the same code  that we were just looking at,   but now we want to analyze it from the call stacks  perspective. And so I'm going to come over here,   and I'm going to put a breakpoint on the initial  call. And we're going to debug into this function.   So as I dive into this function, you'll notice  the call stack grows. And now we have two lists,   this would be a and b from the example. So we  have one and two. And this corresponds to the   two initial nodes that we have here, two and one.  And what we're doing is we're checking if you know   the first list is null, then we return the second  list. And if the second list is null, we return   the first list. This, of course, does not hold  true yet. So we continue and compare the values.   Since one is less than two, we jump in here  and we do another recursive call. And notice   that we pass in the next value that one  was pointing to, but two stays the same.   And we continue this comparison. Now since five is  greater than two, we go to the second conditional,   and we do a recursive call. And notice  the stack frames are just growing.   They're just growing. And this stack frame is  dependent on the result of this stack frame.   And as I continue through and do these  comparisons, I have another stack frame.   And each subsequent stack frame has a dependency  on the results of whichever one is above it.   And so we keep doing this. And we do this  until we hit one of these base cases.   So I continue through. And  now I've hit a base case,   the value that we have is list one, which is 550.  And I'm just going to return this. And when I   return it, what happens? Well, it's the result of  this function. So I say l to Next is 550. And when   that gets evaluated, I just return here. And you  notice that the stack frames will start shrinking   now because we've hit a base case. And so as I  continue through, and I'm returning these values,   you'll notice that our call stack is shrinking in  size. And we're basically reassigning the pointers   to the next node. And it shrinks and shrinks and  shrinks. And now finally, we get to a point where   the sorted operation is complete. And so if we  look at sorted merge, if we look at this value,   we noticed that the first value is one, the next  value is two 513. And it's now in sorted order. So   we've taken two sorted lists and merge them in  ascending order, which is the expected output.   And this is just an example of taking two linked  lists and showing how the merge operation works   on the call stack. This brings me to one  of my favorite topics, which is trees.   And trees are one of the really fantastic data  structures that work really well with recursion.   And the first question we're going to look at  is inserting a value into a binary search tree.   Now a tree for especially a binary search  tree is going to be a series of nodes and   we're going to basically start from the top  down, we're gonna have a bunch of connections,   and the number of children at most that  we can have from a single notice to   and the least amount of nodes you can have  is zero. And so what we're going to do is   we're going to add a bunch of numbers here  and analyze the properties of this tree.   Notice that everything to the left of the root  node 100 is less than 100. And everything to the   right is greater than 100. Now this property it  turns out holds true recursively. So if we look   at at everything to the left is less than 80.  Everything to the right is greater than eight   But it's interesting because everything  to the right is greater than 80,   but also still less than 100. And so if you  look, the largest value on the subtree of 80,   on the right subtree is 95. And 95 is still less  than 100. And so if you're not familiar with the   properties of binary search trees, I encourage  you to just do a quick, quick look and look at   the rules for that. But this is pretty much  the entire rule set for a binary search tree.   And we're faced with this task and the task  is to add this node, and the node is 108.   And if we look at this value, we basically say,  Okay, I'm going to start at the top of this tree,   and I'm going to figure out where can I place 108.  And I compare I say, is 108, greater than 100,   or less than 100? or equal to 100? And I  say it's greater than, so I go to the right.   And I ask is 108 greater than 120, or  less than less than or equal to 120?   And we see it's less than so I go to  the left sub half. And I asked the   same question is 100, a greater than 110 or  less than 110. And I see it the less than,   and so now I go down here. And I draw connection.  And this is the point where 108 belongs.   And here you can see it's its sale still satisfies  the rule set, right? 108 is greater than 100.   So it satisfies that property, it's less than  120. It's less than 110. So this is the valid   position for 108. And so let's look at the code.  Turns out the code for this is extremely simple.   And like I had said, before we consider the base  case, what's the smallest thing I can pass in?   Well, what if there is no root? Right? What if  this is the first value that we're inserting   into the tree? Well, if that's the case,  then what I do is I just create a new node,   I set the data, and I return that value. Right?  And it turns out, that's a fairly good base case.   Now, if we consider the other case, now we  need to start thinking about, okay, well,   what if I need to do some comparisons? And this  is where I start comparing the data. So let's look   at what's happening. The first  question says, If I hit null,   I've recurse to my end goal, according to the  search tree properties of the binary search tree.   Now remember, binary search tree node, insertions  will always happen at the leaf level. So they   always happen at the end. So if you've done  all your comparisons until the very end,   then you've hit a valid position to insert  the data. Right? And that valid position   would be when you have no more comparisons to  do, which is implied by the head node B, no.   Now the next conditional I look at is  basically saying, Okay, I'm at a node,   and I want to compare it to my current node.  So remember, when we were comparing 108 to 100,   I asked this question I say, is the node i want  to insert greater or less than this value. If it's   greater than I say, head dot right equals, and  then I just make another recursive call. And I   progress to either the left or right half of the  subtree. The other side is just the opposite. So   if I'm less than the current node, then I want  to go down the left hand side of the sub tree.   Now, this final piece right here, is, once I've  done all this work, I've done all the insertions,   I just want to return the original root node.   And that original root node just says basically,  here's the tree that you started with.   Let's look at the call stack.  During this insertion process.   I call insert node. And I'm comparing 100 and  108. And I know 108 is greater than 100. So I   recurse down the right hand side. Now when I get  to this point, I add another stack frame. And I'm   comparing 120 and 108. And I know 108 is now less  than this. So I recurse down the left hand side.   Now here, I'm comparing 108 and 110. Add  another stack frame, I compare these values.   And because it's less than  I go to the left hand side.   And this is interesting, because now  my value that I compare against is no.   And remember, when we look at the code here, no is  just the base case. This is what we consider when   we insert a node. And so I just create that  108 node and I return it right. And so here   is where I would say, okay, new node had died  data equals 108. And I would return this node.   And so from this stack frame, I would actually  return the 108 node and pop this off the stack.   And what I would do here because I was saying  110 dot left, equals whatever that value was.   Now I can draw connection to 108 and just  start returning those values up the stack.   And that's the process of inserting  a value into a binary search tree.   Let's look at another fun problem, which is also  a kind of a fun depth first tree reversal problem.   And the purpose here is to print all the leaf  nodes. So we're looking at the same tree.   But we want to build a function  that prints out all the nodes   that we see here in the order from left  to right. So 30 6080 590-510-8115 and 150.   Now if we think about the recursion for how this  works, mechanically, we start at the root node,   and we go all the way down. And we hit a  leaf node here, so we would print it out.   And what I would do is I would basically  pop off the call stack and go to this node.   But I would immediately go to this nodes, right  subtree. And this would be another leaf node   because it has no children. And I would print  this and recurse up the stack. And then I would   go back up to 80. And now go down, it's right  half. And here, I'll go down its left sub half,   find a leaf node return, go down the right sub  half, find a leaf node return. And this process   just continues. And the order of execution is  really important to understand here. Remember,   if I'm going down the left sub half, I can't  even consider the right sub half until the left   sub half has been fully explored. And that's  part of the property that depth first search   follows. And we'll look at another DFS example  with a graph in just a second. So now I go down,   I recurse down the left sub half, I find the leaf  node I print it, I recurse back up and do this   all the way until I'm basically out of leaf nodes  to find which brings us here. And now we're done.   So here's the code for this. And  we'll look at this to understand   how things get added on the call stack as  well in the code. But again, our base cases,   what happens if we pass in a null value,  that's a pretty good base case, right?   Because then we would just return there's  nothing to print if you just pass a null.   But we also have to think about the  goal, the goal is to print the leaf node.   And so we want to keep recursing  until we hit a leaf node.   And the properties of a node being a leaf node is  that there are no children. And so this is where   we start to ask, okay, if there are no children  to the left, there are no children to the right,   then I can actually evaluate the underlying  value of that node and just return.   Now, if we're not a leaf node, then we need  to recurse down the left sub half of the tree,   which is what we're doing here. And if  we have a right sub half to recurse,   then we go down the right sub half after the  left sub half has been explored. So this allows   us again, this is why we evaluate the left hand  side first. And then we go down the right hand   side recursively. And we do that for all recursive  sub trees up until we're done. So let's look at   the call stack and see how this is executing,  or we're looking at here is the same program. So   we have some code here, print leaves, and it's  the same code that we looked at before. So if   the input root node is null, we just return, we  evaluate the left and right. And this basically   checks if a given node is a belief. And we print  it, and then we pop ourselves off the call stack.   Now if we don't satisfy that, then we go  down the left and right half the subtree.   So using the same code that we looked  at for insertion, I've actually built   up a tree to use. So I have some input with  a bunch of numbers. These are all the numbers   that we were looking at before I believe. And it's  just a random set of numbers. So nothing special.   And we just insert them. So I have a function here  from the previous example called insert node. And   it just builds us a tree for us. And so now what I  want to do is I want to print all the leaves here.   So what I'm going to do is I'm going to add a  breakpoint here. And we're going to debug straight   into it. And the first question I asked is, is the  root node? No. And the answer is no, it's not No.   Right? So this first value. And so what do I do?  Well, I recurse down the entire left half of the   subtree. So remember, it starts at 100. And now  I go down to the left. And since it's not No,   I skip over this. And now I just recurse down the  left half of the tree. And for that node, again,   it's not know. And for that specific node i again  recurse down to the left sub half of the tree.   And I just keep going down the left  half I don't even explore the right half   until this level. Tap has been fully explored.  And you notice we just printed out the first   leaf node, which is 30. And it's only until I  explored that entire sub trees left half, that I   can now go to this other sub trees right half. So  I dive in here. And now I go down the right half.   It turns out that the right half again,  is just another leaf node. And so I can   just print this value. And if I put a breakpoint  here, you'll notice that we just keep printing.   You know, we hit Knowles, we hit  Knowles, and we print the leaves.   And you can see down here, this is the  leaf node evaluation that we're doing.   And the reason I wanted to walk through  this animation is just further emphasize   that when we're thinking about the call stack, I  cannot even consider evaluating this expression   until every recursive call, and every  sub call has been fully evaluated.   Once that holds true, then I can start unraveling  the right sub trees and I work from the bottom up.   And that's the idea for these kind of  DFS traversals that we're dealing with.   This brings us to our last section of algorithms  that we'll be looking at. And we'll look at a   simple example with graphs. And we'll just see  how similar working with graphs is to something   like trees. And we're going to look at a very  popular algorithm known as depth first search.   And the idea is that we start at a node like a,  and we basically say we're searching for a node,   let's say the node we're searching for is H.  And so I start from a and I say, Okay, let me   get all the neighbors and see is this my value,  so a is not my value. So I go to my neighbor,   and I say B, B is not my value, go to C, D is not  my value, but I get all my neighbors. And this is   at an interesting point, because I'm searching  for H, right, which is in the far half. But when   I get my neighbor's, let's say the first neighbor  I get is E. And so I actually have to explore down   the depths of E before I even consider going to H.  And so I go down, and I see E. And now I go to F.   And now I have to look at all of F's  neighbors. So the first neighbor I go to is K.   And if k had neighbors, I would  explore all of those neighbors.   But it doesn't, so I can just pop off. And now I  go to J and back to F. Now I go to I am back to F,   and I kind of recurse backup the call stack.  So now that I've explored all the neighbors of   right, I've explored one neighbor of D, I  only have one unvisited neighbor left for   me to explore. And it's g so I go to G and I  say is this the value that I'm looking for?   And it's not. So I look at the neighbors of G  and I say is this the value I'm looking for?   And it is. And this is the idea of depth first  search. And so we're going to look at a piece   of code and walk through it line by line and see  how something like this would work recursively.   We look at a piece of code like  this, and we have the input node.   And to avoid cycles, which can happen in graphs,  which is different than trees will keep a set   of visited values. So we never want to visit the  same node more than once. And so that's what the   visited set is going to do. And the goal integer  here is just the node that we're looking for.   So I know the graph we were just looking at was  letters, but we're going to assume that we're   working with a graph with integer values. And we  ask our base case questions again. So think about   the smallest thing you could pass in if you pass  in an empty node, a normal node, then there's no   way you could ever find the goal, because the  goal is an integer value. And so here, we would   just return false. So this is a base case. The  other base case is if we've actually found the   node. So let's assume the node you pass in is the  gold node, then here we found the node. And so we   would just return true here, and this would imply  that we found the value that we're looking for.   But let's assume that we haven't found the  value, just like the example. The first thing   that we need to do is we need to aggregate all  of the neighbors from a given node. And so we're   going to assume the node API has a function call  that allows you to aggregate all the neighbors.   And the first question we ask is have  we have we visited this node before?   Remember, we want to avoid cycles. And so we  never want to visit a node more than once.   And so if it's been visited, we just ignore it  and we continue. But if it hasn't been visited,   then we add it to the visited set. And we start  the exploration. And we ask this question,   have we found the node that we've been working  with with this particular neighbor. If we've   found the solution, then we can just return  right away. Now if we haven't found the solution   from this particular neighbor, then we'll continue  recursing through the rest of the neighbors   And it's at that point, if we found it, we would  return true. And this brings us to our last base   case, let's assume that we've traversed all of  the neighbors. And we never found the solution.   That would mean that the goal value that we're  looking for was just never in the graph to begin   with. And this brings us to our final base case  of just returning false. And this is a way to say,   hey, I've searched for this node, but it  didn't exist in the graph. It's false,   it did not show up in the search. And  that's where we can terminate the algorithm.   I think it's important to spend just a little  bit of time talking about optimizations that   you can make when using recursion. And  the first that we're going to look at is   memoir, zation. And caching, which we  talked about briefly when writing Fibonacci.   And the question that we're trying to answer here  is how can we speed up our program by pretty much   storing things that we've already re computed or  computed for the first time. So if I've computed   some very expensive operation, and I have to re  compute it again, in a subsequent recursive call,   I want to check to see if I've done  that work already. And if I have,   I'm just going to return the result instead  of re computing that operation again.   And the best way to kind of conceptualize This  is looking at the Fibonacci tree sequence.   Here, we see that I'm doing F of three twice,  and I'm re computing F of two, three times.   And the question is, why can't I just compute  f of two once, and then just reuse that value,   so I don't grow these sub trees. And it  turns out, you can do that really easily.   Let's look at the modified Fibonacci code. Notice  I have a hashmap here and hashmaps have this   property, such that we can retrieve values from  the map in constant time access, so big O of one.   And the reason is because the data that we  put into the hashmap is hashed. And so we have   constant time access to the memory addresses  in which that data is stored. And so when I   call the Fibonacci function, the first thing  I always do is I say, Hey, does the cache have   this value. And you can also notice that I've pre  populated the cache with the base cases as well.   Now in the instance, that the cache does not have  the value, I still go about the recursive call,   as I had done previously. But I ensure that  once that recursive call has been evaluated,   I put the result into the cache.  And then I return my result.   And this is a really key component because  the cache is global, right. And so all the   subsequent recursive calls are putting their  results in the cache. And as things get added,   and popped off the stack, I can just keep  cross referencing the cache to see if I'm   doing redundant work or not. And that saves me  a lot of computational power. So that's the idea   of memorization and caching. And you can do a  lot of this in a lot of different situations.   The next optimization I want to just discuss is  this idea of tail call recursion optimization. And   people usually refer to this as you know,  tail call optimization, or tail recursion.   And the idea here is that basically, the compiler  in certain languages, especially functional   programming languages, will optimize the number of  stack frames that get added to the call stack to   basically remove this idea of stack  overflows in a lot of scenarios.   Now, the way that the compiler does this analysis  is it really looks at the last function call.   And it has to be a recursive call. And we'll  look at an idea of comparing these two things.   And there was actually a response on the  computer science Stack Exchange by this user,   and he gave two examples. The first is  this simple recursive factorial function.   And notice that the final return value is a  value multiplied by a recursive call. Now,   this is not tail recursive, because the return  value is not just the recursive call. And so in   order for this to be tail recursive optimized,  we need to modify to look something like this.   Now, if you spend a second to look at this  code, you'll realize that functionally it's   doing the exact same thing. But we've had  to modify the parameters to get to a point   where we can build a function that's tail call  optimized. And the reason it's optimized with this   tail call recursive property is because the final  return value is in itself just a recursive call.   And it turns out in certain operating in certain  languages, in their compilers, they've implemented   some optimization techniques to exploit  this property and reduce stack overflows.   Now, I just want to make a couple notes  on this. The rule of thumb is to always   make your recursive calls be the last instruction  Nothing gets added to it, just the recursive call.   And the other thing to consider is that this is  supported mostly by functional languages. But it's   not inherent in languages like Python and Java and  even JavaScript. There are certain browsers for   ies 2016. They do support tail call recursion  optimization for things like JavaScript,   but it's not supported across the board. And so  as you're considering this optimization technique,   think about what language you're using. And think  about, you know, the compiler that you're using   and where your code is being executed to see if  this sort of optimization is supported or not.   So this brings us to our final slide. What's  next? And when we ask this question, it's really   to consider like how far do you want to go in  your journey of recursion. And that brings us to   another video that we'll have coming out, known  as algorithmic mental models for backtracking.   And this is the entire notion that we basically  solve even more complex problems by looking at a   decision space and recursively going through  and seeing if our decision was good or bad,   and backtracking to see if we can  solve the solution in a different way.   And that's the next phase that I think  is important for understanding recursion.   So thank you guys so much for watching. Be sure  to check out my channel youtube.com slash the   symbol engineer and connect with me on youtube  or Twitter or LinkedIn. And I'd be happy to talk   with you and answer any questions you may have.  So thanks, guys for watching and have a good day.
Info
Channel: freeCodeCamp.org
Views: 134,410
Rating: 4.9700398 out of 5
Keywords:
Id: IJDJ0kBx2LM
Channel Id: undefined
Length: 111min 36sec (6696 seconds)
Published: Mon Jul 19 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.