< Return to Video

Lecture 8 | Programming Abstractions (Stanford)

  • 0:00 - 0:08
  • 0:08 - 0:11
  • 0:11 - 0:15
    This presentation is delivered by the Stanford Center for Professional Development.
  • 0:15 - 0:24
  • 0:24 - 0:26
    Good afternoon.
  • 0:26 - 0:29
    You guys are talkative.
  • 0:29 - 0:32
    So we talked just a little bit about the vocabulary of recursion at the
  • 0:32 - 0:36
    end of Friday. It was very rushed for time, so I'm
  • 0:36 - 0:40
    just gonna repeat some of those words to get us thinking about what the
  • 0:40 - 0:42
    context for solving problems recursively looks like. And then we're gonna go along and
  • 0:42 - 0:45
    do a lot of examples [cuts out].
  • 0:45 - 0:48
    So the idea is that a recursive function is one that calls itself.
  • 0:48 - 0:51
    That's really all it means
  • 0:51 - 0:54
    at the most trivial level. It says that in the context of writing a
  • 0:54 - 0:56
    function binky, it's gonna make a call
  • 0:56 - 1:00
    or one or more calls to binky itself past
  • 1:00 - 1:03
    an argument as part of solving or doing some task.
  • 1:03 - 1:04
    The idea is that
  • 1:04 - 1:08
    we're using that because the problem itself has some self-similarity where the
  • 1:08 - 1:11
    answer I was seeking - so the idea of surveying the whole campus can actually be thought of as well if
  • 1:11 - 1:14
    I could get somebody to survey this part of campus and this part of campus,
  • 1:14 - 1:17
    somebody to survey all the freshmen, somebody to [cuts out]
  • 1:17 - 1:18
    and whatnot,
  • 1:18 - 1:21
    that those in a sense are - those surveys are just smaller instances of the same kind
  • 1:21 - 1:24
    of problem I was originally trying to solve. And
  • 1:24 - 1:26
    if I could recursively
  • 1:26 - 1:30
    delegate those things out, and then they themselves may in turn delegate even
  • 1:30 - 1:32
    further to smaller but same
  • 1:32 - 1:36
    structured problems to where we could eventually get to something so simple - in
  • 1:36 - 1:40
    that case of asking ten people for their input
  • 1:40 - 1:44
    that don't require any further decomposition - we will have worked our way to this base
  • 1:44 - 1:48
    case that then we can gather back up all the results and solve the whole thing.
  • 1:48 - 1:51
    This is gonna feel very mysterious at first,
  • 1:51 - 1:53
    and some of the examples I give you'll say I have
  • 1:53 - 1:56
    really easy alternatives other than recursion, so it's not gonna seem worth
  • 1:56 - 1:58
    the pain of trying to get your head around it.
  • 1:58 - 2:03
    But eventually, we're gonna work our way up to problems where recursion really is the right solution,
  • 2:03 - 2:06
    and there are no other alternatives that are obvious or simple
  • 2:06 - 2:07
    to do.
  • 2:07 - 2:07
    So the
  • 2:07 - 2:11
    idea throughout this week is actually just a lot of practice. Telling you
  • 2:11 - 2:13
    what the terms mean I think is not actually gonna help you understand it. I think what you
  • 2:13 - 2:18
    need to see is examples. So I'm gonna be doing four or five examples today, four or five
  • 2:18 - 2:20
    examples on Wednesday, and four or five examples on Friday
  • 2:20 - 2:22
    that each kind of
  • 2:22 - 2:25
    build on each other, kind of take the ideas
  • 2:25 - 2:27
    and get a little more sophisticated.
  • 2:27 - 2:28
    But
  • 2:28 - 2:31
    by the end of the week, I'm hoping that you're gonna start to see these patterns, and realize
  • 2:31 - 2:35
    that in some sense the recursive solutions tend to be more alike than different.
  • 2:35 - 2:37
    Once you have your head around how to solve
  • 2:37 - 2:41
    one type of problem, you may very well be able to take the exact same technique
  • 2:41 - 2:43
    and solve several other problems that
  • 2:43 - 2:46
    may sound different at first glance, but in the end, the recursive structure looks the
  • 2:46 - 2:47
    same.
  • 2:47 - 2:54
    So I'd say just hold the discomfort a little bit, and wait to see as
  • 2:54 - 2:57
    we keep working which example may be the one that kind of sticks out for you to
  • 2:57 - 2:59
    help you get it through.
  • 2:59 - 3:02
    So we're gonna start with things that fit in the category of
  • 3:02 - 3:03
    functional recursion.
  • 3:03 - 3:05
    The functional in this case just says that
  • 3:05 - 3:09
    you're writing functions that return some non-void thing, an integer, a string,
  • 3:09 - 3:10
    some
  • 3:10 - 3:12
    vector of results, or whatever that is,
  • 3:12 - 3:14
    and that's all it means to be a functional recursion. It's a recursive
  • 3:14 - 3:18
    function that has a result to it. And because of the recursive nature, it's gonna
  • 3:18 - 3:22
    say that the outer problem's result, the answer to the larger problem is gonna be
  • 3:22 - 3:26
    based on making calls, one or more calls to the function itself to get the answer
  • 3:26 - 3:30
    to a smaller problem, and then adding them, multiplying them, comparing them to decide
  • 3:30 - 3:33
    how to formulate the larger answer.
  • 3:33 - 3:35
    All recursive
  • 3:35 - 3:35
    code
  • 3:35 - 3:38
    follows the same decomposition
  • 3:38 - 3:42
    into two cases. Sometimes there's some subdivisions within there, but two general
  • 3:42 - 3:43
    cases.
  • 3:43 - 3:45
    The first thing - this base case.
  • 3:45 - 3:48
    That's something that the recursion eventually has to stop.
  • 3:48 - 3:52
    We keep saying take the task and break it down, make it a little smaller,
  • 3:52 - 3:55
    but at some point we really have to stop doing that. We can't
  • 3:55 - 3:57
    go on infinitely.
  • 3:57 - 4:00
    There has to be some base case, the simplest possible version of the problem that
  • 4:00 - 4:05
    you can directly solve. You don't need to make any further recursive calls. So the idea
  • 4:05 - 4:10
    is that it kinda bottoms out there, and then allows the recursion to kind of
  • 4:10 - 4:10
    unwind.
  • 4:10 - 4:12
    The recursive cases,
  • 4:12 - 4:15
    and there may be one or more of these, are the cases where it's not that simple, that the
  • 4:15 - 4:20
    answer isn't directly solvable, but if you had the answer to a smaller, simpler
  • 4:20 - 4:20
    version, you
  • 4:20 - 4:24
    would be able to assemble that answer you're looking for
  • 4:24 - 4:27
    using that information from the recursive call.
  • 4:27 - 4:31
    So that's the kind of structure that they all look like. If I'm
  • 4:31 - 4:34
    at the base case, then do the base case and return it.
  • 4:34 - 4:37
    Otherwise, make some recursive calls, and
  • 4:37 - 4:43
    use that to return a result from this iteration.
  • 4:43 - 4:45
    So let's first look at something that
  • 4:45 - 4:48
    - the first couple examples that I'm gonna show you actually are gonna be so
  • 4:48 - 4:52
    easy that in some sense they're almost gonna be a little bit counterproductive because they're gonna
  • 4:52 - 4:54
    teach you that recursion lets you do things that you already
  • 4:54 - 4:57
    know how to do. And then I'm gonna work my way up to ones that actually
  • 4:57 - 4:59
    get beyond that.
  • 4:59 - 5:02
    But let's look at first the raise to power.
  • 5:02 - 5:05
    C++ has no built-in exponentiation operator. There's nothing that raises
  • 5:05 - 5:07
    a base to a particular exponent
  • 5:07 - 5:08
    in the operator set.
  • 5:08 - 5:12
    So if you want it, you need to write it, or you can use - there's a math library called
  • 5:12 - 5:15
    pow for raise to power. We're gonna run our own version of it
  • 5:15 - 5:17
    because it's gonna give us some practice thing about this.
  • 5:17 - 5:20
    The first one I'm gonna show you is one that should feel very familiar and
  • 5:20 - 5:24
    very intuitive, which is using an iterative formulation. If I'm
  • 5:24 - 5:27
    trying to raise the base to the exponent, then that's really simply
  • 5:27 - 5:30
    multiplying base by itself exponent times.
  • 5:30 - 5:31
    So this one
  • 5:31 - 5:32
    uses a for loop
  • 5:32 - 5:37
    and does so. It starts the result at one, and for
  • 5:37 - 5:40
    iterations through the number of exponents keeps
  • 5:40 - 5:46
    multiplying to get there. So
  • 5:46 - 5:48
    that one's fine, and it will perfectly work.
  • 5:48 - 5:51
    I'm gonna show you this alternative way
  • 5:51 - 5:54
    that starts you thinking about what it means to divide a problem up in a
  • 5:54 - 5:56
    recursive strategy.
  • 5:56 - 6:00
    Base to the exponent - I wanna raise five to the tenth power. If
  • 6:00 - 6:05
    I had around some delegate, some
  • 6:05 - 6:08
    clone of myself that I could dispatch to solve
  • 6:08 - 6:11
    the slightly smaller problem of computing five to the ninth power,
  • 6:11 - 6:15
    then all I would need to do is take that answer and multiply it by one more five, and I'd get
  • 6:15 - 6:17
    five to the tenth.
  • 6:17 - 6:19
    Okay.
  • 6:19 - 6:22
    If I write code
  • 6:22 - 6:25
    that's based on that,
  • 6:25 - 6:28
    then I end up with something here - and I'm gonna
  • 6:28 - 6:31
    let these two things go through to show us
  • 6:31 - 6:32
    that
  • 6:32 - 6:34
    to compute the answer to five
  • 6:34 - 6:36
    to the tenth power,
  • 6:36 - 6:38
    what I really need is the answer to five
  • 6:38 - 6:42
    to the ninth power, which I do by making a recursive call to the same function I'm in
  • 6:42 - 6:43
    the middle of writing.
  • 6:43 - 6:45
    So this is raise that I'm
  • 6:45 - 6:49
    defining, and in the body of it, it makes a call to raise. That's what is the
  • 6:49 - 6:51
    mark of a recursive function here. I
  • 6:51 - 6:55
    pass slightly different arguments. In this case, one smaller exponent
  • 6:55 - 6:57
    which is getting a little bit closer
  • 6:57 - 7:00
    to that simplest possible case
  • 7:00 - 7:02
    that we will eventually terminate at where
  • 7:02 - 7:07
    we can say I don't need to further dispatch any delegates and any clones out there
  • 7:07 - 7:08
    to do the work, but if
  • 7:08 - 7:11
    the exponent I'm raising it to is zero, by
  • 7:11 - 7:16
    definition anything raised to the exponent of zero is one, I could just stop there.
  • 7:16 - 7:19
    So when computing five to the tenth, we're gonna
  • 7:19 - 7:21
    see some recursion at work. Let me
  • 7:21 - 7:24
    take this code into the compiler
  • 7:24 - 7:27
  • 7:27 - 7:31
    so that we can see a little bit about how this actually works
  • 7:31 - 7:32
    in terms of that.
  • 7:32 - 7:35
    So that's exactly the code I have there, but
  • 7:35 - 7:42
    I can say
  • 7:42 - 7:43
    something that I
  • 7:43 - 7:46
    know the answer to. How about that?
  • 7:46 - 7:49
  • 7:49 - 7:51
    First, we'll take a look at it
  • 7:51 - 7:55
    doing its work, so five times five times five should be 125.
  • 7:55 - 7:57
    So we can test a couple
  • 7:57 - 7:58
    little
  • 7:58 - 8:01
    values while we're at it. Two the sixth power, that should be
  • 8:01 - 8:07
    64, just to see a couple of the cases, just to feel good about what's going on.
  • 8:07 - 8:10
    And then raising say 23 to the zero power
  • 8:10 - 8:11
    should be one
  • 8:11 - 8:14
    as anything raised to the zero power should be.
  • 8:14 - 8:16
    So a little bit of spot testing
  • 8:16 - 8:18
    to feel good about what's going on.
  • 8:18 - 8:22
    Now I'm gonna go back to this idea like two to the sixth.
  • 8:22 - 8:24
    And I'm gonna set a breakpoint here.
  • 8:24 - 8:25
    Get my breakpoint out.
  • 8:25 - 8:36
    And I'm gonna run this guy in the debugger.
  • 8:36 - 8:39
    Takes a little bit longer to get the debugger up and running,
  • 8:39 - 8:44
    so I'll have to make a little padder up while we're going here.
  • 8:44 - 8:47
    And then it tells me right now it's breaking on raise, and
  • 8:47 - 8:53
    I can look around in the debugger. This is a - did not
  • 8:53 - 8:56
    pick up my compilation? I think it did not. I
  • 8:56 - 8:59
    must not have saved it right before I did it because it's actually got the base is 23 and the
  • 8:59 - 9:00
    exponent is zero. It turns
  • 9:00 - 9:06
    out I don't wanna see that case, so I'm gonna go back and
  • 9:06 - 9:08
    try again. I
  • 9:08 - 9:16
    wanna see it -
  • 9:16 - 9:24
    no, I did not.
  • 9:24 - 9:28
    And I'm just interested in knowing a little bit about the mechanics of what's gonna happen in a
  • 9:28 - 9:29
    recursive situation.
  • 9:29 - 9:33
    If I look at the first time that I hit my breakpoint,
  • 9:33 - 9:35
    then I'll see that there's a little bit of the beginnings of the
  • 9:35 - 9:39
    student main, some stuff behind it. There's a little bit of magic underneath your
  • 9:39 - 9:43
    stack that you don't really need to know about, but starting from main it went into raise,
  • 9:43 - 9:46
    and the arguments it has there is the base is two, the exponent is six.
  • 9:46 - 9:50
    If I continue from here, then you'll notice the stack frame got one deeper.
  • 9:50 - 9:53
    There's actually another indication of raise, and in fact, they're both active at the
  • 9:53 - 9:54
    same time.
  • 9:54 - 9:57
    The previous raise that was trying to compute two to the sixth
  • 9:57 - 10:01
    is kind of in stasis back there waiting for the answer to come back from
  • 10:01 - 10:05
    this version, which is looking to raise two to the fifth power. I continue again,
  • 10:05 - 10:08
    I get two to the fourth. I keep doing this. I'm gonna see these guys kinda stack up,
  • 10:08 - 10:12
    each one of those kind of waiting for the delegate or the clone
  • 10:12 - 10:17
    to come back with that answer, so that then it can do its further work
  • 10:17 - 10:19
    incorporating that result to compute the thing it needed to do.
  • 10:19 - 10:22
    I get down here to raising
  • 10:22 - 10:24
    two to the first power,
  • 10:24 - 10:26
    and then finally I get to two to the zero,
  • 10:26 - 10:29
    so now I've got these eight or so stacked frames,
  • 10:29 - 10:30
    six up there.
  • 10:30 - 10:32
    This one, if I step
  • 10:32 - 10:34
    from here,
  • 10:34 - 10:37
    it's gonna hit the base case of returning one,
  • 10:37 - 10:39
    and then
  • 10:39 - 10:43
    we will end up kind of working our way back out. So now, we are
  • 10:43 - 10:46
    at the end of the two to the one case
  • 10:46 - 10:49
    that's using the answer it got from the other one, multiplying it by two.
  • 10:49 - 10:53
    Now I'm at the two to the two case, and so each of them's kind of unfolding in the stack is
  • 10:53 - 10:55
    what's called unwinding. It's popping back off
  • 10:55 - 10:57
    the stack frames that are there
  • 10:57 - 11:01
    and kind of revisiting them, each passing up the information it got back,
  • 11:01 - 11:04
    and eventually telling us that
  • 11:04 - 11:08
    the answer was 64, so I will let that go.
  • 11:08 - 11:15
  • 11:15 - 11:17
    So the idea that
  • 11:17 - 11:21
    all of those stack frames kind of exist at the same time - they're all being maintained
  • 11:21 - 11:24
    independently, so the idea that the compiler isn't confused by the idea that raise is
  • 11:24 - 11:26
    invoking raise which is invoking raise,
  • 11:26 - 11:30
    that each of the raise stack frames is distinct from the other ones, so the
  • 11:30 - 11:33
    indications are actually kept separate. So one had two to the sixth, the next one had two to
  • 11:33 - 11:35
    the fifth, and so on.
  • 11:35 - 11:36
    And then eventually
  • 11:36 - 11:37
    we
  • 11:37 - 11:40
    need to make some progress toward that base case, so that we can then
  • 11:40 - 11:42
    stop that recursion
  • 11:42 - 11:43
    and unwind.
  • 11:43 - 11:47
    Let me actually show you something while I'm here, which is one thing that's
  • 11:47 - 11:48
    a pretty common mistake
  • 11:48 - 11:51
    to make early on in a recursion is to somehow fail to make progress toward
  • 11:51 - 11:53
    that base case
  • 11:53 - 11:57
    or to
  • 11:57 - 11:58
    -
  • 11:58 - 12:01
    not all cases make it to the base case. For example, if
  • 12:01 - 12:03
    I did something where
  • 12:03 - 12:06
    I forgot to
  • 12:06 - 12:07
    subtract one [cuts out],
  • 12:07 - 12:10
    and I said oh yeah, I need to [cuts out]. In this case, I'm passing it exactly the
  • 12:10 - 12:13
    same arguments I got.
  • 12:13 - 12:16
    If I do this and I run this guy,
  • 12:16 - 12:19
    then what's gonna happen is it's gonna go two to the sixth, two to the sixth, two to the sixth,
  • 12:19 - 12:20
    and I'm gonna
  • 12:20 - 12:21
    let go of
  • 12:21 - 12:23
    this breakpoint here because
  • 12:23 - 12:30
    I don't really wanna watch it all happening.
  • 12:30 - 12:31
    And there it goes.
  • 12:31 - 12:35
    Loading 6,493 stack frames, zero percent completed, so
  • 12:35 - 12:38
    that's gonna take a while as you
  • 12:38 - 12:41
    can imaging. And usually, once you see this error message, it's your clue to say I
  • 12:41 - 12:45
    can cancel, I know what happened. The only reason I should've
  • 12:45 - 12:46
    gotten
  • 12:46 - 12:50
    6,500 stack frames loaded up is because I made a mistake that
  • 12:50 - 12:53
    caused the stack to totally overflow.
  • 12:53 - 12:57
    So the behavior in C++ or C is that when you have
  • 12:57 - 13:01
    so many of those stack frames, eventually the space that's been allocated or set
  • 13:01 - 13:03
    aside for the function stack will be exhausted.
  • 13:03 - 13:05
    It will use all the space it has,
  • 13:05 - 13:09
    and run up against a boundary, and typically report it in some way that
  • 13:09 - 13:13
    suggests - sometimes you'll see stack overflow, stack out of memory error.
  • 13:13 - 13:14
  • 13:14 - 13:15
    In this case, it's showing you the
  • 13:15 - 13:17
    7,000 stack frames that
  • 13:17 - 13:20
    are behind you, and then if you were to examine them you would see they all have exactly the
  • 13:20 - 13:24
    same argument, so they weren't getting anywhere.
  • 13:24 - 13:27
    I'm not gonna actually let it do that because I'm
  • 13:27 - 13:28
    too impatient.
  • 13:28 - 13:29
    Let
  • 13:29 - 13:30
    me fix this code while I'm here.
  • 13:30 - 13:33
    But other things like even this code that actually looks correct for
  • 13:33 - 13:39
    some situations also has a subtle bug in it. Even if
  • 13:39 - 13:40
    I fixed this, which is
  • 13:40 - 13:44
    that, right now it assumed that the exponent is positive,
  • 13:44 - 13:46
    that it's some number
  • 13:46 - 13:47
  • 13:47 - 13:49
    that I can subtract my way down to zero.
  • 13:49 - 13:53
    If I actually miscalled raise, and I gave it a negative exponent, it would
  • 13:53 - 13:57
    go into infinite recursion as well. If you started it
  • 13:57 - 14:00
    at ten to the negative first power, it would go negative first, negative second, negative
  • 14:00 - 14:02
    third.
  • 14:02 - 14:05
    6,500 stack frames later, we'd run out of space.
  • 14:05 - 14:08
    In this case, since we're only intending to handle those positive powers, we
  • 14:08 - 14:12
    could just put an error that was like if the exponent is less than zero then
  • 14:12 - 14:14
    raise an error and don't even
  • 14:14 - 14:21
    try to do anything with it. Okay. So let
  • 14:21 - 14:25
    me show you a slightly different way of doing this that's also recursive,
  • 14:25 - 14:28
    but that actually gets the answer a little bit more efficiently.
  • 14:28 - 14:31
    This is a different way of dividing it up, but still using a
  • 14:31 - 14:36
    recursive strategy which is that if I'm trying to compute five to the tenth power,
  • 14:36 - 14:40
    but if I have the answer not of five to ninth power, but instead I have the
  • 14:40 - 14:42
    answer of five to the fifth power,
  • 14:42 - 14:44
    and then I multiply that by itself,
  • 14:44 - 14:48
    I would get that five to the tenth power that I seek.
  • 14:48 - 14:51
    And then there's a little bit of a case though of what if the power I was trying to
  • 14:51 - 14:54
    get was odd, if I was trying to raise it to the eleventh power, I could compute the
  • 14:54 - 14:58
    half power which get me to the fifth - multiplied by the fifth, but then I need
  • 14:58 - 14:59
    one more
  • 14:59 - 15:04
    base multiplied in there to make up for that half. Okay.
  • 15:04 - 15:05
    And so I can write
  • 15:05 - 15:08
    another recursive formulation here. Same
  • 15:08 - 15:11
    sort of base case about
  • 15:11 - 15:14
    detecting when we've gotten down, but then in this case the recursive call we make is
  • 15:14 - 15:17
    to base of the exponent divided by two,
  • 15:17 - 15:19
    and then we use it with an
  • 15:19 - 15:23
    if else whether the exponent was even or odd, it decided whether to just square
  • 15:23 - 15:25
    that number or whether to square it and
  • 15:25 - 15:28
    tack in another power of the base while we're at it.
  • 15:28 - 15:31
    So this one is gonna be much more quick about getting down to it,
  • 15:31 - 15:34
    whereas the one we saw was gonna put one stack frame down for every
  • 15:34 - 15:35
  • 15:35 - 15:39
    successive exponent power. So if you wanted to raise something to the 10th, or 20th,
  • 15:39 - 15:41
    or 30th power, then you were
  • 15:41 - 15:43
    giving yourself 10, 20, 30 stack frames.
  • 15:43 - 15:46
    Something like 30 stack frames is not really something to be worried
  • 15:46 - 15:48
    about, but if you were really trying to make this work on much larger
  • 15:48 - 15:49
    numbers,
  • 15:49 - 15:52
    which would require some other work because exponent
  • 15:52 - 15:55
    is a very rapidly growing operator and would overflow your integers quickly,
  • 15:55 - 15:57
    but this way
  • 15:57 - 16:00
    very quickly divides in half. So it goes from a power of 100, to a power of
  • 16:00 - 16:05
    50, to 25, to 12, to 6, to 3, to 2, to 1, so
  • 16:05 - 16:08
    that dividing in half is a much quicker way to work our way down to that base case
  • 16:08 - 16:12
    and get our answer back, and we're doing a lot fewer calculations
  • 16:12 - 16:14
    than all those multiplies,
  • 16:14 - 16:16
    one per
  • 16:16 - 16:19
    base. So just a little
  • 16:19 - 16:21
    diversion.
  • 16:21 - 16:22
    So let me tell you something,
  • 16:22 - 16:23
    just a little bit about
  • 16:23 - 16:27
    kind of style as it applies to recursion.
  • 16:27 - 16:28
    Recursion is
  • 16:28 - 16:34
    really best when you can express it in the most kind of direct, clear, and
  • 16:34 - 16:36
    simple code.
  • 16:36 - 16:38
    It's hard enough to get your head around a recursive formulation
  • 16:38 - 16:42
    without complicating it by having a bunch of extraneous parts where
  • 16:42 - 16:43
    you're
  • 16:43 - 16:47
    doing more work than necessary, or redundantly handling certain things. And
  • 16:47 - 16:49
    one thing that's actually very easy to fall in the trap of
  • 16:49 - 16:52
    is thinking about there's lots of other base cases you might be able to
  • 16:52 - 16:56
    easily handle, so why not just go ahead and call out for them,
  • 16:56 - 16:57
    test for
  • 16:57 - 16:59
    - you're at the base case. You're close to the base case.
  • 16:59 - 17:02
    Checking before you might make a recursive call, if you're gonna hit the base case when you
  • 17:02 - 17:08
    make that call, then why make the call. I'll just anticipate and get the answer it
  • 17:08 - 17:09
    would've returned anyway.
  • 17:09 - 17:13
    It can lead to code that looks a little bit like this before you're done. If the
  • 17:13 - 17:16
    exponent's zero, that's one. If the exponent's one, then I can just
  • 17:16 - 17:19
    return the base. If it's two, then I can just multiply the base by itself. If it's
  • 17:19 - 17:20
    three, I can start doing this.
  • 17:20 - 17:22
    Certainly, you
  • 17:22 - 17:25
    can follow this to the point of absurdity, and even for some of the simple
  • 17:25 - 17:28
    cases, it might seem like you're saving yourself a little bit of extra time. You're like
  • 17:28 - 17:31
    why go back around and let it make another recursive call. I could stop it right
  • 17:31 - 17:33
    here. It's easy to know that answer.
  • 17:33 - 17:36
    But as you do this, it complicates the code. It's a little harder to read. It's
  • 17:36 - 17:38
    a little harder to
  • 17:38 - 17:39
    debug.
  • 17:39 - 17:43
    Really, the expense of making that one extra call, two extra calls
  • 17:43 - 17:46
    is not the thing to be worried about.
  • 17:46 - 17:50
    What we really want is the cleanest code that expresses what we do, has the
  • 17:50 - 17:53
    simplest cases to test, and to examine, and to follow,
  • 17:53 - 17:56
    and not muck it up with things that in
  • 17:56 - 17:57
    effect
  • 17:57 - 18:01
    don't change the computational power of this but just introduce the opportunity for error.
  • 18:01 - 18:02
    Instead of a
  • 18:02 - 18:05
    multiply here, I accidentally put a plus.
  • 18:05 - 18:07
    It might be easy to overlook it
  • 18:07 - 18:08
    and not realize what I've done,
  • 18:08 - 18:10
    and then
  • 18:10 - 18:14
    end up computing the wrong answer when it gets to the base case of two. In fact, if
  • 18:14 - 18:15
    you do it this
  • 18:15 - 18:17
    way, most things would stop at three,
  • 18:17 - 18:20
    but these would suddenly become kind of their own special cases that were only
  • 18:20 - 18:25
    hit if you directly called them with the two. The recursive cases will all stop earlier. It
  • 18:25 - 18:28
    just complicates your testing pass now because not everything is going through
  • 18:28 - 18:31
    the same code.
  • 18:31 - 18:34
    So we call this arm's length recursion. I put a big X on that
  • 18:34 - 18:35
    looking ahead,
  • 18:35 - 18:36
    testing before you get there.
  • 18:36 - 18:41
    Just let the code fall through.
  • 18:41 - 18:43
    So
  • 18:43 - 18:45
    Dan, he's not here today,
  • 18:45 - 18:48
    but he talked to me at the end of Friday's class, and it made
  • 18:48 - 18:49
    me
  • 18:49 - 18:53
    wanna actually just give you a little bit of
  • 18:53 - 18:54
    insight into
  • 18:54 - 18:57
    recursion as it relates to
  • 18:57 - 18:58
  • 18:58 - 19:02
    efficiency. Recursion by itself, just the idea of applying a recursive technique to solving a
  • 19:02 - 19:06
    problem, does not give you any guarantee that it's gonna be the best solution, the
  • 19:06 - 19:09
    most efficient solution, or the fact that it's gonna give you a very inefficient solution.
  • 19:09 - 19:11
    Sometimes people have kind of
  • 19:11 - 19:11
    a
  • 19:11 - 19:15
    bad rap on recursion. It's like recursion will definitely be inefficient.
  • 19:15 - 19:17
    That actually is not guaranteed.
  • 19:17 - 19:21
    Recursion often requires exactly the same resources as the iterative approach.
  • 19:21 - 19:24
    It takes the same amount of effort. Surveying the campus - if you're gonna
  • 19:24 - 19:27
    survey the 10,000 people on campus and get everybody's information back,
  • 19:27 - 19:30
    whether you're doing it divide and conquer, or whether you're sitting out there in White
  • 19:30 - 19:30
    Plaza
  • 19:30 - 19:34
    asking each person one by one, in the end 10,000 people got survey.
  • 19:34 - 19:36
    The recursion is not
  • 19:36 - 19:39
    part of what made that longer or shorter. It might actually depending on how you
  • 19:39 - 19:42
    have resources work out better. Like if you could get a bunch of people in parallel
  • 19:42 - 19:46
    surveying, you might end up completing the whole thing in less time, but it required
  • 19:46 - 19:49
    more people, and more clipboards, and more paper
  • 19:49 - 19:52
    while the process is ongoing than me standing there with one piece of paper and a
  • 19:52 - 19:57
    clipboard. But then again, it took lots of my time to do it.
  • 19:57 - 20:00
    So in many situations, it's really no better or no worse
  • 20:00 - 20:02
    than the alternative. It makes a little bit of some tradeoffs of where the time is
  • 20:02 - 20:03
    spent.
  • 20:03 - 20:06
    There are situations though where recursion can actually make something that was
  • 20:06 - 20:09
    efficient inefficient. There are situations where it can take something that was inefficient and
  • 20:09 - 20:11
    make it efficient. So
  • 20:11 - 20:15
    it's more of a case-by-case basis to decide whether recursion is the
  • 20:15 - 20:18
    right tool if efficiency is one of your primary concerns.
  • 20:18 - 20:21
    I will say that for problems with simple iterative solutions
  • 20:21 - 20:24
    that operate relatively efficiently, iteration
  • 20:24 - 20:26
    is probably the best way to solve it.
  • 20:26 - 20:27
    So like
  • 20:27 - 20:31
    the raise to power, yeah you could certainly do that
  • 20:31 - 20:32
    iteratively. There's not
  • 20:32 - 20:35
    some huge advantage that recursion is offering. I'm using them here because they're
  • 20:35 - 20:37
    simple enough to get our head around.
  • 20:37 - 20:39
    We're gonna work our way toward problems where
  • 20:39 - 20:42
    we're gonna find things that require recursion, which
  • 20:42 - 20:45
    is kind of one of the third points I put in. Why do we learn recursion? What's recursion gonna be good
  • 20:45 - 20:48
    for? First off, recursion is awesome.
  • 20:48 - 20:52
    There are some problems that you just can't solve using anything but recursion,
  • 20:52 - 20:55
    so that the alternatives like I'll just write it iteratively won't work.
  • 20:55 - 20:59
    You'll try, but you'll fail. They inherently have a recursive
  • 20:59 - 20:59
    structure to them
  • 20:59 - 21:02
    where recursion is the right tool for the job.
  • 21:02 - 21:06
    Often, it produces the most beautiful, direct, and clear elegant code.
  • 21:06 - 21:08
    The next assignment that will go out
  • 21:08 - 21:11
    has these problems that you do in recursion, and they're each about ten lines
  • 21:11 - 21:13
    long. Some of them are like five lines long.
  • 21:13 - 21:18
    They do incredible things in five lines because of the descriptive power of
  • 21:18 - 21:19
    describing it as
  • 21:19 - 21:22
    here's a base case, and here's a recursive case, and everything else just follows
  • 21:22 - 21:23
    from
  • 21:23 - 21:26
    applying this, and reducing the problem step by step.
  • 21:26 - 21:28
    So you will see things where
  • 21:28 - 21:31
    the recursive code is just beautiful, clean, elegant, easy to understand, easy to follow,
  • 21:31 - 21:32
    easy to test,
  • 21:32 - 21:34
    solves the recursive problem. And in
  • 21:34 - 21:37
    those cases, it really is a much better answer than trying to hack up some
  • 21:37 - 21:39
    other iterative form
  • 21:39 - 21:41
    that may in the end be
  • 21:41 - 21:44
    no more efficient. It may be even less efficient. So don't let efficiency be
  • 21:44 - 21:46
    kind of a big
  • 21:46 - 21:52
    fear of what recursion is for you. So I'm gonna do more examples.
  • 21:52 - 21:54
  • 21:54 - 21:56
    I've got three more examples,
  • 21:56 - 21:58
    or four I think today, so
  • 21:58 - 22:01
    I will just keep
  • 22:01 - 22:03
    showing you different things, and hopefully
  • 22:03 - 22:05
    the patterns will start to kind of
  • 22:05 - 22:07
    come out of the mist for you.
  • 22:07 - 22:10
    A palindrome string is one that reads the same forwards and backwards
  • 22:10 - 22:14
    when reversed. So was it a car or a cat I saw, if you read that
  • 22:14 - 22:16
    backwards, it turns out
  • 22:16 - 22:17
    it
  • 22:17 - 22:21
    says the same thing. Go hang a salami, I'm a lasagna
  • 22:21 - 22:22
    hog. Also
  • 22:22 - 22:26
    handy when you need to have a bar bet over what the longest palindrome is
  • 22:26 - 22:28
    to have these things around.
  • 22:28 - 22:31
    There are certainly ways to do this iteratively. If you were given a string and you were interested to
  • 22:31 - 22:33
    know is it a palindrome,
  • 22:33 - 22:36
    you could kind of do this marching - you're looking outside and kind of marching your way
  • 22:36 - 22:38
    into the middle.
  • 22:38 - 22:42
    But we're gonna go ahead and let recursion kinda help us
  • 22:42 - 22:45
    deal with the subproblem of this,
  • 22:45 - 22:47
    and imagine that at the simplest possible form - you could say that
  • 22:47 - 22:49
    a palindrome
  • 22:49 - 22:51
    consists of
  • 22:51 - 22:53
    an interior palindrome,
  • 22:53 - 22:56
    and then the same letter tacked on to the front and the back.
  • 22:56 - 22:58
    So if you look at was it a car or a cat I saw,
  • 22:58 - 23:01
    there are two Ws there. It starts and it ends with a W,
  • 23:01 - 23:04
    so all palindromes must start and end with the same letter. Okay.
  • 23:04 - 23:05
    Let's check that,
  • 23:05 - 23:11
    and if that matches, then extract that middle and see if it's a palindrome.
  • 23:11 - 23:13
    So it feels like I didn't really do anything. It's like
  • 23:13 - 23:17
    all I did was match two letters, and then I said by the way
  • 23:17 - 23:19
    delegate this problem
  • 23:19 - 23:23
    back to myself, making a call to a function I haven't even finished writing
  • 23:23 - 23:27
    to examine the rest of the letters.
  • 23:27 - 23:29
    And then I need a base case.
  • 23:29 - 23:31
    I've got a recursive case, right?
  • 23:31 - 23:34
    Take off the outer two letters. Make sure they match.
  • 23:34 - 23:37
    Recur on the inside.
  • 23:37 - 23:42
    What is the simplest possible palindrome? One letter? One
  • 23:42 - 23:44
    letter. One letter makes a good palindrome. One letter is by
  • 23:44 - 23:47
    definition first and last letter are the same letter, so it matches. That's
  • 23:47 - 23:53
    a great base case. Is it the only base case? [Inaudible]. Two
  • 23:53 - 23:56
    letters is also kind of important, but there's actually an even
  • 23:56 - 23:58
    simpler form, right?
  • 23:58 - 24:02
    It's the empty string. So both the empty string and the single character string are by
  • 24:02 - 24:05
    definition the world's simplest palindromes. They meet the
  • 24:05 - 24:08
    requirements that they read the same forward and backwards. Empty string forwards
  • 24:08 - 24:10
    and backwards is trivially the
  • 24:10 - 24:14
    same, so that makes it even easier than doing the two-letter case.
  • 24:14 - 24:17
    So if I write this code to look like that where
  • 24:17 - 24:19
    if the length of the strength is
  • 24:19 - 24:24
    one or fewer, so that handles both the zero and the one case, then I return true.
  • 24:24 - 24:25
    Those are trivial
  • 24:25 - 24:26
  • 24:26 - 24:28
    palindromes of the easiest
  • 24:28 - 24:30
    immediate detection.
  • 24:30 - 24:31
    Otherwise,
  • 24:31 - 24:33
    I've got a return here that says
  • 24:33 - 24:35
    if the first character
  • 24:35 - 24:38
    is the same as the last character,
  • 24:38 - 24:39
    and the
  • 24:39 - 24:42
    middle - so the substring starting at Position 1
  • 24:42 - 24:47
    that goes for length minus two characters that picks up the interior
  • 24:47 - 24:50
    discarding the first and last characters - if that's also a palindrome, then we've got a
  • 24:50 - 24:51
    palindrome.
  • 24:51 - 24:54
    So given the short circuiting nature of the and, if
  • 24:54 - 24:56
    it looks at the outer two characters and they don't match, it immediately just
  • 24:56 - 24:58
    stops right there and says false.
  • 24:58 - 25:01
    If they do match, then it goes on looking
  • 25:01 - 25:03
    at the next interior pair
  • 25:03 - 25:07
    which will stack up a recursive call looking at its two things, and eventually we
  • 25:07 - 25:10
    will either catch a pair that doesn't match,
  • 25:10 - 25:14
    and then this false will immediately return its way out, or it will keep
  • 25:14 - 25:17
    going all the way down to that base case, hit a true,
  • 25:17 - 25:21
    and know that we do have a full palindrome there.
  • 25:21 - 25:24
    So you could certainly write this with a for loop. Actually, writing
  • 25:24 - 25:27
    it with a for loop is almost a little bit trickier because you
  • 25:27 - 25:30
    have to keep track of which part of the string are you on, and what happens when you
  • 25:30 - 25:34
    get to the middle and things like that. In some sense, the recursive
  • 25:34 - 25:35
    form really kinda
  • 25:35 - 25:38
    sidesteps that by really thinking about it in a more holistic way
  • 25:38 - 25:41
    of like the outer letters plus an inner palindrome
  • 25:41 - 25:43
    gives me the answer I'm looking for.
  • 25:43 - 25:46
    So this idea of
  • 25:46 - 25:49
    taking a function you're in the middle of writing and making a call to it as
  • 25:49 - 25:50
    though it worked
  • 25:50 - 25:53
    is something that - they require this idea of the leap of faith.
  • 25:53 - 25:55
    You haven't even finished describing
  • 25:55 - 25:58
    how is palindrome operates,
  • 25:58 - 26:01
    and there you are making a call to it depending on it working in order to get
  • 26:01 - 26:02
    your
  • 26:02 - 26:04
    function working. It's a very kind of
  • 26:04 - 26:06
    wacky thing to get your head around.
  • 26:06 - 26:08
    It feels a little bit
  • 26:08 - 26:09
    mystical at first.
  • 26:09 - 26:11
  • 26:11 - 26:13
    That feeling you feel a little bit
  • 26:13 - 26:16
    discombobulated about this is probably pretty normal,
  • 26:16 - 26:17
    but we're gonna
  • 26:17 - 26:18
    keep seeing examples,
  • 26:18 - 26:21
    and hope that
  • 26:21 - 26:23
    it starts to feel a little less unsettling.
  • 26:23 - 26:26
    Anybody wanna ask a question about this so far? Yeah?
  • 26:26 - 26:28
    So I guess create your
  • 26:28 - 26:30
    base case first, then test it? [Inaudible]. That's a great question.
  • 26:30 - 26:35
    So I would say typically that's a great strategy. Get a base
  • 26:35 - 26:38
    case and test against the base case, so the one character and the two character
  • 26:38 - 26:39
    strings.
  • 26:39 - 26:42
    And then imagine one layer out, things that will make one recursive
  • 26:42 - 26:46
    call only. So in this case for the palindrome, it's like what's a two-character
  • 26:46 - 26:47
    string?
  • 26:47 - 26:50
    One has AB. One has AA. So one that is a palindrome, one that isn't,
  • 26:50 - 26:52
    and watch it go through.
  • 26:52 - 26:55
    Then from there you have to almost take that leap and say
  • 26:55 - 26:58
    it worked for the base case. It worked for one out. And then you have to start
  • 26:58 - 27:02
    imagining if it worked for a string of length N, it'll work for one of N plus one,
  • 27:02 - 27:03
    and
  • 27:03 - 27:07
    that in some sense testing it exhaustively across all strings is
  • 27:07 - 27:09
    of course impossible, so you have to kind of
  • 27:09 - 27:12
    move to a sort of larger case where you can't just sit there and trace the whole
  • 27:12 - 27:15
    thing. You'll have to in some sense take a faith thing that says
  • 27:15 - 27:18
    if it could have computed whether the interior's a palindrome, then adding two
  • 27:18 - 27:21
    characters on the outside
  • 27:21 - 27:21
  • 27:21 - 27:24
    and massaging that with that answer should produce the right thing. So some bigger
  • 27:24 - 27:26
    tests
  • 27:26 - 27:28
    that
  • 27:28 - 27:30
    verify that the recursive case
  • 27:30 - 27:32
    when exercised does the right thing,
  • 27:32 - 27:34
    then the pair together - All your code is going through
  • 27:34 - 27:38
    these same lines. The outer case going down to the recursive case, down
  • 27:38 - 27:39
    to that base case, and that's
  • 27:39 - 27:43
    one of the beauty of writing it recursively is that in some sense
  • 27:43 - 27:46
    once this piece of code works for some simple cases, the idea that setting it to
  • 27:46 - 27:48
    larger cases
  • 27:48 - 27:51
    is almost - I don't wanna
  • 27:51 - 27:54
    say guaranteed. That maybe makes it sound too easy, but in fact, if it
  • 27:54 - 27:55
    works for
  • 27:55 - 27:58
    cases of N and then N plus one, then it'll work for 100, and 101, and
  • 27:58 - 28:02
    6,000, and 6,001, and all the way through all the numbers by
  • 28:02 - 28:06
    induction. Question?
  • 28:06 - 28:10
    You have to remove all the [inaudible], all the spaces? Yeah. So definitely like the was it a car to match I should definitely
  • 28:10 - 28:13
    be taking my spaces out of there to make it right. You
  • 28:13 - 28:21
    are totally correct on that.
  • 28:21 - 28:22
    So let me show you
  • 28:22 - 28:25
    one where recursion is definitely
  • 28:25 - 28:27
    gonna buy us some real efficiency
  • 28:27 - 28:30
    and some real clarity in solving a
  • 28:30 - 28:32
    search problem. I've got a
  • 28:32 - 28:35
    vector. Let's say it's a vector of strings. It's a vector
  • 28:35 - 28:37
    of numbers. A vector of anything, it doesn't really matter.
  • 28:37 - 28:40
    And I wanna see if I can find a particular entry in it. So unlike the
  • 28:40 - 28:44
    set which can do a fast contains for you, in vector
  • 28:44 - 28:46
    if I haven't done anything special with it,
  • 28:46 - 28:49
    then there's no guarantee about where to find something. So if I wanna say
  • 28:49 - 28:51
  • 28:51 - 28:54
    did somebody score 75 on the exam, then I'm gonna have
  • 28:54 - 28:56
    to just walk through the vector starting at Slot
  • 28:56 - 28:59
    0, walk my way to the end, and
  • 28:59 - 29:02
    compare to see if I find a 75. If I get to the end and I haven't found one, then
  • 29:02 - 29:04
    I can say no.
  • 29:04 - 29:05
    So that's what's called linear search. Linear
  • 29:05 - 29:10
    kind of implies this left to right sequential processing.
  • 29:10 - 29:11
    And linear search
  • 29:11 - 29:15
    has the property that as the size of the input grows, the amount of time
  • 29:15 - 29:18
    taken to search it grows in proportion.
  • 29:18 - 29:20
    So if you have a 1000 number array,
  • 29:20 - 29:23
    and you doubled its size, you would expect that doing a linear search on it should take
  • 29:23 - 29:30
    twice as long for an array that's twice as large.
  • 29:30 - 29:34
    The strategy we're gonna look at today is binary search
  • 29:34 - 29:37
    which is gonna try to avoid this looking in every one of those boxes to
  • 29:37 - 29:38
    find something.
  • 29:38 - 29:42
    It's gonna take a divide and conquer strategy, and it's gonna require a sorted
  • 29:42 - 29:42
    vector.
  • 29:42 - 29:45
    So in order to do an efficient lookup,
  • 29:45 - 29:48
    it helps if we've done some pre-rearrangement
  • 29:48 - 29:49
  • 29:49 - 29:51
    of the data. In this case, putting it into sorted order
  • 29:51 - 29:55
    is gonna make it much easier for us to be able to find something in it because
  • 29:55 - 29:55
    we have
  • 29:55 - 29:58
    better information about where to look,
  • 29:58 - 29:59
    so much faster.
  • 29:59 - 30:03
    So we'll see that surely there was some cost to that,
  • 30:03 - 30:05
    but typically binary search is gonna be used when you have an array where you
  • 30:05 - 30:07
    don't do a lot of changes to it so
  • 30:07 - 30:11
    that putting it in a sorted order can be done once, and then from that point forward you can
  • 30:11 - 30:15
    search it many times, getting the benefit of the work you did to put it in
  • 30:15 - 30:16
    sorted order.
  • 30:16 - 30:19
    If you plan to sort it just to search it, then in some sense all the time you spent
  • 30:19 - 30:20
    sorting it
  • 30:20 - 30:25
    would kind of count against you and unlikely to come out ahead.
  • 30:25 - 30:28
    So the insight we're gonna use is
  • 30:28 - 30:32
    that if we have this in sorted order, and
  • 30:32 - 30:34
    we're trying to search the whole thing - we're looking for let's say the No. 75
  • 30:34 - 30:36
    -
  • 30:36 - 30:38
    is that if we just look at the middlemost element,
  • 30:38 - 30:40
    so we have this idea that we're looking at the whole vector right now from the
  • 30:40 - 30:42
    start to the end,
  • 30:42 - 30:45
    and I look at the middle element, and I say it's a 54. I can say
  • 30:45 - 30:49
    if 75 is in this vector because it's in sorted order,
  • 30:49 - 30:51
    it can't be
  • 30:51 - 30:52
    anywhere over here.
  • 30:52 - 30:55
    If 54's right there, everything to the left of 54
  • 30:55 - 30:57
    must be less than that,
  • 30:57 - 30:59
    and 75 wouldn't be over there.
  • 30:59 - 31:00
    So that means I can
  • 31:00 - 31:03
    just discard that half of the
  • 31:03 - 31:06
    vector from further consideration.
  • 31:06 - 31:07
    So now I have this half
  • 31:07 - 31:10
    to continue looking at which is the things that
  • 31:10 - 31:13
    are the right of 54 all the way to the end.
  • 31:13 - 31:17
    I use the same strategy again. I say if I'm searching a vector - so
  • 31:17 - 31:19
    last time I was searching a vector that had 25 elements. Now I've got one that's got
  • 31:19 - 31:21
    just 12.
  • 31:21 - 31:24
    Again, I use the same strategy. Look at the one in the middle.
  • 31:24 - 31:26
    I say oh, it's an 80,
  • 31:26 - 31:29
    and then I say well the number I'm looking for is 75.
  • 31:29 - 31:30
    It can't be
  • 31:30 - 31:35
    to the right of the 80. It must be to the left of it.
  • 31:35 - 31:36
    And then that lets me
  • 31:36 - 31:40
    get rid of another quarter of the vector. If I keep doing this, I get rid of
  • 31:40 - 31:43
    half, and then a quarter, and then an eighth, and
  • 31:43 - 31:47
    then a 16th. Very quickly, I will narrow in on the position where if 75 is in
  • 31:47 - 31:53
    this vector, it has to be. Or I'll be able to conclude it wasn't there at all. Then I kind of
  • 31:53 - 31:55
    work on my in, and
  • 31:55 - 31:59
    I found a 74 and a 76 over there, then I'm done.
  • 31:59 - 32:02
    That base case comes when I have
  • 32:02 - 32:04
    such a small little vector there
  • 32:04 - 32:05
    where
  • 32:05 - 32:09
    my bounds have crossed in such a way that I can say I never found what I
  • 32:09 - 32:16
    was looking for.
  • 32:16 - 32:19
    So let's walk through
  • 32:19 - 32:21
    this
  • 32:21 - 32:22
  • 32:22 - 32:25
    bit of code that kind of puts into C++ the thing I just described here.
  • 32:25 - 32:29
    I've got a vector. In this case, I'm using a vector that's containing strings.
  • 32:29 - 32:32
    It could be ints. It could be anything. It doesn't really matter.
  • 32:32 - 32:36
    I've got a start and a stop, which I identify the sub-portion of the vector
  • 32:36 - 32:40
    that we're interested in. So the start is the first index to consider. The stop is the
  • 32:40 - 32:41
    last index to consider.
  • 32:41 - 32:46
    So the very first call to this will have start set to zero and stop set to the vector's
  • 32:46 - 32:48
    size minus one. I
  • 32:48 - 32:51
    compute the midpoint index
  • 32:51 - 32:54
    which is just the sum of the start and stop divided by two,
  • 32:54 - 32:56
    and then I compare
  • 32:56 - 32:58
    the key that I'm looking for to
  • 32:58 - 33:02
    the value at that index. I'm looking right in the middle. If it happens to match,
  • 33:02 - 33:06
    then I return that. The goal of binary search in this case is to return the index of a
  • 33:06 - 33:08
    matching element within the vector,
  • 33:08 - 33:11
    or to return this not found negative one constant
  • 33:11 - 33:14
    if it was unable to find any match anywhere.
  • 33:14 - 33:16
    So when we do find it
  • 33:16 - 33:20
    at whatever the level the recursion is, we can just immediately return that. We're done.
  • 33:20 - 33:21
    We found it. It's good.
  • 33:21 - 33:24
    Otherwise, we're gonna make this recursive call
  • 33:24 - 33:28
    that looks at either the left half or the right half based on if key is
  • 33:28 - 33:28
    less than
  • 33:28 - 33:30
    the value we found at the midpoint,
  • 33:30 - 33:32
    then the place we're searching is -
  • 33:32 - 33:34
    still has the same start position,
  • 33:34 - 33:36
    but is now capped
  • 33:36 - 33:38
    by the
  • 33:38 - 33:40
    element exactly to the left of the midpoint,
  • 33:40 - 33:43
    and then the right one,
  • 33:43 - 33:45
    the inversion of that,
  • 33:45 - 33:47
    one to the right of the midpoint
  • 33:47 - 33:49
    and the stop unchanged.
  • 33:49 - 33:53
    So taking off half of the elements under consideration at each stage,
  • 33:53 - 33:54
    eventually
  • 33:54 - 33:58
    I will get down to the simplest possible case. And the simplest possible case isn't that I have a one-element
  • 33:58 - 34:02
    vector and I found it or not. The really simple case is actually that I have zero
  • 34:02 - 34:03
    elements in my vector
  • 34:03 - 34:04
    that I just kept
  • 34:04 - 34:06
    moving in the
  • 34:06 - 34:09
    upper and lower bound point until they crossed,
  • 34:09 - 34:12
    which meant that I ran out of elements to check.
  • 34:12 - 34:15
    And that happens when the start index is greater than the stop index. So the start
  • 34:15 - 34:19
    and the stop if they're equal to each other mean that you have a one element vector left
  • 34:19 - 34:20
    to search,
  • 34:20 - 34:22
    but if you - For
  • 34:22 - 34:24
    example, if you got to that case where you have that one element vector left to search, you'll look
  • 34:24 - 34:28
    at that one, and if it matches, you'll be done. Otherwise, you'll end up
  • 34:28 - 34:30
    either decrementing the stop
  • 34:30 - 34:33
    to move past the start, or incrementing the start to move past the
  • 34:33 - 34:34
    stop.
  • 34:34 - 34:36
    And then that next iteration will hit this base case that said
  • 34:36 - 34:40
    I looked at the element in a one-element vector. It didn't work
  • 34:40 - 34:40
    out.
  • 34:40 - 34:43
    I can tell you for sure it's not found.
  • 34:43 - 34:45
    If it had been here, I would've seen it.
  • 34:45 - 34:47
    And this is looking at just one element
  • 34:47 - 34:49
    each
  • 34:49 - 34:50
    recursive call,
  • 34:50 - 34:55
    and the recursive calls in this case stack up to a depth
  • 34:55 - 34:57
    based on the logarithm of the size
  • 34:57 - 34:59
    to the power of two,
  • 34:59 - 35:02
    so that if you have 1000 elements,
  • 35:02 - 35:06
    you look at one, and now you have a 500-element collection to look
  • 35:06 - 35:09
    at again. You look at one, you have
  • 35:09 - 35:12
    a 250 element, 125, 60, 30,
  • 35:12 - 35:12
    15.
  • 35:12 - 35:14
    So at each stage
  • 35:14 - 35:15
    half of them
  • 35:15 - 35:19
    remain for the further call, and the number of times you can do that
  • 35:19 - 35:22
    for 1000 is the number of times you can divide 1000 by two, which is
  • 35:22 - 35:24
    the log based two of that,
  • 35:24 - 35:26
    which is roughly ten.
  • 35:26 - 35:29
    So if you were looking at 1000 number array, if it's in sorted order,
  • 35:29 - 35:32
    it takes you ten comparisons
  • 35:32 - 35:34
    to conclusively determine
  • 35:34 - 35:38
    where an element is if it's in there, or that it doesn't exist in the array at
  • 35:38 - 35:39
    all.
  • 35:39 - 35:41
    If you take that 1000 element array,
  • 35:41 - 35:43
    and you make it twice as big,
  • 35:43 - 35:46
    so now I have a 2000
  • 35:46 - 35:50
    number array, how much longer does it take? One more step.
  • 35:50 - 35:51
    One more step.
  • 35:51 - 35:55
    Just one, right? You look at one, and you have a 1000 number array, so however long it took
  • 35:55 - 35:56
    you to do that 1000 number array,
  • 35:56 - 36:00
    it takes one additional comparison, kinda one stack frame on top of that
  • 36:00 - 36:01
    to get its way down.
  • 36:01 - 36:04
    So this means actually this is super efficient.
  • 36:04 - 36:06
    That you can search so for example
  • 36:06 - 36:09
    a million is roughly two the 20th power.
  • 36:09 - 36:12
    So you have a million entry
  • 36:12 - 36:16
    collection to search, it will take you 20 comparisons to say for sure
  • 36:16 - 36:17
    it's here or not,
  • 36:17 - 36:18
    and here's where I found it,
  • 36:18 - 36:20
    just 20.
  • 36:20 - 36:22
    You go up to two million, it takes
  • 36:22 - 36:24
    21. The
  • 36:24 - 36:27
    very slow growing function, that logarithm function, so that tells you
  • 36:27 - 36:28
  • 36:28 - 36:31
    that this is gonna be a very efficient way of searching a sorted array. A
  • 36:31 - 36:34
    category
  • 36:34 - 36:37
    thing called the divide and conquer that
  • 36:37 - 36:39
    take a problem,
  • 36:39 - 36:42
    divide it typically in half, but sometimes in thirds or some other
  • 36:42 - 36:45
    way, and then kind of - in this case
  • 36:45 - 36:49
    it's particularly handy that we can throw away some part of the problem, so we
  • 36:49 - 36:56
    divide and focus on just one part to solve the problem.
  • 36:56 - 36:58
    All right. So this is the first one
  • 36:58 - 37:02
    that's gonna start to
  • 37:02 - 37:04
    really inspire you for how recursion can help you solve problems
  • 37:04 - 37:05
    that you might
  • 37:05 - 37:08
    have no idea how to approach any other way
  • 37:08 - 37:10
    than using a recursive formulation.
  • 37:10 - 37:13
    So this is an exercise that comes out of the reader in Chapter 4,
  • 37:13 - 37:16
    and the
  • 37:16 - 37:18
    context of it is you have N things,
  • 37:18 - 37:22
    so maybe it's N people in a dorm, 60 people in a dorm,
  • 37:22 - 37:26
    and you would like to choose K of them. Let's K a real number, four
  • 37:26 - 37:29
    - four people to go together to Flicks.
  • 37:29 - 37:32
    So of your 60 dorm mates, how many different ways could you
  • 37:32 - 37:33
    pick a subset
  • 37:33 - 37:35
    of size four
  • 37:35 - 37:38
    that doesn't repeat any of the others? So you can pick the
  • 37:38 - 37:41
    two people from the first floor, one person from the middle floor, one person
  • 37:41 - 37:44
    from the top floor, but then you can kind of shuffle it up. What if you took all the people from the
  • 37:44 - 37:45
    first floor, or
  • 37:45 - 37:48
    these people from that room, and whatnot? You can imagine there's a lot of kind of
  • 37:48 - 37:50
    machinations
  • 37:50 - 37:53
    that could be present here,
  • 37:53 - 37:57
    and counting them, it's not quite obvious
  • 37:57 - 37:59
    unless you kinda go back to start working on your real math
  • 37:59 - 38:01
    skills
  • 38:01 - 38:05
    how it is that you can write a formula for this.
  • 38:05 - 38:07
    So what I'm gonna give you is
  • 38:07 - 38:11
    a recursive way of thinking about this problem.
  • 38:11 - 38:12
    So I drew
  • 38:12 - 38:15
    a set of the things we're looking at?
  • 38:15 - 38:18
    So there are N things that we're trying to choose K out
  • 38:18 - 38:21
    of. So right now, I've got
  • 38:21 - 38:23
    12 or so
  • 38:23 - 38:24
    people, or
  • 38:24 - 38:26
    items, whatever it is.
  • 38:26 - 38:29
    What I'm gonna do is I'm gonna imagine just designating one
  • 38:29 - 38:30
    totally at random.
  • 38:30 - 38:33
    So pick Bob.
  • 38:33 - 38:35
    Bob is one of the people in the dorm. I'm gonna kind of separate him from
  • 38:35 - 38:39
    everybody else mentally in my mind, and draw this line, and kinda mark him with a red flag
  • 38:39 - 38:41
    that says that's Bob.
  • 38:41 - 38:45
    So Bob might go to Flicks or might not go to Flicks. Some of the subsets for
  • 38:45 - 38:52
    going to Flicks will include Bob, and some will not.
  • 38:52 - 38:53
    Okay.
  • 38:53 - 38:57
    So what I'd like to think about is how many different subsets can I make that will
  • 38:57 - 38:58
    include Bob,
  • 38:58 - 39:01
    and how many different subsets can I make that don't include Bob. And
  • 39:01 - 39:05
    if I added those together, then that should be the total number of subsets I can make
  • 39:05 - 39:08
    from this collection.
  • 39:08 - 39:11
    Okay, so the subsets that include Bob -
  • 39:11 - 39:15
    so once I've committed to Bob being in the set, and let's say I'm trying to pick four
  • 39:15 - 39:16
    members out of here,
  • 39:16 - 39:18
    then I have Bob
  • 39:18 - 39:22
    and I have to figure out how many ways can I pick three people to accompany Bob to
  • 39:22 - 39:24
    the Flicks.
  • 39:24 - 39:28
    So I'm picking from a slightly smaller population. The population went down by one,
  • 39:28 - 39:31
    and the number I'm seeking went down by one,
  • 39:31 - 39:34
    and that tells me all the ways I can pick three people to go with Bob.
  • 39:34 - 39:38
    The ones that don't include Bob, and Bob's just out of the running,
  • 39:38 - 39:42
    I look at the remaining population which is still one smaller, everybody but Bob, and I
  • 39:42 - 39:47
    look for the ways I can still pick four people from there.
  • 39:47 - 39:51
    So what I have here is trying to compute C of NK
  • 39:51 - 39:56
    is trying to compute C of N minus one K minus one and add it to C of N minus one K.
  • 39:56 - 39:58
    So this is
  • 39:58 - 39:59
    picking
  • 39:59 - 40:02
    friends to accompany Bob. This is picking people without Bob.
  • 40:02 - 40:03
    Add those together,
  • 40:03 - 40:08
    and I will have the total number of ways I can pick K things out of N.
  • 40:08 - 40:13
    So we're very much relying on this recursive idea of if I had the answer
  • 40:13 - 40:16
    - I don't feel smart enough to know the answer directly,
  • 40:16 - 40:20
    but if I could defer it to someone who was actually willing to do more
  • 40:20 - 40:23
    scrutiny into this thing, if you could tell me how many groups of three you can
  • 40:23 - 40:28
    join with Bob, and how many groups of four you can pick without Bob, I can tell you
  • 40:28 - 40:30
    what the total answer is.
  • 40:30 - 40:33
    The simplest possible base case
  • 40:33 - 40:38
    we're gonna work our way down to are when there are just no choices remaining at all.
  • 40:38 - 40:40
    So if you look back at my
  • 40:40 - 40:43
    things that are here, in
  • 40:43 - 40:46
    both cases the population is getting smaller,
  • 40:46 - 40:50
    and in one of the recursive calls, the number of things I'm trying to
  • 40:50 - 40:52
    pick is also getting smaller.
  • 40:52 - 40:55
    So they're both making progress toward kind of shrinking those down to where
  • 40:55 - 40:56
    there's kind of
  • 40:56 - 40:59
    more and more constraints on what I have to choose.
  • 40:59 - 41:02
    For example, on this one
  • 41:02 - 41:05
    as I keep shrinking the population by one
  • 41:05 - 41:07
    and trying to get the same number of people,
  • 41:07 - 41:10
    eventually I'll be trying to pick three people out of three,
  • 41:10 - 41:14
    where I'm trying to pick of K of K remaining. Well, there's only one way to do
  • 41:14 - 41:17
    that which is to take everyone.
  • 41:17 - 41:18
    On this one, I'll
  • 41:18 - 41:20
    eventually keep picking people,
  • 41:20 - 41:23
    so the K is always less than the N in this case. The
  • 41:23 - 41:26
    K will eventually kinda bottom out, or I'll say I've
  • 41:26 - 41:30
    already picked all the people. I've already picked four people. I need to pick zero more. And
  • 41:30 - 41:34
    at that point, that's also very easy, right? Picking zero out of a population, there's
  • 41:34 - 41:38
    only one way to do that.
  • 41:38 - 41:41
    So what we end up with is
  • 41:41 - 41:45
    a very simple base case of if K equals zero - so I'm not trying to choose anymore.
  • 41:45 - 41:49
    I've already committed all the slots. Or if K is equal to N
  • 41:49 - 41:50
    where I've
  • 41:50 - 41:52
    discarded a whole bunch of people, and now I'm down to where I'm
  • 41:52 - 41:56
    facing I've gotta get four, and I've got four left. Well, there's only one way to do those things,
  • 41:56 - 41:59
    and that's to take everybody or to take nobody.
  • 41:59 - 42:00
    And then otherwise,
  • 42:00 - 42:02
    I make those two recursive calls
  • 42:02 - 42:05
    with Bob, without Bob, add them together
  • 42:05 - 42:13
    to get my whole result.
  • 42:13 - 42:14
    That's wacky. I'm gonna
  • 42:14 - 42:18
    read you something,
  • 42:18 - 42:22
    and then we'll call it a day. I
  • 42:22 - 42:31
    brought a book with me. I stole this from my children.
Title:
Lecture 8 | Programming Abstractions (Stanford)
Description:

Lecture 8 by Julie Zelenski for the Programming Abstractions Course (CS106B) in the Stanford Computer Science Department.

Julie talks about solving problems recursively. She covers functional recursion with the simple example of writing an exponential function using recursion. From the simple program performing as an exponential function Julie continues to show a more efficient recursion code. The next example she covers is that of binary search and how recursion is used in this instance.

Complete Playlist for the Course:
http://www.youtube.com/view_play_list?p=FE6E58F856038C69

CS 106B Course Website:
http://cs106b.stanford.edu

Stanford Center for Professional Development:
http://scpd.stanford.edu/

Stanford University:
http://www.stanford.edu/

Stanford University Channel on YouTube:
http://www.youtube.com/stanforduniversity/

more » « less
Video Language:
English
Duration:
42:37
N. Ueda edited English subtitles for Lecture 8 | Programming Abstractions (Stanford)
Eunjeong_Kim added a translation

English subtitles

Revisions