Description: In this lecture, Prof. Grimson introduces the concept of recursion and the Python dictionary data type.
Instructor: Prof. Eric Grimson
The following content is provided under a Creative Commons license. Your support will help MIT OpenCourseWare continue to offer high quality educational resources for free. To make a donation or view additional materials from hundreds of MIT courses, visit MIT OpenCourseWare at ocw.mit.edu.
ERIC GRIMSON: Ladies and gentlemen, I'd like to get started. My name's Eric Grimson. I have the privilege of serving as MIT'S chancellor for academic advancement, you can go look up what that means, and like John I'm a former head of course six. This term, with Ana and John, I'm going to be splitting the lectures, so I'm up to date.
OK last time Ana introduced the first of the compound data types, tuples and lists. She showed lots of ways of manipulating them, lots of built in things for manipulating those structures. And the key difference between the two of them was that tuples were immutable, meaning you could not change them, lists were mutable, they could be changed, or mutated. And that led to both some nice power and some opportunities for challenges. And, in particular, she showed you things like aliasing, where you could have two names pointing to the same list structure, and because of that, you could change the contents of one, it would change the appearance of the contents of the other, and that leads to some nice challenges. So the side effects of mutability are one of the things you're going to see, both as a plus and minus, as we go through the course.
Today we're going to take a different direction for a little while, we're going to talk about recursion. It Is a powerful and wonderful tool for solving computational problems. We're then going to look at another kind of compound data structure, a dictionary, which is also mutable. And then we're going to put the two pieces together and show how together they actually give you a lot of power for solving some really neat problems very effectively. But I want to start with recursion. Perhaps one of the most mysterious, at least according to programmer's, concepts in computer science, one that leads to lots of really bad computer science jokes, actually all computer science jokes are bad, but these are particularly bad.
So let's start with the obvious question, what is recursion? If you go to the ultimate source of knowledge, Wikipedia, you get something that says, in essence, recursion is the process of repeating items in a self-similar way. Well that's really helpful, right? But we're going to see that idea because recursion, as we're going to see in a second, is the idea of taking a problem and reducing it to a smaller version of the same problem, and using that idea to actually tackle a bunch of really interesting problems. But recursion gets used in a lot of places. So it's this idea of using, or repeating, the idea multiple times. So wouldn't it be great if your 3D printer printed 3D printers? And you could just keep doing that all the way along.
Or one that's a little more common, it's actually got a wonderful name, it's called mise en abyme, in art, sometimes referred to as the Droste effect, pictures that have inside them a picture of the picture, which has inside them a picture of the picture, and you get the idea. And of course, one of the things you want to think about in recursion is not to have it go on infinitely. And yes there are even light bulb jokes about recursion, if you can't read it, it says, how many twists does it take to screw in a light bulb? And it says, if it's already screwed in, the answer is 0. Otherwise, twist it once, ask me again, add 1 to my answer. And that's actually a nice description of recursion.
So let's look at it more seriously. What is recursion? I want to describe it both abstractly, or algorithmically, and semantically or, if you like, in terms of programming. Abstractly, this is a great instance of something often called divide-and-conquer, or sometimes called decrease-and-conquer. And the idea of recursion is, I want to take a problem I'm trying to solve and say, how could I reduce it to a simpler version of the same problem, plus some things I know how to do? And then that simpler version, I'm going to reduce it again and keep doing that until I get down to a simple case that I can solve directly. That is how we're going to think about designing solutions to problems.
Semantically, this is typically going to lead to the case where a program, a definition of function, will refer to itself in its body. It will call itself inside its body. Now, if you remember your high school geometry teacher, she probably would wrap your knuckles, which you're not allowed to do, because in things like geometry you can't define something in terms of itself, right? That's not allowed. In recursion, this is OK. Our definition of a procedure can in its body call itself, so long as I have what I call a base case, a way of stopping that unwinding of the problems, when I get to something I can solve directly. And so what we're going to do is avoid infinite recursion by ensuring that we have at least one or more base cases that are easy to solve. And then the basic idea is I just want to solve the same problem on some simpler input with the idea of using that solution to solve the larger problem.
OK, let's look at an example, and to set the stage I'm going to go back to something you've been doing, iterative algorithms. For loops, while loops, they naturally lead to what we would call iterative algorithms, and these algorithms can be described as being captured by a set of state variables, meaning one or more variables that tell us exactly the state of the computation. That's a lot of words, let's look at an example. I know it's trivial, but bear with me.
Suppose I want to do integer multiplication, multiply two integers together, and all I have available to me is addition. So a times b is the same as adding a to itself b times. If I'm thinking about this iteratively, I could capture this computation with two state variables. One we'd just call the iteration number, and it would be something, for example, that starts at b, and each time through the loop reduces 1. One. And it will keep doing that until I've counted down b times, and I get down to 0.
And at the same time, I would have some value of the computation, I might call it result, which starts at 0, first time through adds an a, next time through adds an a, and it just keeps track of how many things have I added up, until I get done. And yeah, I know you could just do mult, but this is trying to get this idea of, how would I do this iteratively. So I might start off with i, saying there are b things still to add, and the result is 1. The first time through the loop, I add an a, reduce i by 1. Next time through the loop, I add in another a, reduce i by 1, and you get the idea. I just walk down it until, eventually, I got to the end of this computation.
So we could write code for this, and, actually, it should be pretty straightforward. There it is. Going to call it mult_iter, takes in two arguments a and b, and I'm going to capture exactly that process. So notice what I do, I set up result internally as just a little variable I'm going to use to accumulate things. And then, there is the iteration, as long as b is greater than 0 what do I do? Add a to result, store it away, reduce b by 1, and I'll keep doing that until b gets down to being equal to 0, in which case I just return the result. OK, simple solution.
Now, let's think about this a different way. A times b is just adding a to itself b times, and that's the same as a plus adding a to a itself b minus 1 times. OK, that sounds like leisure to me, that sounds like just playing with words. But it's really important, because what is this? Ah, that's just a times b minus 1, by the definition of the top point. And I know you're totally impressed, but this is actually really cool, because what have I done? I've taken one problem, this one up here, and I've reduced it to a simpler version of the same problem, plus some things I know how to do. And how would I solve this? Same trick, that's a times a times b minus 2, I would just unwrap it one more time, and I would just keep doing that until I get down to something I can solve directly, a base case. And that's easy, when b equal to 1, the answer is just a. Or I could do when b is equal to 0 the answer is just 0. And there's code to capture that.
Different form, wonderful compact description, what does it say? It says, if I'm at the base case, if b is equal to 1, the answer is just a. Otherwise, I'm going to solve the same problem with a smaller version and add it to a and return that result. And that's nice, crisp characterization of a problem. Recursive definition that reduces a problem to a simpler version of the same problem. OK, let's look at another example. Classic problem in recursion is to compute factorial, right? n factorial, or n bang if you like, n exclamation point is n times n minus 1, all the way down to 1. So it's the product of all the integers from 1 up to n assuming n is a positive integer.
So we can ask the same question if I wanted to solve this recursively what would the base case be? Well, when n is equal to 1, it's just 1. In the recursive case, will n times n minus 1 all the way down to 1, that's the same as n times n minus 1 factorial. So I can easily write out the base case, and I've got a nice recursive solution to this problem. OK, if you're like me and this is the first time you've seen it, it feels like I've taken your head and twisted it about 180 degrees. I'm going to take it another 180 degrees because you might be saying, well, wait a minute, how do you know it really stops. How do you know it really terminates the computation? So let's look at it.
There is my definition for fact, short for factorial. Fact of 1 is, if n is equal to 1 return 1, otherwise return n times fact of n minus 1. And let's use the tools that Ana talked about, in terms of an environment at a scope, and think about what happens here. So when I read that in or I evaluate that in Python, it creates a definition that binds the name fact to some code, just all of that stuff over here plus the name for the formal parameter, hasn't done anything with it yet.
And then I'm going to evaluate print a fact of 4. Print needs a value, so it has to get the value of fact of 4, and we know what that does. It looks up fact, there it is, it's procedure definition. So it creates a new frame, a new environment, it calls that procedure, and inside that frame the formal parameter for fact is bound to the value passed in. So n is bound to 4. That frame is scoped by this global frame meaning it's going to inherit things in the global frame. And what does it do? It says, inside of this frame evaluate the body of fact. OK, so it says as n equal to 1? Nope, it's not, it's 4. So in that case, go to the else statement and says, oh, return n times fact of n and n as 4, fact of n minus 1 says I need to return 4 times fact of 3.
4 is easy, multiplication is easy, fact of 3, ah yes, I look up fact. Now I'm in this frame, I don't see fact there, but I go up to that frame. There's the definition for fact, and we're going to do the rest of this a little more quickly, what does that do? It creates a new frame called by fact. And the argument passed in for n is n minus 1, that value, right there, of 3. So 3 is now bound to n. Same game, evaluate the body is n equal to 1? No, so in that case, I'm going to go to the return statement, it says return 3 times fact of 2. And notice it's only looking at this value of n because that's the frame in which I'm in. It never sees that value of n.
OK, aren't you glad I didn't do fact of 400? We've only got two more to go, but you get the idea. Same thing, I need to get fact of 2 is going to call fact again with n bound to 2. Relative that evaluates the body and is not yet equal to 1. That says I'm going to the else clause and return 2 times fact of 1. I call fact again, now with n bound to 1, and, fortunately, now that clause is true, and it says return 1. Whoops, sorry, before I do, so there's the base case. And it may seem apparent to you, but this is important, right? I'm unwinding this till I get to something that can stop the computation. Now I'm simply going to gather the computation up, because it says return 1. Who asked for it? Well that call to fact of 1. So that reduces to return 2 times 1. And who called for that? Fact of 2. That reduces to return a 3 times 2, which reduces to 4 times 6, which reduces to printing out 24. So it unwinds it down to a base case and it stops.
A couple of observations, notice how each recursive call creates its own frame, and as a consequence, there's no confusion about which value of n I'm using. Also notice, in the other frames, n was not changed. We did not mutate it. So we're literally creating a local scope for that recursive call, which is exactly what we want. Also notice how there was a sense of flow of control in computing fact of something, that reduces to returning n times fact of n minus 1, and that creates a new scope. And that will simply keep unwinding until I get to something that can return a value and then I gather all those frames back up. So there's a natural flow of control here. But most importantly, there's no confusion about which variable I'm using when I'm looking for a value of n.
All right, because this is often a place where things get a little confusing, I want to do one more example. But let me first show you side by side the two different versions of factorial. Actually, I have lied slightly, we didn't show this one earlier but there's factorial if I wanted to do it iteratively. I'd set up some initial variable to 1, and then I'd just run through a loop. For example, from 1 up to just below n minus 1, or 1 up to n, multiplying it and putting it back into return product.
Which one do you like more? You can't say neither you have to pick one. Show of hands, how many of you like this one? Some hesitant ones, how many prefer this one? Yeah, that's my view. I'm biased, but I really like the recursive one. It is crisper to look at, you can see what it's doing. I'm reducing this problem to a simpler version of that problem. Pick your own version but I would argue that the recursive version is more intuitive to understand. From a programmer's perspective, it's actually often more efficient to write, because I don't have to think about interior variables. Depending on the machine, it may not be as efficient when you call it because in the recursive version I've got it set up, that set of frames. And some versions of these languages are actually very efficient about it, some of them a little less so. But given the speed of computers today, who cares as long as it actually just does the computation.
Right, one more example, how do we really know our recursive code works? Well, we just did a simulation but let's look at it one more way. The iterative version, what can I say about it? Well, I know it's going to terminate because b is initially positive, assuming I gave it an appropriate value. It decreases by 1 every time around this loop, at some point it has to get less than 1, it's going to stop. So I can conclude it's always going to terminate. What about the recursive version? Well, if I call it with b equal to one, I'm done. If I call it with b greater than one, again it's going to reduce it by one on the recursive call, which means on each recursive call it's going to reduce and eventually it gets down to a place, assuming I gave it a positive integer, where b is equal to one. So it'll stop, which just good.
What we just did was we used the great tool from math, second best department at MIT. Wow, I didn't even get any hisses on that one, John, all right, and I'm now in trouble with the head of the math department. So now that I got your attention, and yes, all computer science jokes are bad, and mine are really bad, but I'm tenured. You cannot do a damn thing about it.
Let's look at mathematical induction which turns out to be a tool that lets us think about programs in a really nice way. You haven't seen this, here's the idea of mathematical induction. If I want to prove a statement, and we refer to it as being indexed on the integers. In other words, it's some mathematical statement that runs over integers. If I want to prove it's true for all values of those integers, mathematically I'd do it by simply proving it's true for the smallest value of n typically n is equal to 0 or 1, and then I do an interesting thing. I say I need to prove that if it's true for an arbitrary value of n, I'm just going to prove that it's also then true for n plus 1. And if I can do those two things I can then conclude for an infinite number of values of n it's always true.
Then we'll relate it back to programming in a second, but let me show you a simple example of this, one that you may have seen. If I had the integers from 0 up to n, or even from 1 up to n, I claim that's the same as n times n plus 1 over 2. So 1, 2, 3, that's 6, right. And that's exactly right, 3 times 4, which is divided by 2, which gives me out 6. How would I prove this? Well, by induction? I need to do the simple cases if n is equal to 0, well then this side is just 0. And that's 0 times 1, which is 0 divided by true. So 0 equals 0, it's true.
Now the inductive step. I'm going to assume it's true for some k, I should have picked n, but for some k, and then what I need to show is it's true for k plus 1. Well, there's the left hand side, and I want to show that this is equal to that. And I'm going do it by using exactly this recursive idea, because what do I know, I know that this sum, in here, I'm assuming is true. And so that says that the left hand side, the first portion of it, is just k times k plus 1 over 2, that's the definition of the thing I'm assuming is true. To that I'm going to add k plus 1. Well, you can do the algebra, right? That's k plus 1 all times k over 2 plus 1, which is k plus 2 over 2. Oh cool, it's exactly that. Having done that, I can now conclude this is true for all values of n.
What does it have to do with programming? That's exactly what we're doing when we think about recursive code, right? We're saying, show that it's true for the base case, and then what I'm essentially assuming is that, if it works for values smaller than b, then does the code return the right answer for b? And the answer is, absolutely it does, and I'm using induction to deduce that, in fact, my code does the right thing. Why am I torturing you with this? Because this is the way I want you to think about recursion. When I'm going to break a problem down into a smaller version of the same problem, I can assume that the smaller version gives the answer. All I have to do is make sure that what I combined together gives me out the right result.
OK, you may be wondering what I'm doing with these wonderful high tech toys down here. I want to show you another example of recursion. So far we've seen simple things that have just had one base case, and this is a mythical story called The towers of Hanoi and this story, as I heard it, is there's a temporal somewhere in Hanoi with three tall spikes and 64 jewel-encrusted golden disks all of a different size. They all started out on one spike with the property that they were ordered from smallest down to largest.
And there are priests in this temple who are moving the disks one at a time, one per second, and their goal is to move the entire stack from one spike to another spike. And when they do nirvana is achieved and we all get a really great life. We'll talk separately about how long is this going to take because there's one trick to it. They can never cover a smaller disk with a larger disk as they're doing it, so they've got a third disk as a temporary thing. And I want to show you how to solve this problem because you're going to write code with my help in a second, or I'm going to write code with your help in a second to solve it.
So let's look at it, so watch carefully, moving a disk of size one, well that's pretty easy, right? Moving a disk of size two, we'll just put this one on the spare one while you move it over so you don't cover it up. That's easy. Moving a disk of size three, you've got be a little more careful, you can't cover up a smaller one with a larger one, so you have to really think about where you're putting it. It would help with these things didn't juggle and there you go, you got it done. All right, you're watching? You've got to do four. To do four, again, you've got to be really careful not to cover things up as you do this. You want to get the bottom one eventually exposed, and so are you going to pull that one over there. If you do the pattern really well, you won't notice if I make a serious mistake as I'm doing this, which I just did. But I'm going to recover from that and do it that way to put this one over here, and that one goes there, and if I did this in Harvard Square I could make money. There you go, right?
OK, got the solution? See how to solve it? Could you write code for this? Eh, maybe not. That's on the quiz, thanks, John, don't tell them on the quiz, damn. All right, I want to claim though that in fact there's a beautiful recursive solution. And here's the way to think about it recursively. I want to move a tower of size n, I'm going to assume I can move smaller towers and then it's really easy. What do I do, I take a stack of size n minus 1, I move it onto the spare one, I move the bottom one over, and then I move a stack of size n minus 1 to there, beautiful, recursive solution. And how do I move the smaller stack? Just the same way, I just unwind it, simple, and, in fact, the code follows exactly that.
OK, I do a little [INAUDIBLE] domain up here to try and get your attention, but notice by doing that what did I do? I asked you to think about it recursively, the recursive solution, when you see it, is in fact very straightforward, and there's the code. Dead trivial, well, that trivial is unfair, but it's very simple. Right? I simply write something, so let me describe it, I need to say how big of tower am I moving and I'm going to label the three stacks a from, a to, and a spare. I have a little procedure that just prints out the move for me, and then what's the solution? If it's just a stack of size one, just print the move, take it to from-- from from to to. Otherwise, move a tower of size n minus 1 from the from spot to the spare spot, then move what's left of tower size one from to two, and then take that thing are stuck on spare and move it over to two, and I'm done.
In that code that we handed out, you'll see this code, you can run it. I'm not going to print it out because, if I did, you are just going to say, OK, it looks like it does the right kind of thing. Look at the code, nice and easy, and that's what we like you to do when you're given a problem. We asked you to think about recursively. How do I solve this with a smaller version of the same problem? And then how do I use that to build the larger solution? This case is a little different. You could argue that this is not really a recursive call here, it's just moving the bottom one, I could have done that directly. But I've got two recursive calls in the body here. I have to move a smaller stack twice. We're going to come back to that in a little bit.
Let me show you one other example of recursion that runs a little bit differently. In this case it's going to have multiple base cases and this is another very old problem, it's called the Fibonacci numbers. It's based on something from several centuries ago when a gentleman, named Leonardo of Pisa, also known as Fibonacci, asked the following challenge. He said, I'm going to put a newborn pair of rabbits, one male and one female, into an enclosure, a pan of some sort. And the rabbits have the following properties, they mate at age one month, so they take a month to mature. After a one month gestation period, they produce another pair of rabbits, a male and a female, and he says I'm going to assume that the rabbits never die. So each month mature females are going to produce another pair. And his question was, how many female rabbits are there at the end of a year, or two years, or three years?
The idea is, I start off with two immature rabbits, after one month they've matured, which means after another month, they will have produced a new pair. After another month, that mature pair has produced another pair, and the immature pair has matured. Which means, after another month, those two mature pairs are going to produce offspring, and that immature pair has matured. And you get the idea, and after several months, you get to Australia. You can also see this is going to be interesting to think about how do you compute this, but what I want you to see is the recursive solution to it. So how could we capture this?
Well here's another way of thinking about it, after the first month, and I know we're going to do this funny thing, we're going to index it 0, so call it month 0. There is 1 female which is immature. After the second month, that female is mature and now pregnant which means after the third month it has produced an offspring. And more generally, that the n-th month, after we get past the first few cases, what do we have? Any female that was there two months ago has produced an offspring. Because it's taken at least one month to mature, if it hasn't already been mature, and then it's going to produce an offspring. And any female that was around last month is still around because they never die off.
So this is a little different. This is now the number of females at month n is the number of females T month n minus 1, plus the number of females and month n minus 2. So two recursive calls, but with different arguments. Different from towers of Hanoi, where there were two recursive calls, but with the same sized problem. So now I need two base cases, one for when n is equal to 0, one for when n is equal to 1. And then I've got that recursive case, so there's a nice little piece of code. Fibonacci, I'm going to assume x is an integer greater than or equal to 0. I'm going to return Fibonacci of x. And you can see now it says, if either x is equal to 0 or x is equal to 1 I'm going to return 1, otherwise, reduce it to two simpler versions of the problem but with different arguments, and I add them up.
OK, and if we go look at this, we can actually run this, if I can find my code. Which is right there, and I'm just going to, so we can, for example, check it by saying fib of 0. I just hit a bug which I don't see. Let me try it again. I'll try it one more time with fib of 0. Darn, it's wrong, let me try it. I've got two different versions of fib in here, that's what I've got going on. So let me do it again, let's do fib of 1. There we go, fib of 2 which is 2, fib of 3 just three, and fib of 4 which should add the previous two, which gives me 5. There we go. Sorry about that, I had two versions of fib in my file, which is why it complained at me. And which is why you should always read the error instructions because it tells you what you did wrong. Let's go on and look at one more example of doing recursion, and we're going to do dictionaries, and then we're going to pull it all together.
So far we've been doing recursion on numerical things, we can do it on non-numerical things. So a nice way of thinking about this is, how would I tell if a string of characters is a palindrome? Meaning it reads the same backwards and forwards. Probably the most famous palindrome is attributed to Napoleon "Able was I ere I saw Elba." Given that Napoleon was French, I really doubt he said "Able was I ere I saw Elba," but it's a great palindrome. Or another one attributed to Anne Michaels "Are we not drawn we few drawn onward to a new era," reads the same backwards and forwards. It's fun to think about how do you create the palindromes.
I want to write code to solve this. Again, I want to think about it recursively, so here's what I'm going to do. I'm first going to take a string of characters, reduce them all to lowercase, and strip out spaces and punctuation. I just want the characters. And once I got that, I want to say, is that string, that list of characters or that collection of characters as I should say, a palindrome? And I'm going to think about it recursively, and that's actually pretty easy. If it's either 0 or 1 long, it's a palindrome. Otherwise you could think about having an index at each end of this thing and sort of counting into the middle, but it's much easier to say take the two at the end, if they're the same, then check to see what's left in the middle is a palindrome, and if those two properties are true, I'm done. And notice what I just did I nicely reduced a bigger problem to a slightly smaller problem. It's exactly what I want to do.
OK? So it says to check is this, I'm going to reduce it to just the string of characters, and then I'm going to check if that's a palindrome by pulling those two off and checking to see they're the same, and then checking to see if the middle is itself a palindrome. How would I write it? I'm going to create a procedure up here, isPalindrome. I'm going to have inside of it two internal procedures that do the work for me. The first one is simply going to reduce this to all lowercase with no spaces. And notice what I can do because s is a string of characters. I can use the built in string method lower, so there's that dot notation, s.lower. It says. apply the method lower to a string. I need an open and close per end to actually call that procedure, and that will mutate s to just be all lowercase.
And then I'm going to run a little loop, I'll set up answer or ans to be an empty string, and then, for everything inside that mutated string, I'll simply say, if it's inside this string, if it's a letter, add it into answer. If it's a space or comma or something else I'll ignore it, and when I'm done just return answer, strips it down to lowercase. And then I'm going to pass that into isPal which simply says, if this is either 0 or 1 long, it's a palindrome, returned true. Otherwise, check to see that the first and last element of the string are the same, notice the indexing to get into the last element, and similarly just slice into the string, ignoring the first and last element, and ask is that a palindrome. And then just call it, and that will do it. And again there's a nice example of that in the code I'm not going to run it, I'll let you just go look at it, but it will actually pull out something that checks, is this a palindrome.
Notice again, what I'm doing here. I'm doing divide-and-conquer. I'm taking a problem reducing it, I keep saying this, to a simpler version of the same problem. Keep unwinding it till I get down to something I can solve directly, my base case and I'm done. And that's really the heart of thinking about recursive solutions to problems. I would hope that one of the things I remember, besides my really lousy patter up here, is the idea of Towers of Hanoi, because to me it's one of the nicest examples of a problem that would be hard to solve iteratively, but when you see the recursive solution is pretty straightforward. Keep that in mind as you think about doing recursion.
OK, let's switch gears, and let's talk very briefly about another kind of data type called a dictionary. And the idea of a dictionary I'm going to motivate with a simple example. There's a quiz coming up on Thursday. I know you don't want to hear that, but there is, which means we're going to be recording grades. And so imagine I wanted to build a little database just to keep track of grades of students. So one of the ways I could do it, I could create a list with the names of the students, I could create another list with their grades, and a third list with the actual subject or course from which they got that great. I keep a separate list for each one of them, keep them of the same length, and in essence, what I'm doing here is I'm storing information at the same index in each list. So Ana, who's going to have to take the class again, gets a B, John, who's created the class, gets an A plus, Sorry Ana, John's had a longer time at it.
All right, bad jokes aside, what I'm doing is I can imagine just creating lists. I could create lists of lists, but a simple way is to do lists where basically at each index I've got associated information. It's a simple way to deal with it. Getting a grade out takes a little bit of work because if I want to get the grade associated with a particular student, what would I do? I would go into the name list and use the method index, which you've seen before, again notice the dot notation it says, this is a list, use the index method, call it on student, and whatever the value of student is, it will find that in the list, return the index at that point, and then I can use that to go in and get the grade in the course and return something out.
Simple way to do it but a little ugly, right, because among other things, I've got things stored in different places in the list. I've got to think about if I'm going to add something to the list I've got to put them in the same spot in the list. I've got to remember to always index using integers which is what we know how to do with lists, at least so far. It would be nice if I had a better way to do it, and that's exactly what a dictionary is going to provide for me. So rather than indexing on integers I'd like to index directly on the item of interest. I'd like to say where's Ana's record and find that in one data structure. And so, whereas a list is indexed by integers, and has elements associated with it, a dictionary is going to combine a key, or if you like, a name of some sort, with an actual value. And we're going to index just by the name or the label as we go into it. So let me show you some examples.
First of all, to create a dictionary I use curly braces, open closed curly brace, so an empty dictionary would be simply that call. If I want to create an actual dictionary, before I insert things into it, I use a little bit of a funky notation. It is a key or a label, a colon, and then a value, in this case the string Ana and the string b, followed by a comma which separates it from the next pairing of a key and a label, or a key and a value, and so on. So if I do this what it does in my dictionary is it creates pairings of those labels with the values I associated with them. OK, these are pretty simple, but in fact, there's lots of nice things we can do with it.
So once we've got them indexing now is similar to a list but not done by a number, it's done by value. So if that's my key, I can say, what's John's grade, notice the call, it's grades, which is in my dictionary, open close square brackets, with the label John. And what it does, it goes in and finds that in the dictionary, returns the value associated with it. If I ask for something not in the dictionary, it's going to give me a key error. Other things we can do with dictionaries, we can add entries just like we would do with lists. Grades as a dictionary, in open and closed square brackets, I put in a new label and a value, and that adds that to the dictionary.
I can test if something's in the dictionary by simply saying, is this label in grades, and it simply checks all of the labels or the keys for the dictionary to see if it's there, and if it's not returns false. I can remove entries, del, something we've seen before, a very generic thing. It will delete something, and in this case, it says, in the dictionary grades, find the entry associated with that key, sorry, Ana, you're about to be flushed, remove it. She's only getting a b in the class and she teaches it. We've got to do something about this, right? So I can add things, I can delete things, I can test if things are there.
Let me show you a couple of other things about dictionaries. I can ask for all of the keys in the dictionary. Notice the format, there is that dot notation, grades as a dictionary, it says, use the keys method associated with this data structure dictionaries. Open close actually calls it, and it gives me back a collection of all the keys in some arbitrary order. I'm going to use a funny term here which I'm not certain we've seen so far. It returns something we call an iterable, it's like range. Think of it as giving us back the equivalent of a list, it's not actually a list, but it's something we can walk down. Which is exactly why I can then say, is something in a dictionary, because it returns this set of keys, and I can test to see something's in there. I can similarly get all of the values if I wanted to look at them, giving us out two iterables.
Here are the key things to keep in mind about dictionaries. The values can be anything, any type, mutable, immutable. They could be duplicates. That'd actually makes sense, I could have the same value associated, for example, the same grade associated with different people, that's perfectly fine. The values could be lists, they could be other data structures, they could even be other dictionaries. They can be anything, which is great.
The keys, the first part of it are a little more structure. They need to be unique. Well duh, that make sense. If I have that same key in two places in the dictionary, when I go to look it up, how am I going to know which one I want? So it needs to be unique, and they also need to be immutable, which also makes sense. If I'm storing something in a key in the dictionary, and I can go and change the value of the key, how am I going to remember what I was looking for? So they can only be things like ints, floats, strings, tuples, Booleans. I don't recommend using floats because you need to make sure it's exactly the same float and that's sometimes a little bit challenging, but nonetheless, you can have any immutable type as your key. And notice that there's no order to the keys or the values. They are simply stored arbitrarily by the Python as it puts them in.
So if I compare these two, lists or ordered sequences indexed by integers, I look them up by integer index, and the indices have to have an order as a consequence. Dictionaries are this nice generalization, arbitrarily match keys to values. I simply look up one item by looking up things under the appropriate key. All I require is that the keys have to be immutable.
OK, I want to do two last things I've got seven minutes to go here. I want to show you an example of using dictionaries, and I'm going to do this with a little bit more interesting, I hope, example. I want to analyze song lyrics. Now I'm going to show you, you can already tell the difference between my age and Ana's age. She used Taylor Swift and Justin Bieber. I'm going to use The Beatles. That's more my generation. Most of you have never heard of The Beatles unless you watched Shining Time Station where you saw Ringo Starr, right?
OK, what I'm going to do is, I want to write a little set of procedures that record the frequencies of words in a song lyric. So I'm going to match strings, or words, to integers. How many times did that word appear in the song lyric? And then I want to ask, can I easily figure out which words occur most often, and how many times. Then I'm going to gather them together to see what are the most common words in here. And I'm going to do that where I'm going to let a user say, I want every word that appears more than some number of times. It's a simple example, but I want you to see how a mutation of the dictionary gives you a really powerful tool for solving this problem.
So let's write the code to do that. It's also in the handout, here we go. Lyrics to frequency's, lyrics is just a list of words, strings. So I'm going to set up an empty dictionary, there's that open close curly brace, and here's what I want to do. I'm going to walk through all the words in lyrics. You've seen this before, this is looping over every word in lyrics. Ah, notice what I'm going to do. I'm going to simply say-- so the first part is, I can easily iterate over the list, --but now I'm going to say, if the word is in the dictionary, and because the dictionary is iterable, it's simply going to give me back all of the keys, it's simply going to say, in this case, if it's in the dictionary, it's already there, I've got some value associated with it, get the value out, add 1 to it, put it back in. If it's not already in the dictionary, this is the first time I've seen it, just store it into the dictionary. And when I'm done just return the dictionary. OK?
So I'm going to, if I can do this right with my Python, show you an example of this. I have put in one of the great classic Beatles songs, you might recognize it right there. Mostly because it's got a whole lot of repetitions of things. So she loves you yeah, yeah, yeah, yeah. Sorry, actually they sing it better than I just did it sarcastically. Sorry about that, but I got she loves you there, and here's my code up here, lyrics to frequency. So let's see what happens if we call it. And we say lyrics to frequencies she loves you. And it would help if I can type, all right, we'll try it one more time, lyrics to frequency's, she loves you. Cool, this gave me back a dictionary, you can see the curly braces, and there are all the words that appear in there and the number of times that they appear.
What's the order? You don't care. You don't know. What we want to do is to think about how can we analyze this, so let's go back and look at the last piece of this. Which is, OK, I can convert lyrics to frequencies. So here's the next thing I want to do, how do I find the most common words? Well, here's what I'm going to do, frequencies is the dictionary, something that I just pulled out. So I can use the values method on it which returns and iterable, as I said earlier, again notice the open close because I got to call it.
That gives me back an iterable that has all of the frequencies inside of there, because it's an iterable, I can use max on it, and it will take that editable and give me back the biggest value. I'm going to call that best, I'm going to set up words to be an empty list, and then I'm just going to walk through all of the entries in the dictionary saying, if the value at that entry is equal to best add that entry into words, just append it onto the end of the list. And when I'm done all of that loop, I'm just going to return a tuple of both the collections of words that period that many times and how often they appeared. I'm going to show you an example in a second, but notice I'm simply using the properties of the dictionary.
The last thing I want to do then is say, I want to see how often the words appear. So I'm going to give it a dictionary and a minimum number of times. And here I'm going to set result up to be an empty list, I'm going to create a flag called false, it's going to keep track of when I'm done. And as long as I'm not yet done, I'll call that previous procedure that's going to give me back the most common words and how often they appeared. I check and remember it was a tuple, how often do they appear, if it's bigger than the thing I'm looking for, I'll add that into my result.
And then the best part is, I'm now going to walk through all the words that appeared that many times, and just delete them from the dictionary. I can mutate the dictionary. And by doing that, I can go back around and do this again, and it will pull out how many times has this appeared and keep doing it. When I can go all the way through that, if I can't find any more, I'll set the flag to true which means it will drop out of here and return the result. I'm going to let you run this yourself, if you do that, you'll find that it comes up with, not surprisingly, I think yeah is the most common one and she loves you, followed by loves and a few others. What I want you to see here is how the dictionary captured the pieces we wanted to.
Very last one, there's Fibonacci, as we called it before. It's actually incredibly inefficient, because if I call it, I have to do all the sub calls until I get down to the base case, which is OK. But notice, every other thing I do here, I've actually computed those values. I'm wasting measures, or wasting time, it's not so bad with fib of 5, but if this is fib of 20, almost everything on the right hand side of this tree I've already computed once. That means fibs very inefficient. I can improve it by using a dictionary, very handy tool. I'm going to call fib not only with a value of n, but a dictionary which initially I'm going to initialized to the base cases. And notice what I do, I'm going to say if I've already computed this, just return the value in the dictionary. If I haven't, go ahead and do the computation, store it in the dictionary at that point, and return the answer.
Different way of thinking about it, and the reason this is really nice is a method called memoization, is if I call fib of 34 the standard way it takes 11 million plus recursive calls to get the answer out. It takes a long time. I've given you some code for it, you can try it and see how long it takes. Using the dictionary to keep track of intermediate values, 65 calls. And if you try it, you'll see the difference in speed as you run this. So dictionaries are valuable, not only for just storing away data, they're valuable on procedure calls when those intermediate values are not going to change. What you're going to see as we go along is we're going to use exactly these ideas, using dictionaries to capture information, but especially using recursion to break bigger problems down into smaller versions of the same problem, to use that as a tool for solving what turn out to be really complex things. And with that, we'll see you next time.