Hi, I'm Joe James. I have a master's degree in computer science and I'm a software engineer in Silicon Valley and in this course We're going to learn about data structures in Python. You might be wondering, well, why should I care about data structures in Python?  You wouldn't try to pound in a nail with a screwdriver. That just doesn't make sense. Carpenters know you can't do that. You also wouldn't try to drive in his screw with a pair of pliers. So carpenters know that for every task there's a best tool for the job. And that's why carpenters carry around a tool belt full of tools and those tools are specialized for different tasks. And that's exactly what you're going to do when you master data structures in Python. Data structures are your tool belt. For each task you face as a programmer, you're going to know exactly which data structure to use and how to use it. You're going to save time, write better code, and do it more efficiently. So in this course we're going to learn about Python's built-in data structures, strings, lists, tuples, sets, and dictionaries. Then we're going to continue to learn about queues, stacks, and heaps. We're also going to learn about linked lists. Then we're going to cover binary search trees and graphs. You're going to learn how to use these data structures, how to implement them in Python, and you're going to learn the strengths and weaknesses of each of these data structures. So let's look at some of the code. First we'll look at sequence types, string, list, and tuple. So I put a link here to the documentation. This is the official Python documentation. I encourage you to check that out if you have any questions or you want more detail on any of these items. Just look at the documentation link here. So first is indexing. We can access any item in a sequence by using its index. We put that index inside square brackets. An indexing starts with zero. The first element is always zero. So the fourth element is going to be index three. So here we have a string frog that has four letters in it. If we want the fourth element, which is g, we print out x of three. That gives us the g as you can see in the output here. If we have a list of pig, cow, and horse, these are strings. We can print out x of one, and that's going to give us cow. And again, it's just square brackets. And a tuple with four names in it, and if we want the very first name, we print out x of zero with the zero in square brackets, and that gives us Kevin. So that's indexing. Now let's look at slicing. We can slice out substrings, sublists, or subtuples using indexes. So the way indexing works is you put inside the square brackets separated by colons three possible parameters. So we can put a start, an end plus one, and a step. And I'll show you what each of these means. So let's just use a string for this example. Computer, the word computer. Again, the c is index zero. So here we're going to print out x of one to four. So since we didn't put a third parameter, that means the step is assumed to be one. That's the default step. So when we put one, that means the o. And that is inclusive. The from is, or the start is always inclusive. The end is non-inclusive. So if we look at item number four, that's the u. That's the fifth item. It's non-inclusive. So it's really just going to get omp for us. And when we look at the result, it prints out omp. Now here's an example using a step. So we print out x of one to six. Again, we're going to start at the o because the one is inclusive. And we're going to go up to six, which is the e, but it's non-inclusive. And we're going to do it in a step of two. So in other words, we're going to get the o, the p, and the t. And that's what we print out, o, p, t. So it takes every other item when we have a step of two. Next, we're going to not put an end plus one. We're just going to leave an open colon there. And what happens is when we put the open colon, three colon nothing, basically the default is end of string. So everything from the third item on. So here the third item is p because we start counting at zero. The third item is p and we get everything from p onward, p-u-t-e-r. So if you don't know how long a string is, or you just want to get all of the remaining elements to skip the first three items or something, you use an open colon. Next, we're not going to declare a start. So we can skip the start by just putting colon five. So in other words, our default is we're going to start from the beginning of the string. We're going to get up to the sixth item because the five is non-inclusive. So c-o-m-p-u. And you can see our result here is c-o-m-p-u. So you can see if you don't declare a start, the start defaults to the beginning. And if you don't declare an end, the end defaults to the end. And if you don't declare a step, the step defaults to one. So that's basically what's happening here. Let's look at a negative index. So if we print out just a negative index, negative one, that counts from the right side of the string. So here we want the very last item in the string. We put negative one, we get the r, the last element. And then here we get the last three items. So we put a negative three as the from and to the end of the string. So that gives us T-E-R, that gives us the last three elements. And then if we want to get everything except the last two elements of the string, we leave the start blank so we get everything from the beginning up until the last two items. So that is how slicing works. And it works exactly the same as we just covered for this string example. Works exactly the same on lists and tuples. Now let's look at adding and concatenating. So we can combine two sequences of the same type using the plus sign. Let's look at a string example first. We want to combine horse and shoe and we just print x. So we see the result is horseshoe. If we have two separate lists and we want to merge those two lists together, we can use a plus sign. And when we print that, we get a single list with three elements in it. And in the case of tuples, if we have two separate tuples and we print the result, we get a single tuple with four elements in it. Now it's important to note here for the second one to be considered a tuple, we have to include a comma here. If we don't have that comma, it's just a string in parentheses. If we include this comma, that tells Python that this is actually a tuple. We can use the multiplying function to multiply a sequence using the asterisk. In a string example, if we want to print bug three times, we just do bug times three and then print. And you can see the result here is bug bug bug. And the same with a list. We have a list eight comma five and we want to multiply that by three. What we get is eight five eight five eight five. So we're not actually multiplying the elements by three. We're multiplying the list by three. And then the same with tuple.  Now let's look at testing membership. We can test whether an item is in or not in a sequence. These are really easy. It's almost English keywords in Python. They mean it's so simple. So with a string, let's say we have a string called bug and we want to check if the letter U is in our string. We just say U in X. And that's going to give us a Boolean result, true or false. In this case, U is in X so it returns true. And now we have a list, pig, cow, and horse. If we print cow not in Y, it's going to print false because cow is in Y. And for a tuple example, we have a tuple here with four names and if we print one of those names in Z, we get true because it is in Z. So it's really easy to check membership using in or not in. If we want to iterate through the items in a sequence, we can say four item in X. An item can be any variable name you want. It can be four number in X or whatever you like. Here I just used a variable name item, print item. If you want both the index and the item, here we have a list with seven, eight, and three in it. We say index, item in enumerate X. The enumerate is going to return both an index and an item. So actually, again, these variable names are arbitrary. The first one is going to be the index. The second one is going to be the item itself. So you can name them whatever variable name you like, but you can get both the index and the item using the enumerate function. And then we have access to both the index and the item, as you can see the results. If you want to get the count or the number of items in a sequence, you can just use the len function. So in the string example, we have bug. We get len of X, three. This list, we have three items in the list. We print len of Y, we get three. In our tuple, the len of Z is four. To find the minimum item, Python checks this lexical graphically, which means the smallest on the ASCII scale. So you can use the minimum function on either alpha or numeric types, but you cannot mix alpha and numeric types into a list or a tuple. You'll get an error. So here in our string example, we print the min of X. We get the smallest letter, which is B. And in the list, we print the minimum of Y, which is cow, because it basically is going to compare the first letter first and see it comes first alphabetically. And the same with the tuple, we print the one that comes first alphabetically and that's Craig. So that's the min. The maximum item in a sequence, again, lexical graphically, and it can be done alpha or numerically, but not both. So with bug, the maximum is U. And with pig, cow, and horse, we see that pig actually comes last alphabetically. And in our tuple example, N is the last letter alphabetically. We can find the sum of items in a sequence. They have to be numeric. So if you mix in other items that are non-numeric, let's say strings or something, it's going to give you an error. So in the case of a string, we throw a string in here and we find that we print some, you're just going to get an error. But if we have a list of numbers and we print the sum of that list, you can see we get 27. You can also do slicing. You can combine slicing to get a sum of part of the list. So if here we want to just get the last two numbers, 8 and 12, we can do negative 2 onward. And that gives us 20 because we're adding 8 and 12 only. And for the tuple, we have another tuple, Z, with four items in it and we add those together, to get 80. Sorting returns a new list of items in sorted order, but it returns it as a list. So here we have a string, bug. We print the sorted version of X and what we get back is the letters basically separated and sorted as a list, elements of a list. Our list example, it sees that these are strings. It puts them in sorted order and it's important to understand that this does not change the original list. The sorted function is not an in place sort. It returns a new list with a sort result. And our tuple example, we have four names here and we put those in sorted order and get Craig, Jenny, Kevin, and Nicholas. So let's say you don't want to sort by the first letter though. You want to sort instead by the second letter. Well you can use a lambda function to do that. I'm not going to cover lambda functions in detail in this video, but I want to make you aware that you can sort stuff either by reverse order or using some other parameter. And we do that using key equals some lambda function. And here we for each item, K, again you can use as an arbitrary variable named K, we're going to take the one item which is the second letter. So here it's going to be E, I, E, and R. And we're going to put those in sorted order based on the second letter. And we can see those with the second letter in sorted order. If we want to get the count of items in a sequence, we can use the count function. And here we're going to, we have the word hippo in a string. We're going to count the number of times the letter P appears in hippo and we can see that the result is 2. Here I added the word cow to the list twice. So if we get the count of the word cow, we see that that also is 2. And here we just get the word count of Kevin in this list and we can see that it's 1. And we can get the index of an item by passing in that item and asking for the index of it. And what it's going to give us is the index of the first occurrence of that item. So in the case of hippo, if we're looking for P, it's going to give us, let's see, H is 0, I is 1, the first P is 2. So we can see the result we get is 2. It stops looking after it finds the first item that matches in the sequence. And our list example, we have two cows in here and we're going to get the index of cow and we're going to get 1 as a return value. And then for the tuple, we get the index of Jenny and we can see that's 0, 1, 2. So unpacking of items in a sequence, we can unpack those into a number of variables. It's important that our number of variables exactly matches the length of that list or string because if not, we're going to get an error. So here we have a list x equals pig, cow, and horse and if we want to unpack those and assign each of these values to its own variable, we can say a, b, c equals x. And then that's basically going to put these in order, assigning them to a, b, and c. So when we print out a, b, and c now, these are each separate variables that we have on horse. So that's called unpacking. In the next lecture, we'll learn more detailed features of lists, tuples, sets, and dictionaries. So now let's dig into some of the specifics of lists, tuples, sets, and dictionaries. Again, as a recap, lists are the most general purpose data structure in Python. You're going to use these for almost everything in Python. I should say a lot of stuff. And lists can grow and shrink in size as needed, so you can continue adding items to them or deleting items from them and the size of the list will shrink accordingly, automatically. Python does that for you. And lists are a sequence type, so all of the sequence functions that we covered above are all useful for lists, and they're also sortable. A lot of data structures are not sortable, lists are. That makes them useful for sorting data. So let's look at some of the constructors for lists. How do we create a new list? So there are a few different ways of doing this. One, we can create an empty list by just saying x equals list and then parentheses. That calls the list constructor with no parameters, and it gives us a new empty list. And another way to do it is, this is probably the most common way is to pass in the items we want in that list inside square brackets. These square brackets, we can separate each item with a comma. We can pass in, here we have multiple different data types. We have strings, we have integers and floating point values all in the same list. That's one nice thing about the versatility of lists you can see here. We can also create a tuple which will cover tuple constructors in a minute, but as we create a new tuple and we can pass that tuple into the list constructor just by putting it inside the parentheses in the list constructor. And that will create a new list and assign it to z. And lastly, we can use list comprehensions. I'm going to have another section on list comprehensions in a few minutes. So, I just wanted to give you a little teaser of what you can do with list comprehensions to create new lists with sets of values. So, here we're going to create a new list called a, and we put square brackets and basically inside of that is a for loop and the range function. So, we can say m for m in range 8. That's going to count from 0 to 7 and for each value m it's going to assign m to the list. So, here we get a new list with values 0 through 7 in it. And then if we want to do something more fancy here's just a taste of it. i in range 10. So, we're going to count 0 through 9. And we're only going to take the ones that are greater than 4. If i is greater than 4 then it will pass i squared into the list. So, here we get 5 through 9 squared into this new list. So, that's a taste of what you can do with list comprehensions to create a new list using a for loop and the range function. You can also add if to filter items and you can do whatever you want to the items that you're iterating. Now, let's take a look at the delete function. If we want to delete a single item from a list or we want to delete the entire list we can do that using dell. Here we have a list called x and we have 5, 3, 8, 6 in it. And if we want to delete the 1th item which is the 3 and we can just pass in the 1 in the square brackets, the index of the item. dell x of 1 that deletes the 1th item, the 3 and we can see the new list there. If we want to delete the entire list we just say dell x. Next, the append function if we want to add an item onto the list this is going to add it to the tail end of the list. We create a new list 5, 3, 8, 6 and we do x.append and then we pass in that 7 as an argument and we can append 7 to the tail of the list. Extend basically is similar to the plus function that we used up above. We're basically combining two separate lists into one list. So here we have x equals 5, 3, 8, 6 y equals 12, 13 and we can extend x with y and then we print out the new x and you can see we have all six items in it. We could also have used the plus for that. So insert, we can insert an item at a given index in the list. Here we have the same list we used above and we insert at the 1th position the item 7. So here we can see the result is 5, 7, 3, 8, 6. This is the position or the index we want to insert it at and this is the item we want to insert. And then we can see here that you can not only insert an integer or a floating point value, you can insert a list into a list as an item. We have a list here with a and m as two items and then we print out the revised list and we see that the second item or the 1th item in the list is another list with a and m in it. So that's the insert function. So let's take a look at pop. Pop basically pops off the last item from the list and it returns that item. So you can use that item if you want to. You don't have to. But you are basically shrinking your list by one item. So here we have 5, 3, 8, 6. As we pop off one item using x.pop that pops off the last item of 6. We didn't assign it to anything or do anything with it but we can see the new list is just 5, 3, 8. So we print x.pop and it pops off the 8, the last item on the list now. And we print that. So we can see that the return value is 8 when we do x.pop. Remove. We can remove the first instance of an item. So if there are multiple instances of an item Python is going to start searching at the beginning of the list until it finds that item that matches. It's going to stop searching and it's going to remove that item. It's going to remove the very first one. So if we do x.remove3 we can see the revised list is without the first three. Reverse function can reverse the order of a list. It's an in-place reverse which means that it changes the original list. The original list is no longer the same as it was. So here we have our original list of x equals 5, 3, 8, 6 and then we apply reverse to it. It's not putting these in sorted order but reversing the order of the items. So we get 6, 8, 3, 5 as the reversed list. And then we can apply the sort function to it which is also an in-place sort. You should note that we can use sorted of x. These are Python functions. There are two different ones and they're a little bit confusing here. Sort is an in-place sort. Sorted returns a new list. So it's not an in-place sort. So here using the x.sort function we don't pass anything in as a parameter to the sort function. We're applying the sort function to the x which is what's calling the sort function. And we can see that we put these items in sorted order. And then if you want to do a reverse sort we can pass into the sort function a parameter called reverse equals true. This gives you a descending sort. So we get 8, 6, 5, 3 if we try and use reverse equals true. And again this is the same parameter that you would use in the sorted function if you wanted to reverse sort using sorted function. But this one is an in-place sort. As you can see Python lists are a really powerful data structure and they have a lot of built-in functions and features. But unless you want to become the carpenter who tries to turn every problem into a nail by pounding on it with a hammer, let's continue on in the course and learn other data structures and see what they can be used for. So let's take a closer look at tuples. Just to recap we said that tuples are immutable. That means they can't be changed and you can't add items to the tuple once it's created. They're useful for fixed data. If you're going to have a lot of changes to your data then you should use lists. They're useful for fixed data. They're much faster for finding items than a list is. And these are sequence types which means that all of the above functions are still going to work. So you can use all of those sequence functions that we've used above on tuples. So let's take a look at some of the constructors for tuples. How to create a new tuple. There are a few different ways of doing this. A tuple uses the parentheses as its constructor. So here we can create an empty new tuple using x equals parentheses, empty parentheses. Or if we want to pass in items one two three. The parentheses are actually optional so even when you take the parentheses away one two three separated by commas Python knows that this is a tuple. Now if you want a tuple of just one item you still have to put the comma. That comma tells Python that this is a one item tuple and not just an integer. And then you can see that we print here x and the type of x and we get a tuple with just the two in it. And the class is a tuple. Now let's create list one equals two four six. This is a list with three items in it. We pass that list into the tuple constructor and it creates a tuple called x. So here is we print out x which is that tuple two four six. You can now see that it doesn't have square brackets. It has the parentheses around it because it's a tuple. And we print out the type of x and it's a class tuple. So there are several different ways there to create tuples. Tuples again are immutable. However this may be a little confusing so pay attention. Member objects may be mutable. If you have a list as one of the items inside a tuple you can make changes to that list. You can add or delete items from that list. You can change the items in the list. So let's take a look at what we mean here. If we have a tuple with one two three in it and then we try to delete the one item which is the two that's going to fail. That's going to give us an error in Python. If we try to change the value of the two to eight that also fails. We cannot change the value of the two. So it looks like the tuple is totally immutable and unchangeable. And we get one two three. So even if you try those you're just going to get an error. But look at this. If we assign a list a two item list as the zeroth item in this tuple well that list we can just mutable. So we can change or drop items off of this list if we want. So here we're going to pass in two indices. The zero tells Python that yeah we want the zeroth item of tuple y which is this list with one and two in it. And in that list we want the one item which is the two. And that's what we're going to delete. So we're basically deleting this two from this list. And then we're going to print out y and we can see that the result is we get a single item list with the one and the three. So we were able to edit this list with the one and two. We're able to drop items off of that list. And then also if we want to add items to the tuple we can not just add or append. However we can use this concatenation function where we do y plus equals an additional tuple and it will merge the two tuples into one. So here again you need the comma to tell Python that this is a tuple and not just an integer. It's a one item tuple. So if we do y plus equals four we can see that the four is added on to our original tuple y. So concatenating will work. Now let's look at sets. Sets store non-duplicate items. So unique items are really what sets are ideal for. You get very fast access compared to lists. And the reason why is you iterate through a list looking for an item. The only way to do it is to start at the beginning and look at every single item and do a comparison. So if you have a billion items in that list you may have to do a billion comparisons to find that item. But in a set it hashes that item. So it can find it instantly using the hash. It has much faster access than lists. So especially for very large data sets it has much faster access to items than lists. It's great for checking membership. The set is also great for doing math set operations. Things like union and intersection. And keep in mind that sets are unordered which means you cannot sort a set. So let's take a look at the constructors for a set. Here are a few different ways to create a new set. We can use these curly braces and you'll see here as we pass in three five three five we've got some duplicates there. This is it filters out the dupes and gives us a set with just three and five in it. And if we create a new empty set we can just use the set constructor with parentheses and then we print out why you can see we get an empty set. If we want we can also pass in a list. Here we have two three four and we call the set constructor using the parentheses and are passing our set as a parameter. And we get a new set Z and when we print that out we get two three four set. So that's a few different ways to create sets. Some of the set operations you can use you can add an item to a set by using x dot add. So here we add a seven to this list of three eight five and then we remove a three using x dot remove. So you can see the result here is that after you add the seven you get the four item list and when you delete the three you get back down to eight five and seven. So add and remove both work for sets and then if you want to get the length of a set you just use len checking membership we use in or not in so if we want to check if five is in the set we just do five in x or five not in x and that's going to give us a boolean return here we can see we got true for five in x and then we can also pop a random item the set is not ordered so we don't know which item we're going to get we're going to get a random item off the set and then here actually the pop function returns the item itself and so we're printing out that item and the new list x so here we can see the item it gave us is eight and the new set is five and seven and then if we want to delete all the items from the set and get an empty set back we can do x dot clear let's look at some of the mathematical set functions so we said that we can do intersection and union which are and and or functions so the intersection is done using an ampersand with two sets and the union is done using the pipe or bar so set one pipe set two symmetric difference or exclusive or in other words in items that are in set one but not in set two or in set two but not in set one and the difference we can just use a subtraction so set one minus set two gives us the difference between those two and then we can check if one set is a subset or fully contained in the other set using the less than or equal to or greater than or equal to super super set so we have two different sets here set one and set two and when we do the intersection we can see that the intersection is three they both have this value three when we do the union we get all the items that are in either set so one two three four five so when we do exclusive or using the up carat we get one two four five which is all the items that are in one set or the other but not in both and then minus we get one and two and then since neither set is a subset of the other set both of these two return false so those are some of the mathematical set operations you can do on sets now let's take a look at dictionaries so first to recap on dictionaries dictionaries are key value pairs so most programming languages have some equivalent of the python dictionary they don't always call it that some of them call it a hash map java calls it a hash map dictionaries are unordered that means they cannot be sorted they can be converted to a list and then sort it as a list but they cannot be sorted as a dictionary so some of the functions that we can do how do we create new dictionaries let's take a look here at our constructors so if we create a new dictionary using the curly braces then we need to pass in members key value pairs separated by a colon and then spaced out with commas ok so here we have three key value pairs the key is on the left a colon and then the value and these are three different ways of creating exactly the same dictionary so in the second example we pass in a list of tuples so the tuple contains two items it has a a string and separated by a comma the floating point so there are three tuples in a list passed into the dictionary constructor which is in parentheses and then the third one passes into the dictionary constructor notice there are no quotation marks around the strings here we just have pork equals 25.3 and python knows that this is a string so these are three different ways to create dictionaries in python they all do exactly the same thing some of the operations of dictionaries now we notice that shrimp is not in our dictionary so if we want to add shrimp we can say x of shrimp equals 38.2 this in this case there is no shrimp in the dictionary yet so it's going to add a new key value pair for shrimp 38.2 if there already was a shrimp in the dictionary then it would update the value to 38.2 for shrimp it looks up this key and it will update the value for it so this is add or update and python is not going to tell you if that was in there or not if you want to check you can check first if shrimp in dictionary but if you just do this x of shrimp equals 38.2 it's going to overwrite anything that was already in the dictionary for shrimp if you want to delete an item this is just dell x of shrimp is going to delete shrimp from the dictionary and then you can see that we print out the new dictionary there is no shrimp in it and if you want to get the length of the dictionary you can print lin of x that will tell you how many key value pairs are in the dictionary and if you want to delete all the items from dictionary you can use x.clear and lastly just delete the entire dictionary and free up the memory that it's using you can use dell x so to access keys in the dictionary you can access these separately or you can access them together so here are a few different ways of doing that we have this dictionary y with pork, beef and chicken in it those are the keys, the strings pork, beef and chicken so if we do y.keys we get a list of pork, beef and chicken it dumps these out as a list if we do y.values it dumps these out as a list of point values and if we do items we can say print y.items it's going to print out all the key value pairs so here it prints them out as a list of tuples or key value pairs to check membership in just the keys you don't have to specify keys you can if you want you can say beef in y.keys or you can just simply say beef in y and that's going to check in y's keys only it's not going to check in the values if you want to check for membership only in the values you can check clams in y.values and all these membership tests are going to have a boolean return true or false so to iterate a dictionary keep in mind that these items are in random order and you're not going to be able to iterate them in any kind of sorted order python is going to give them back to you on whatever order it wants so for key in y print key that will give you all the keys in the dictionary one at a time and then you can get the value by saying y of key so here you can see we printed out each key and its value if we want to iterate with a separate variable for both the key and the value sometimes this is helpful if you're doing a lot of operations inside the loop you can put whatever variables you want I used k, v as my variable names and what you do is you iterate y.items and then items returns a tuple a two item tuple of the key and the value and it assigns to whatever variable names you have here so k and v in my case so you can see the result here is the same we iterate through the items we print out both the key and the value so that wraps up this video on built-in python data structures now you should have a pretty good understanding of how to use pythons built-in data structures strings, lists, tuples, sets and dictionaries make sure you download the code and get some practice using it because its hands on practice its going to make you a good programmer in the next section we'll learn how to use list comprehension to create new lists hi I'm Joe this chapter we're going to cover a pretty cool feature of python called list comprehensions that enables you to create new lists of values using a comprehension or basically sort of a for loop and iteration inside of a list creator so our basic format is transform sequence and filter so we can apply a filter to it if we want and we put that inside square brackets and that's going to the result is going to be assigned to a new list so we're going to use the random module a little bit in this you don't need that for all list comprehension but for one or two of my examples so I'm going to import that so we have a series of about 10 examples or something I'll show you they get increasingly more complex so here we're going to just get values within a range typically in list comprehensions you're going to use the range function here we just use range of 10 and you know the range function returns of numbers in this case it starts with 0 which is a default and it goes up through 9 because the 10 is non inclusive so 0 through 9 and what we're going to add to this new list is x for x in that range so in this part the first x we could apply some sort of a transform or a function on that x if we wanted to x squared 2x whatever we want and then this is where we declare the variable each x in this range so the result is a series of values under 10 so 0 through 9 under 10 integers okay so that's the simplest example of a list comprehension now let's look at some of the other more crazy stuff that you can do with list comprehensions we could get all the squares if we want I told you we could apply a transformation to the x if we want so here we declare our variable is x as we iterate through under 10 which is this list we just created okay so we don't necessarily have to use the range function we can use any sequence here which means we could use a list we could use a tuple a set or even a string or a range function so x squared for x in under 10 so it's going to iterate through these the square of each one of them and it's going to assign that to this new list called squares and then we're going to print out squares so you can see the result is 0 through 81 the squares of the previous list okay let's see what else we can do get odd numbers using mod okay so odds equals x for x in range 10 so here we're going to just basically iterate through 0 through 9 the variable we're going to use is called x and we're going to send x to this odds list but here look we apply a test a condition if x mod 2 is equal to 1 in other words if it's odd if x is odd then we'll send it to this odds list when we print it out we see that we get 1, 3, 5, 7 and 9 now let's get the multiples of 10 so we're going to use the range function again as our sequence 0 through 9 and instead of adding x to the list we're going to add x times 10 so this is not a whole lot different from the x squared we did we get the multiples of 10 so 0 through 90 now let's get all the numbers from a string so we start out with a string named s that has a combination of letters and numbers in it maybe sometimes you want to filter out and delete all the numbers or whatever but here what we're going to do is just create a new list with all those numbers so nums equals x for x in s in other words we're going to iterate through the letters or characters in s and we're going to test if each one is numeric and if it is then we're going to add it to this list and then when we print out the list we're basically we get a list so I'm going to use this little join function to join the numbers into a single single string so we get 2073 in other words we managed to grab all the integers in this string so here we're going to get the index of a list item and we're going to do that by using the enumerate function so we iterate using enumerate names name this is a list and we're going to enumerate enumerate returns both a key and a value for each item in the list so we start out with cos and zero and then we get paid row and one on new and two right so we're getting each name we're getting the key and the value our test is if the value is equal to on new okay so that means here then the key is going to be equal to two and then what do we add to the list well we add k we add k we add the key we add two so at the end result here we get a list with just a two in it because that's the only one that passes this test and then when we print out the zero with item in the list of course it's a two it's a one item list and we can also delete an item from a list here we have a list of letters actually a string we start by iterating through the string abcdef converting it to a list of letters just by adding each letter in the string to a list so now we have a list of abcdef as individual letters we shuffle those using this random function so now we have shuffled letters abcdef and each one of them is basically a string object in the letters list so we're going to create a new list that passes this test a for a in letters if a is not c right in other words every letter in this list except for uppercase c so we're going to get abdef and you see when we print them out we get defab but we do not get c so we get yeah that worked pretty cool huh we basically filtered out the c wherever it is in the list we don't know but we filtered it out that wraps up this lecture on list comprehensions you can download this code from my github site and use test to code and run these examples and I encourage you to use list comprehensions these are really a useful tool in python for creating lists in this section we're going to learn how to use stacks queues and heaps first we'll cover the fundamentals of each of these data structures what key operations each of them has and then how you can implement them in python these are three very useful data structures let's start by learning about stacks a stack is a last in first out data structure that's called blifo so what that means is that all the push and pop operations are to the top of the stack they only affect the top item on the stack the only way to access the bottom items on the stack like in this diagram item one is to first remove all of the items above it we have a couple of different key operations here push allows us to push an item on to the top of the stack and we use the pop command to pop an item off of the top of the stack some other stack operations are peak sometimes you might want to get an item off of the top of the stack without actually removing it let's say we need access to the top item we want to know what it is and we can use the peak command to see a copy of the top item without actually removing it from the stack or clear to remove all the items from the stack and empty the stack out there are a lot of different use cases for stacks one very common use case is the command stack all computer programs track each command that you execute and most programs you use have the option of undoing the previous command in order to do that the program has to keep track of which commands you've executed in which order so it does that using a stack each time you execute a command it pushes that command on to the stack so that it has a record of it and if you click the undo button it's going to pop the last command off of the stack and it's going to reverse that command so the command stack is used to execute the undo function in programming now let's take a look at how stacks can be implemented in code we have the python list which makes a great foundational data structure to store the stack in and actually python gives us most of the functionality that we need to create a stack with the list so the underlying data structure beyond our stack is going to be a python list python gives us the append function which we can use to push an item onto the stack and it gives us the pop function which we can use to remove an item from the stack we're actually pushing items onto a list and popping them from a list so here's one implementation using the python list we can create a new stack my stack equals an empty list and then we can push items onto the stack using a pen here we push 4, 7, 12 and 19 onto the stack we can see 4 items now a little more test code here when we pop an item off the stack we can see that we get the 19 first when we pop the second item off we get the 12 so it's popping off the last item first which is exactly what we want so that's typical stack operation however it's using a python list now if we wanted to write a wrapper class so that we can add some additional functionality to our stack that's actually not that hard so let's take a look at how that can be done here's a stack using a list as the underline data structure but using a wrapper class so that we can rename our functions as we like and we can also add additional functions and features to our stack so we'll start with a constructor an init function and this basically just has a new list it creates an empty list just as we did before the push is going to add an item so we receive an item and we just use the append function to add that item onto the list what the user is going to see is he's pushing an item onto the stack but what we're doing behind the scenes is appending that item to a list next the pop function first we want to check if the list actually has items on the list if the list is empty we don't want to try a pop operation if there's at least one item on the list then we'll pop that item off and return it the peak function allows us to just look at the top item on the list and return that item but without taking it off so here we return the top item on the list but without removing it and lastly if someone wants to print out the stack or show all the items that are on the stack what we're going to do is just show the string representation of the list now let's look at some test code and see how our stack works my stack equals stack and then we can push an item, we'll push a 1, we'll push a 3 and then when we print out the stack we can see that yeah we have a 1 and a 3 on our stack and when we pop an item off of the stack we get the 3 which was the last item that we put on the stack when we peak we get the 1 which is the only item left on the stack but peaking is going to give us the top item on the stack if we pop another item we get 1 and now the stack is empty so if we try to pop another item we get none so basically all those key features of our stack are all working just fine so this is how we can use a wrapper class to implement a stack in python with an underlying data structure like the python list so the python list is a very versatile data structure and here we've used it to create a stack now let's take a look at queues a queue is a FIFO or first in, first out data structure this is really intuitive because we see queues in every walk of life almost in everything you do on a daily basis you encounter queues queues have two key functions you enqueue an item by adding it to the end of the line you dequeue an item means removing it from the front of the line so some use cases for queues just about everything you wait in line for so bank tellers placing an order at McDonald's or your favorite restaurant DMV, customer service supermarket checkout pretty much anything that has a line is what a queue is and it's important to be able to model that in a computer program so the queue data structure allows us to do that now let's take a look at how we can implement a queue in python it's actually pretty simple because python already provides us a built in library called the deque or DEQUE that's a double ended queue that allows you to add and remove items from both ends of the queue for our simple queue we don't really need that functionality we just want to be able to add items to one end of the queue and pop them off of the other so we can use the append function to add items or push items onto our queue and we can use pop left to remove items or pop items off of the queue you can see the full documentation in python here if you want to learn more about how double ended queues work so for basically just using a double ended queue in python as a single ended queue we can use from collections import deque that's going to import our double ended queue library and then we'll create a new queue my queue and that's going to be a double ended queue object and then we can append items or push items using the append function so we can push a 5 and we can push a 10 onto the queue and then when we print out the queue we see that we have a double ended queue we have a 5 and a 10 on it and then if we want to pop items off of the queue we use pop left and here we get the 5 that pops an item off of the tail end of the queue or the left end of the queue so it's pretty easy to implement a queue in python this is obviously a common enough data structure that python built in a library for it now as a fun exercise for you you may try writing a class for the double ended queue to make a single ended queue using push and pop as we did similar for the stack in this lecture we're going to learn how to use max heaps now implementation wise the underlying data structure is going to be a list and it's the functions of a max heap are not a whole lot different from stack and queue so I think you're going to be able to pick this up fairly easily now when you look at this graphical representation of a max heap though it looks a lot like a tree and I know we haven't covered trees yet that's going to be covered in section 5 but bear with me I think you're going to figure this out pretty easily so the one condition of a max heap is that every node is less than or equal to its parent that's the key so you'll see that 25 is the parent of 16 and 24 25 has a left child and a right child is greater than or equal to both of those and then 16 is greater than or equal to both of its left and right children and so on so every node in the tree has to be less than or equal to its parent and every parent node has to be greater than or equal to the nodes below it so that is the core condition of a max heap and the reason it's like this is so that we can instantly remove this max number anytime we want anytime we want to pop the top number off of the heap we know that it's the highest number in that heap so the highest number always rises to the top of the heap and it can be instantly removed and used so max heaps are fast if you're familiar with big O notation you can insert or add an item to a max heap in big O of log in time which is extremely fast and you can get an item max item off of the top of the heap in big O of one time which is pretty much instantaneous you can remove the max or pop in big O of log in time so the response time for a max heap is extremely fast and that's why we use max heaps for some things if you need to pop the maximum number off a heap you can get very quickly max heaps are easy to implement in python using a list not as easy as the other two data structures we just covered but not too hard so I think you'll be able to follow this but I'll warn you the code is a little bit longer in Harrier than the previous two examples that we just covered but I've already written all the code all you have to do is follow along with the explanation so you can see that using a list we have an index that corresponds a list index that corresponds to each node starting with one at the top and then two three across four five six seven across on the next tier eight nine ten so it's a pretty easy indexing system that corresponds to these nodes on the tree and then when you look at our list how we put the items into the list well look 25 is an index one and then 16 and then 24 so look we know that 16 is not greater than 24 but it looks like wow it's lower than node three here or index three why is that? well because our rule is that 16 has to be greater than everything below it on the tree which it is so our condition is met this is a max heap 16 does not have to be greater than 24 we didn't say greater than everything behind it on the list no no no no on the tree so this is a valid max heap okay and that is how it is implemented in the list indices so we can instantly access any node in the tree or any node in the max heap now let's say we wanted to access the five we know that the index is four this is index number four for this this five node now we can also access five's parent which is the 16 we simply divide the index by two so this four divided by two gives us the index of five's parent node which is 16 and if we want to access five's children it's the same thing five's left child is times two to get the index of five's left child eight and then times two plus one gives us the index of five's right child eight and nine so if we take a given node at index four we can access his right and left child by times two and times two plus one and we can access four's parent by just divide by two so it's pretty quick easy operations to access the parent and children node in this tree now max heap operations like I said these are exactly the same operations that we just covered for stacks and cubes so we want to be able to insert or push an item onto the heap we want to be able to peek find out what is the max item on the heap without popping it off and then we want to be able to remove an item from the heap and return it which is a pop operation so the same three operations for heaps as we had for the previous two data structures let's look at how those work so push we can add a value to the end of the array and then we float it up to its proper position so let's look at an example we want to push a twelve onto this heap what we're going to do is put it at the very last spot in the array which is here right and we have a spot for it so in other words it's going to be eleven's right child it's the last index in the array and then we need to float it up to its proper position well how do we do that we need to compare twelve to eleven if twelve is greater then these two will swap places yeah it is greater so we want the twelve and eleven to swap places now we need to compare twelve to its new parent sixteen is twelve greater than sixteen no it's not so there's no more swapping twelve is already floated up to its proper position in the heap so this is one of the key behind the scenes functions that we have to code which is called float up or bubble up when we add an item to the bottom of the tree we need to be able to bubble it up to its correct position in the heap by comparing it to its parent nodes and then swapping so we use that every time we do a push operation peak just returns the value at the top of the heap okay which is going to be heap of number one index number one and that's pretty straightforward we don't really need to pop it off or anything we just get that item and return it and then pop we're going to move this top most item we want to pop off the max which we know is in index position one first we're going to swap it with the item in the last position then we're going to delete it from the heap and then we're going to bubble down the item here to its proper position so let's take a look at the example so eleven is in the last position twenty five is the item we want to pop so we're going to swap those two twenty five and eleven swap places now we can remove twenty five from the heap without affecting the rest of the heap it's in the last place it doesn't affect anything else in the heap our next step is to bubble that eleven down to its correct position so we compare eleven to twenty four eleven is less than twenty four so it needs to move down next we're going to compare eleven to nineteen and eleven is less than nineteen so that's going to swap places so we do some comparisons with the child nodes to move eleven down until there's either no further room to go down or it's not greater than any of its children nodes so that's the operation for pop and now we can just return the twenty five that we pulled off that's it so those are the three key operations and that's basically how they work in a nutshell now let's take a look at the code again the underlying data structure we're using is a list so you're going to see us using list indices throughout this program now the public functions we have here are push, peak and pop we're pretty familiar with those by this point but we also have supporting private functions that we need for this heap and we have basically a swap, a float up and a bubble down and we use those as internal utility functions these are not part of the user interface and then the string function is so that we can print the heap so our constructor when we create a new heap we have the option of passing in a list of items that we want to add to the heap if we don't pass in a list of items then we'll get back an empty heap with just a zero in it so we put a zero in the very first element because we don't use that we start our elements at index number one for the max heap if we pass in this list of items we're basically going to iterate through those add them one at a time to the end of the list and then float it up to its proper position that's the push operation so essentially we're pushing them all one at a time and when you look at the push method it does exactly the same thing it appends the data that you passed in to the end of the heap and then it floats it up to its proper position in the heap so that is the push operation which is almost identical to the constructor if you pass in items now the peak operation doesn't do much it only returns the top item on the heap that's all it doesn't take it off there's no pop operation going on it's just a peak now let's look at the pop operation there are a few different cases here depending on how many items we have in the list if the list has exactly two items one of those is the zero that we're not counting we're not using that as part of our max heap so if there are exactly two items that means there's really just one item in our max heap we'll pop off that number one item with index number one and we'll return it we pass that into variable max and then here at the end here we return max if there are more than two items then we're going to swap the maximum item which is in position index one with the last item so we get the last item in the list we swap those two we pop off the last item and assign it to this variable max and then we bubble down the first item that we moved into the top position so it's exactly what we just walked through in the slides and then at the end we return the max so that's how the pop operation works and in our utility functions here swap really just swaps two different items we pass in two indices we swap those two items in the list now the float up function is going to receive an index of the item that we want to float up probably the very bottom item in the list initially first we'll get the index of its parent if it's already in the top position then there's no floating up to do it's already risen to the top otherwise we're going to compare it to its parent and if it's greater than its parent then those two need to swap places so we'll swap the positions of the item index passed in with its parent then we'll call the float up function recursively on the parent so this will continue the float up function until the element reaches its proper position bubble down kind of does the opposite it takes an element that's at the top of the list and it bubbles it down to its proper position so we can pass in an index we get the left child and the right child by multiplying the index by two and times two plus one and then we'll set the largest equal to index we do a little comparison if the item we're bubbling down is less than its left child then we're going to swap positions with the left one if the item we're bubbling down is less than the right child then we're going to swap positions with the right child so if there's any swapping to be done at the very end here we check do we need to swap if so we'll call this swap function on the item we're bubbling the target item with the larger of those two and then we'll recursively call the bubble down function again on the same item that we just bubbled until it reaches its proper position and our test code here we don't have a whole lot of test code we create a new max heap with three items in it obviously the 95 is the highest one and the 21 is the second highest we push a 10 on and then we can see that our list now has ignore the zero, really has four items in it and when we pop one off we get of course the 95 and when we peak at the next item now that the 95 is gone we can see that the 21 is the next item in the max heap that is how a max heap works and that's how you can implement it in Python and by simply changing a few greater than or less than signs you could change this to a min heap let's say you have a collection of items that you want to store and you want to be able to iterate through them so you want to be able to find an item in the list you want to be able to insert an item but you need very fast insertion speed especially at the front of the list you want to be able to insert new items at the front you need to be able to remove items from the list you also may want to iterate forward and backward through the list or possibly even in a continuous circle through the list so one possible storage solution for these requirements is a linked list we're going to learn how to use linked lists and what they are and we're going to learn a few different types of linked lists a standard linked list a bidirectional or doubly linked list and also a circular linked list we'll learn how those work the major operations used with linked lists and we're also going to go through of course how to code a linked list in Python so let's take a look at how linked lists work every linked list is going to be composed of what we'll call nodes you can call it whatever you want in this video we're just going to call each item in a linked list a node you could store whatever data you want in a linked list it could be a student node it could be an employee node whatever it doesn't matter but we're going to call it a node and that's kind of the common nomenclature for the items in a linked list just call nodes and each node is connected to the next node so it has a pointer to the next nodes so those two things it has a piece of data which for us is just going to be an integer in this video and it has a pointer to the next node so those are the two key components of every node in the linked list now a linked list looks something like this each node has its own piece of data and it also has a pointer to the next node there can be any number of nodes it's basically unlimited only by the amount of memory you have in your system and the very last node here you'll see there's no next pointer so we're going to store like a none there to indicate that there's no next node that's the last node in the list at the very front we call that the root node that's the first node in the list so we need a pointer to point to the starting point for the list and this is what we call the root so we have a pointer to the root and the operations that we need for each linked list we need to be able to find data we need to be able to add a piece of data we need to be able to remove a piece of data from the linked list and we need to be able to print the list somehow so we're going to see how each of these operations works and then we're going to see how to code it in Python now the attributes for a linked list you have a pointer to that root node and then we're also going to track the size of the linked list so at any given time you could find out how many nodes are in the list let's look at the add operation so here's our linked list for the root we want to add 10 that's our command let's add 10 so we first are going to create a new node with that data 10 in it we don't have anything in the next pointer yet but what we're going to do is we're going to point the next pointer to where the root is currently pointing so the root currently points at this 5 node we're going to put our next pointer for this new node pointing at the root node and then we change our root to the 10 so we effectively inserted this new 10 node at the very beginning of the linked list that's how we're going to do the insert operation next we want to try and remove a number so let's try and remove 5 obviously if we try to remove a number like 200 and it's not in this linked list then we'll get back either a false or a none or sorry dude or an error or something but if we have a number that's in the list and we want to try and remove that so we're going to remove 5 first we need to find that 5 so we start at the root we check if this is the number no it's not oh is this the number yes it is so there's the node that we want to remove pretty easy to remove it we take the previous node to this 5 we change the previous nodes next pointer to 5's next pointer the root is 17 so we just change 5's previous node which in this case is the root 10 we change that next pointer to where 5's next pointer goes and so now effectively the 5 node is just completely cut out of the linked list when we iterate through the linked list starting from the root we're going to follow this path and we're never even going to know that the 5 is there the 5 node still exists but we don't have access to it anymore it's effectively deleted from the linked list we're going to start with the node class we're going to use the same node class for 3 different types of linked lists that we cover in this section so doubly linked lists and circular linked lists are going to use the same node class and you'll see in the node class here that we have 3 attributes we have a piece of data we have a next node and we have a previous node now in our standard linked list we do not need the previous node we're going to use the same node class we do not need the previous node so we're just not going to use that attribute in the standard linked list but it'll be there for us so that we can reuse the node class for the other 2 linked lists and then we also have this string representation that gives us back essentially the data in parentheses so that's what the string representation of a node is so in the linked list we have 4 methods we have an add, find remove, and a print list let's see how all those work first our constructor has 2 attributes we keep track of the root node and we also keep track of size so each time we add or remove a node we're going to increment or decrement the size accordingly to add a new node we pass in the data that we want to create that new node with we create a new node with passing in the data and the next node as the root node keep in mind we're inserting this node at the very beginning of the list so the current root node is going to be the second node so we pass that in as the next node for this new node and then we change the root node to the new node we increment our size by 1 and we're done with the add operation to find a piece of data we pass in that piece of data we are going to iterate through the list one node at a time we're going to start at the root node which we'll call this node and as long as this node is not none, as long as there it's a valid node we're going to continue to iterate through this list each time through this while loop this else statement is going to bump us forward to the next node if we haven't found what we're looking for yet so what we're looking for is this node's data is equal to d and when we find that we return d if we get through all the way through the while loop we haven't found it, we'll return none because that data is not in the list the remove function we pass in a piece of data we need pointers to this node and this node's previous node so we're going to start iterating through the list to do the find operation at the root which we'll call this node and then we're going to keep track of the previous node because we need that to be able to remove the node when we find the one we want to remove each time through this while loop we have two pointers now to increment we have to increment the previous node to this node and we have to increment this node to this node's next node so if we haven't found what we're looking for yet at the end of this while loop we'll bump forward both of those two pointers and that's how we iterate through the list now our check we check if this node's data is equal to d that we're passed in that we're looking for if we find it we found that data, there's two possibilities for removing that node one, that node is in the root that's this else here in which case we just change the pointer for the root node for our linked list to this node's next node or it's the second node in the list we bypass the current root and we point our root pointer to the second node in the list that effectively deletes the first node in the list now if it's not in the root in other words it's in some other node in the list then we need to delete that by changing the previous node's next node pointer to this node's next node so that is the remove operation if we get all the way through and we haven't found it, we return false if we do successfully remove it we return true and the print operation we print the list we're going to iterate through the list one node at a time starting from the root this while loop is going to check when we reach the end of the list and then we're going to exit and just print a none and for each node we're going to print the string representation of that node followed by a little arrow so you'll see what that looks like when we run the test code in a second so here's our test code we test a variety of different operations here's what a list printed looks like so our string representation of a node is just the value in parentheses and then we put an arrow between it in our print function so we'll create a new list called myList we'll add a few items to it and then we'll print the list and you can see what we get here and then we can print the size of the list we can see the size is equal to 3 we remove one item, the 8 and you can see the size is equal to 2 and you can also find the 5 when we find the 5 it actually returns a 5 when we can print that out and we can also print out the root which is 12 that's the last item we added so that's at the front of the list so that's how the linked list works so a circular linked list is almost identical to a standard linked list except that from the very end node instead of having a non pointer to the next node it's going to have a loop back to the very beginning to the root node the add operation in circular linked list works slightly differently because we have this loop back to the first node from the end node we'd rather not have to go back and update that every time we insert a new node so we don't insert the new node as the root node anymore now instead we're going to insert it as the second node we leave the root pointer and the last pointer the loop back to the root the same and instead we insert our new node as the root's next node and then the next node for our new node is going to be what was previously the second node so that's the add operation for circular linked list so that's basically it that's the only difference with a circular linked list so what are the advantages of a circular linked list well it's great for modeling continuously looping data or objects so something like a monopoly board or a game board or a racetrack or something that continuously loops and there are a lot of different looping objects in the real world so if you want to model some continuously looping set of objects in a computer program a circular linked list is one way to do that the circular linked list is very similar to the linked list with a few modifications in the add method we have to check whether the list is empty and if it is then we add the first node then we make its next node point to itself sounds crazy but we need to loop back to something so we loop back to the first node the root node else if there's already at least one node in the list we can create a new node and insert it into the number 2 position right after the root and change the roots next node to point to this new node so that's how the add operation works we can see the code here it's only a few lines of code the find method works exactly the same as in the regular linked list except that in this lf statement we have to do a check if we've circled all the way back to the root node again because if we have we have to stop our find and return false we didn't find it, we searched the entire list we didn't find the value we were looking for return false for the remove method for the remove method we need to track both this node and the previous node so we're going to set pointers for both of those to start out so we'll start out at the root node and we'll set previous node to none at the end of our while loop here we advance both of these pointers one node so they both move forward to the next node now we test if we found the data that's this first if statement bingo found if we passed this test and if so there are two possibilities for the remove function number one if the previous node is not none tests whether the data was found in the root node and if not the remove is an easy bypass operation for changing the previous node's next pointer which is what we do here and case number two else if we need to delete the root we use this while loop to find the very last node in the list so that we can update its next node to point to the new root because the root has changed so we find that we find the last node we update the last node's next pointer which is the new root and then we update the root pointer itself lastly we decrement the size by one and we return true if we successfully remove the data and in our print list method we iterate through the list we print each node followed by an arrow as you can see and our while loop has to check if we've made it back to the root so we know when to stop while this node dot next node is not the root we continue to iterate through the list now let's take a look at the circular link list test code here we create a new circular link list we'll just call it CLL we're going to add a bunch of items to it using a for loop adding one item at a time and then we can print the size of the list we can see down here the result the size is 5 we try to find an 8 if the value is actually in there it will return 8 if we try to find an item that's not in the list it's going to return false so we can see when we try to find the 12 we get back a false and instead of using our print list function here we're going to iterate through continuously so that you can see when we pass by we're going to print 8 items even though there are only 5 in the list so we're going to see if it actually does circle back to the next node we're just going to continuously get the next node up until we reach 8 of them so we can see 5, 9, 8, 3, 7 and then it starts from the beginning again 5, 9, 8, 3 it'll continue on we print up to 8 items so that shows you that it's a continuous loop and we can continue to loop through that if we want to alright a little more test code we can print the list and here's the current contents of the list and then we remove an 8 and when we print out remove 15 result we see that it's false because there's no 15 we can see that and we print the size of the list now we have 4 items because we removed one we try to remove the 5 node that completes successfully and then we print the list again and we only have 3 items left so that wraps up this lecture on the circular linked list now let's look at doubly linked lists these are also sometimes called bi-directional linked lists because they have arrows pointing both directions to the next node and to the previous node so a regular linked list looks like this a doubly linked list each node has 3 pieces of data it has a pointer to the previous node a pointer to the next node and the data itself is storing so it has those 3 things 3 components to the node of a doubly linked list so this is what a doubly linked list would look like a simple one with 3 nodes 4, 23, 7 so let's say we want to delete an item this is a little more complicated because we have 2 pointers to fix, not just 1 so we found this item that we want to delete we're going to call that this node and then the previous node to this node is the 4 and the next node is the 7 so how do we delete it? well we look at this pointer from the previous node that's pointing to this node and we look at the pointer from the next node that's pointing to its previous node these ones that are pointing to this node they have to be fixed they basically just have to bypass it so what we do is we change 4's next pointer instead of pointing to this it has to point to this node's next node and then for 7's previous pointer instead of pointing to 7's previous node this node's previous node so we basically do 2 adjustments here preve.next equals this.next and next.preve equals this.preve so those are the 2 changes that we do to cut this node out of the list and you see once we get these red arrows in place once we fix these 2 pointers we've effectively cut node 23 out of our linked list we've deleted it that's how the delete operation works in a doubly linked list some advantages of the doubly linked list over a standard linked list you can iterate through the list in either direction that's pretty obvious but when you have a really large linked list and you don't want to iterate through all of the items because you happen to know that your item is towards the end of the list you can actually save quite a lot of time by starting your iteration at the tail end of the list and working right back so you could save a pointer to the very end of the list as well and you can delete a node without iterating through the entire list that is if you have a pointer to that node right if you know where that node is that you want to delete and you don't have to iterate through the whole list to find it each node already stores its previous in next pointer so you can get the nodes on either side of this node that we want to delete without having to iterate through the entire list if you have a pointer to the node you want to delete doubly linked list uses an extra node attribute called preview node as I showed you before when we looked at the node class and it also has an extra list attribute called last you can see here last so that we can always access the tail end of the list or the last item in the list now the add method has to check if the list is empty and if so then the root node is also the last node so otherwise it adds a new node to the beginning and it changes the root's preview node to the new root node so we can see the if else two different conditions here for the add a new node the find method works exactly the same as the find method for the standard linked list there's really no changes on that the remove function is a little different there are three possible cases in the remove function so let's review each one of those case number one we're trying to delete a middle node that's this if statement here so the node is not in either the root or in the last node that's the standard case that we showed in the slides so for this we just do a simple bypass we bypass the target node and by changing the previous node's next pointer and the next node's previous pointer which is exactly what we're doing here and we've basically bypassed the target node that we're trying to delete now the second case is that we're trying to delete the last node this is just like case one except that the previous node's next pointer will be changed to none because the second to last node is now the last node so that's the only difference here so we're changing the basically the second to last node's next pointer to none and the third case is we're trying to delete the root node again similar to case one except that we change the root pointer to point to the second node in other words we change root to point to root's next node and that's it those are the three cases for remove the rest of the remove function is really straightforward the print list method is pretty much the same there's no changes there so let's look at the test code for the doubly linked list here we create a doubly linked list DLL we'll call it we add a bunch of items using a for loop to add each one of those items in we can say we print the size is 5 and we can print the entire list if we want using our print list method and then we can remove an 8 we'll print out the size again we can see the size is 4 so these things all work and then if we try to remove items that are not in the list DLL.Remove15 that doesn't work if we try to find an item that's not in the list it falls back and if we add some numbers 21 and 22 and then we remove a 5 and then we print the list again we can see that 21 and 22 were added to the front of the list and the 5 was removed that was a tail node the last node in the list and we successfully removed that and then just for fun we see that we can print out the last node's previous node which should be the node right before the 9 which is this 3 so we can access nodes from the tail end of the list also that wraps up this section on linked lists so in this section we covered a standard linked list a circular linked list and a doubly linked list or a bi-directional linked list we showed you how those work and then we implemented them in code and again I encourage you to download this code and run it and try it out make some edits to it use your own tests on it to see how it works by using the code you're going to better understand how the code works how linked lists work and how to eventually write your own linked list code hopefully in this lecture we're going to talk about trees after section 1 where we covered pythons built in data structures trees is definitely the next most important section of this course trees are a critically important data structure in all programming languages I'm thinking of a number between 1 and 8 million can you guess my number well you guessed 4 million I just say wrong and you're thinking uh oh aren't you going to tell me higher or lower? No you have to guess until you get my number well you might have to guess every number between 1 and 8 million to figure out which number it is so if you're using a list an unsorted list as your data structure that's what it's like you would have to iterate through the entire list to find the number so you may have to do up to 8 million comparisons to locate that number that I'm thinking of that is the issue with lists when you have a lot of data it's really slow to find data now with a tree it's a little different you guessed 4 million and I say lower you guessed 2 million I say higher you guessed 3 million lower okay wow with 3 guesses and now you've narrowed down the possibilities to between 2 and 3 million there are only 1 million possibilities left with just 3 guesses so with as few as 30 guesses you would be able to find any piece of data in a tree with up to 10 million nodes so that is how fast binary search trees are a balanced binary search tree will let you locate data in a very large tree with as little as 30 comparisons so now let's take a look at some of the major operations of trees and how they work so first let's learn some basic terminology about trees this is a node each part of a tree is called a node and each connection between nodes is called an edge and at the very top of the tree we have a root node you can see that trees are actually upside down compared to real world trees this is more like a root system of a tree or a flipped upside down tree like a management hierarchy in a company or something where you only have one president and then you have multiple vice presidents and so on down trees are great for modeling organizations but that's not the real benefit of trees the real benefit is the speed now we have parent nodes and child nodes in a binary tree a parent can have up to 2 children 1 or 2 children nodes that have the same parent are called sibling nodes at the very bottom of the tree that don't have any children are called leaf nodes not all trees are binary trees but in a binary tree each node can have up to 2 child nodes a left and right child node now some trees may have 5, 10, up to a thousand child nodes for each node we can see that off of 5 here we have a subtree a subtree connects to a root node here and a subtree is basically any part of a tree that in itself is a tree so 8 can be a subtree of 5 compared to node 4 3 and 5 are its ancestors which is its parent node and every node above is the parent in the tree descendants are every node below that node in a tree so node 5's descendant includes everything in its left subtree and everything in its right subtree in a binary search tree each node is greater than every node in its left subtree so here we can see that 15 is greater than every single node in its left subtree and 8 is greater than every node in its left subtree and the 5 is greater than every node in its subtree the 24 is greater than every node in its left subtree So this is a standard requirement for binary search trees. Each node is greater than every node in its left subtree, and it's also less than each node in its right subtree. Here we can see that all the nodes in 15's right subtree are greater than 15, and all the nodes in 24's right subtree are greater than 24. All the nodes in 8's right subtree are greater than 8. So those are two standard requirements for a binary search tree. Some of the standard operations that all binary search trees are going to use, insert, you want to be able to add new data to the tree, fine, you want to be able to locate data in the tree, delete is to remove a node, get size counts all the nodes in a tree to tell us how many pieces of data we have in a tree, and traversals which enable us to walk through the tree node by node, and I'll show you how some of those work. First let's look at the insert method. We're going to always start at the root when we're doing an insert. So the top, in this tree it's 15. We're always going to insert a new node as a leaf, in other words at the very bottom of the tree, but we start at the top to locate the right position, the correct position in the tree to insert that new leaf. So let's say we want to insert 12 in this tree. We're going to start out with comparisons starting at the root. Is 12 less than 15? Yes it is. So we're going to descend down 15's left subtree. Next we're going to compare 12 to 8. Is 12 less than 8? No it's not. So we're going to descend down 8's right subtree towards the 11. Is 12 less than 11? No it's not. And then we compare 12 to 13. And we say well yeah 12 is less than 13. So again we're going to add the 12 as a leaf node. So we can add it as 13's left child. That's how the insert function works. Now let's look at the find method. Again with find we'll always start at the root. Here it's 15. And we're going to do comparisons. So if we want to find 19 in this tree, we're going to start by comparing 19 to 15. Is 19 less than 15? No it's greater so we descend down the right subtree.  You can see how with each comparison and decision we descend down one subtree that cuts in half the number of remaining possibilities to locate an item. So now we've already in two comparisons found the 19 in this tree. So when we find a piece of data with the find function we're always going to return that piece of data. And if we didn't find it, we want to return false to let the user know that data is not existing in the tree. So with delete there are three different possibilities. One is that the node we want to delete is a leaf node. Another possibility is that it has one child node and there's another possibility that has two child nodes. So each one of these we have to handle differently. Delete is a fairly complicated operation. So let's first look at the possibility that the number we want to delete is a leaf node. So look at these gray nodes are all leaf nodes. So in the case of a leaf node it's easy for us to just delete the leaf node without affecting anything else in the tree. These are bottom most nodes. So it doesn't affect the organization of anything else in the tree. We can just delete that node if it's in a leaf. That's the easiest case right there. Now if we have one child these are cases where we have one child node 11, 13 and 28. If we want to delete one of those nodes we have to promote that child node to the target's node's position. So for instance if we want to delete the 28 we would promote 25 to 28 position. If we want to delete the 11 we would have to promote the 13 to 11's position. And the 12 comes with it. You're promoting that entire subtree. So that's the one child delete. And then if you have two children well that gets a little trickier. Let's say we want to delete the 24 and we can see that 24 has two children. We find the next higher node in order to do that we're going to descend down 24's right subtree and then all the way to the left. So here we get to 25. Now if it was a much larger tree it'd be the same operation. You take the right subtree and then the left most node in the right subtree. And here it's 25. So the operation is to basically swap places the 24 and the 25. And then we can delete the 24. So we put the 25 where the 24 was and we can delete the 24. And the same thing if you want to delete the four. Here we want to delete the four. We can see that four has two children. We want to find the next higher node after four and we find that six. We descend down the right subtree and the left most node in the six. So we delete the four and we promote the six to the fourth position. And the seven comes with it because seven is part of six's subtree. So that's the delete operation in a nutshell. It's actually a little more complicated than that. So we're not going to code it in this video. However, I do have another video on YouTube where we code the entire delete function. Now get size. Sometimes we may want to find out how many nodes are in a tree. So this is a pretty easy operation. The get size function returns the number of nodes and it works by using recursion. Actually all of these functions in trees, most of them are recursive. So find, delete, insert. We're doing it recursively because we continue to call the same function using the same parameter until we find the correct position to execute it. So the get size works the same way. The size is equal to one plus the size of the left subtree plus the size of the right subtree. In other words, the size of this tree is equal to, well, five node is one plus the left subtree is the subtree starting with three and then the right subtree is the subtree starting with eight. So we say the size of this tree is one plus the size of these two subtrees. And then we'd call the same thing, the same get size function on threes left and right subtrees. The size of the three subtree is one plus the size of threes left plus the size of threes right subtrees. So we call recursively the get size function as we descend down the tree and eventually we get down to leaf nodes and we return a one. Oh yeah, this is a leaf node, size equals one. And so all those ones get added back up as we retreat back from the call stack. And then let's talk about traversals. Sometimes we need to traverse the data in a tree and there are multiple ways of doing that. A few different traversal algorithms are called pre-order traversal, level traversal, in-order traversal, and post-order traversal. So in this video, we're gonna cover pre-order and in-order traversal. The pre-order traversal, we visit the root before we visit the root subtrees. So in other words, we're always gonna start at the root and then we'll visit the root sub-tree. And the same thing here. With node three, this is basically a sub-tree starting at node three, we're gonna visit three before we visited sub-trees. So we start at the top and then we descend down the left sub-tree. And again, we descend down the left sub-tree and then we hit the right sub-tree. And then we'll come back up and descend down five's right sub-tree. We'll get the left sub-tree and then the right sub-tree. So one, two, three, four, five, six, seven. You can see the order. This is pre-order traversal. And in-order traversal visits the root between visiting root sub-trees. So that means that it can deliver values in sorted order. So here we may want all of these values in sorted order, in which case we're gonna start with the bottom leftmost node and work our way up. So one, three, four. We visit the one's parent node and then it's right sub-tree. And then we visit three's parent node and then it's right sub-tree, starting with the leftmost node. So we're always, in other words, working our way up from the bottom and working our way to the right. So that's an in-order traversal. What are the advantages of binary search trees? Number one, trees use recursion to implement most of their operations, which makes them pretty easy to implement. Most of the code is pretty easy to write. Not very complicated. Well, except for the delete, which has a lot of different cases. But the big huge advantage of binary search trees is speed. They're really, really fast at locating data. You can insert, delete, and find data in a tree in big O of H or the height of the tree, depending on how many levels there are in a tree. Or put another way, log in of the data. The log of 10 million is about 30. In other words, if you don't know a big O means, this is an order of operations, an approximation of how many operations it takes to achieve something. So you can do all of these different operations in about the log N of the amount of data you have, which is very fast. So on a balanced binary search tree with 10 million nodes, you can do all of the insert, delete, and find functions in as little as 30 comparisons. That's incredible, that's incredible. So trees are extremely fast. Now let's take a look at how to implement trees. Now let's look at how to implement a tree in Python. So we have this Jupyter notebook running here. We're gonna implement a binary search tree. We have a few functions. We have a constructor, an insert, find, and get-size method. And then we have two different ways to traverse the tree, pre-order and in-order traversal. And I'll walk you through how all of that code works. So in our constructor, we have three attributes. We have a piece of data that we're gonna pass in as a required attribute. And then optionally, you can pass in a left subtree and a right subtree. In this program, we're really not using the left and right subtrees in the constructor. So I have them defaulting to none. But each node is its own subtree and it has a left and right subtree. So the insert function, the key is defined of correct location to insert it. And we're always gonna insert it at the very bottom of the tree as a new node. The first possibility is the node that we're in is equal to the data that we're trying to add. In which case, we're gonna reject that. We don't want any duplicates in our tree. So in this case, we're gonna return false because we got a duplicate. If the data passed in is less than the data in the current node, then we're gonna descend down the left subtree. So we return self.leftsubtree.insert. So in other words, we're calling recursively the insert function descending down the left subtree and still passing the data along. And the reason we have this return is because we wanna return either true or false at the end of the day if the data was successfully inserted or not. Or if we reach the bottom level of the tree and we found the right position to insert it, we'll create a new subtree with that piece of data and we'll set it up as the left subtree of its parent node. Then we return true because we added that data. Or we can descend down the right subtree. If we had to send down the right subtree, again, we're just doing a recursive function call to the insert function. We're passing the data in down the right subtree. And when we reach the bottom of the right subtree, we'll insert a new tree and we'll attach that as the right child of the parent node and then return true. The find function works very similar except that we don't actually create a new item and insert it. But we recursively descend down that either the left subtree or the right subtree. So if we find that piece of data in the current node that we're in, we'll return that piece of data. If not, we'll do a comparison to the current node to see if we should descend down the left subtree or the right subtree. Now if we reach the bottom of the tree where we have a none node, we're gonna return false. Otherwise, we call the find function recursively on the left subtree. And we pass the data along as the same parameter. And then the same on the right subtree. When we reach the bottom of the right subtree and we didn't find the data yet, in other words, we reach a none node, then we're gonna return false because the data's not in the tree. Or if we didn't reach a none node yet, then we'll continue to send down the right subtree to find that data. So that's the find function. We basically are just doing comparison and then descending down either the right or left subtree until we reach the bottom. Get size, like I showed you in the diagrams before. If the node that we're in is not none, then we're gonna return one plus the size of the left subtree plus the size of the right subtree. So this works recursively. We continue to call get size as we descend down the tree, the left subtree and the right subtree. We add all those together and we add each one for each node we visit. The pre-order traversal checks first if the node that we're in is not none and if it's not, then we'll print that data and then we'll continue down the left subtree calling the pre-order traversal function recursively until we reach a none node and we'll continue down the right subtree recursively until we reach a none node. The in-order traversal is not a whole lot different and you can see what's different between these two is that we're here in the in-order traversal, we have the print statement between those two subtrees. And here the print statement in pre-order is before we descend down the two subtrees. So that's really the key difference between pre-order and in-order traversals. Now let's take a look at the test code. So in this test code, we're gonna create a new tree. We do that using the value of seven we pass in as a parameter. So now we have a tree with a seven as the root. We insert a nine into it so we have two items in the tree and then we just go through this whole list and add a whole bunch of different items. Oh look, there's a duplicate. So it's not gonna add this nine. And we insert each one of those items. Now we're gonna print out the tree. For i in range 16, so this will count from zero through 15, we're gonna do a find operation on each one of those numbers. And we can see what it prints out is for zero it prints false, for five it prints false, and for eight it prints false. But all the rest of them, it returns the number and it prints it out. So you can see that there's no zero, there's no five, and there's no eight in this list. So it's unable to find those it returns a false and that's what we print. The get size function returns a 13. So that's the count of the nodes in the tree is 13. And then we do a pre-order traversal, we print a blank line and we do an in-order traversal. And you can see the in-order traversal is exactly the numbers in sorted order. And again, you can see that the zero, the five, and the eight are missing in this in-order traversal. So that concludes this lecture on trees. I recommend you to download the code, try it out. Maybe try and code the post-order traversal and try and write some different tests to actually use the tree. Maybe try inserting letters in it or something or a million numbers and see how fast the tree performs. So another really important data structure is graphs. Graphs are perfect for modeling real world objects in a lot of cases. A graph consists of vertices and edges connecting those vertices. So for instance, in a social network, the vertices could be people, right? And the connections are friendships or relationships between people. So there are two different types of graphs we can use, undirected graphs where the relationship is both ways. It's bi-directional. So in this case, in a social network, typically relationships work both ways because both people know each other.  So an undirected graph is a very common way to model relationships in a social network or connections between a real network where the nodes would be computers and the connections would be cables connecting those computers. Now another type of graph is a directed graph. So if you wanted to model, for instance, airplane flights between cities, well in some cases there may be a one-way flight or even a round-trip flight, but each leg of that flight is gonna be represented by an arrow, in other words, which direction is going. So if you have a flight going from Chicago to Seattle, well it doesn't necessarily have a return flight. So the best way to model this is using a directed graph which shows a one-way relationship. So transportation and networking are just a couple of the very many different possible applications for graphs in modeling real world problems. So in this section we're gonna cover the two most common ways to implement graphs, which is using an adjacency list and an adjacency matrix. An adjacency list stores a list of neighbors for each vertex in the vertex itself. An adjacency matrix stores a 2D array of all the connections between the vertices in the graph object. So let's take a look at this undirected graph with five vertices and a number of edges connecting them. So in an adjacency list implementation, each vertex would store its own list of vertices that it's connected to. So in this case, A is connected to B, C and E and that list of neighboring vertices would be stored in node A's neighbor lists. And node B is connected to vertices A and C and that would be stored in node B's list of neighbors. Now using the same undirected graph, we could implement this using an adjacency matrix. An adjacency matrix has all the from vertices and to vertices and it puts a zero where there's no edge and a one where there's an edge. So here we can see from A to B there is an edge. Now since this is an undirected graph, this is a mirror image across the diagonal as you might expect. So if edge A connects to B, then B also connects to A. And this 2D matrix would be stored in the graph object itself. So each vertex doesn't have to remember its own neighbors. Now if you have weighted edges, it's much easier to implement this with an adjacency matrix. Why? Because instead of putting just a one, you can put the weight in the matrix. In an adjacency list, you would have to probably put a tuple, which includes the weight. In a directed graph, it's fairly easy to implement in either one of these. So in adjacency list, you would just put the outbound edges from each vertex. So from A, you can see that A has an outbound edge to C. So we would list in A's adjacency list, only node C. And the directed graph in an adjacency matrix is similar. It's not going to be symmetrical across the diagonal anymore because B connects to A, but A does not connect directly to B. So we can see that we have B to A as a one, but A to B is a zero. Which implementation is better? Well, first let's look at two different types of graphs. In a dense graph, every vertex may be connected to every other vertex in the graph. In that kind of graph, there are a lot of edges relative to the number of vertices. So maybe each vertex is connected to every other vertex in the graph, in which case the number of edges would be equal to the number of vertices squared. In a sparse graph, there are relatively few edges, or maybe approximately similar to the number of vertices. So an adjacency matrix takes up v-squared space regardless of how dense the graph is. So because we have a list of vertices along the x-axis and along the y-axis of this matrix, it takes up v-squared space for the matrix. So a matrix for a graph with about 10,000 vertices would take up at least 100 megabytes. So some trade-offs, an adjacency list is faster and uses less space for a really sparse graph. But the con is that it's slower for dense graphs because for dense graphs, it would have to iterate through all of the neighbors. And an adjacency matrix is faster for dense graphs because it can easily look things up using an index. It's also simpler for weighted edges. It's very easy to implement weighted edges. But the con is that it uses a lot of space. So as the number of vertices grows, the amount of space required for the adjacency matrix grows by a factor of v-squared. Next, we're going to learn how to implement each of these methods in Python. Now let's look at the code. I'll post the code on my GitHub site, both in a Jupyter notebook format, which we're going to walk through in this video, as well as the regular Python file. So you can download that and test it in whichever format you prefer and try it out. And I strongly recommend you do that. So you can really understand how graphs work. Now in the code, we basically have two classes. We have a vertex class, which is pretty simple, and there's not a whole lot to it. And then we have the graph class, which is a little bit more substance to it, but we'll walk through exactly how everything works. And then we have a little bit of test code and I'll show you how that works. So first let's look at the vertex class. We have two different methods here. We have a constructor, this init is just a constructor that creates a new vertex. And basically it just sets two different attributes for a vertex object. You can pass in the name in, which is the name of the vertex, which for us is just a single letter. And then it assigns it to this name attribute for the vertex. And then it also creates an empty set for the neighbors for that vertex. And then add neighbors. You can pass in the name of a vertex, not the vertex object, but the name of a vertex. It adds that to the neighbor's set for that vertex. And remember sets don't store duplicates. If that neighbor has already been added to the vertex, it doesn't matter, it won't be added a second time. Now let's look at the graph class, which is a little more complex. So we have a few different methods here. First, each graph object stores a vertices dictionary of all of the vertices. And that is in the format of name, vertex name, and then vertex object. So you can always access from vertex name, you can always access the vertex object through the dictionary. And when we add a vertex, we pass in that vertex. We check if that the object passed in is actually a vertex object. And if the name is in the vertex list yet, and if it's already in that dictionary, then we're obviously not gonna add it a second time. And then if it's not, then we'll add that vertex to the vertex dictionary and return true. Otherwise we return false, which says the add was unsuccessful. And to add an edge, each time we create a new edge, we check if both of the vertices are actually existing real vertices in the graph because if the vertex does not exist in that graph, we can't add an edge connecting that vertex. And assuming that both vertices are valid vertices, then we add U to V's neighbor list and V to U's neighbor list. That's why we have two different add operations here. And we return true. And then to print the graph, we're really just gonna print out the neighbor list for each vertex. So the name of the vertex and then that vertex is neighbor list. And our test code, well we'll create a new graph G here. And then there are a few different ways to add vertices to that graph. Here's three different ways. So we can create a new vertex A by passing into the vertex constructor the name of vertex A. And then we can add vertex A using the add vertex method. Another way is we can do this all, both of those steps in one operation using just add vertex and then create a new vertex called vertex B. And then we could also use a for loop if we wanna add a whole series of vertices. Here A through K, we have to use ORG to get the numerical equivalent of A so that we can iterate through those using the range function. And then we convert that back into a letter using CHR, the character equivalent of that number. So there's three different ways there that we can add vertices to our graph when we did all three. And we don't have to worry about duplicates because we check for duplicates in our add vertex function. And then in adding edges, well, I just created a list of edges here. Basically a list consists of two letters, which is the two vertices that that edge connects to. And then we iterate through those and we add edge with the first letter and the second letter passing those two in. And lastly, we print the graph.  What does that graph look like? Well, our print function doesn't really visualize the graph, but it does show us what the adjacency lists look like. Look, the adjacency list for A looks like this. It has neighbors B and E. And B has neighbors A and F. C has just a neighbor G. So if you take a look at the edges list, you can figure that out too, right? A has an edge to B and E, right? And so you see B and E in A's neighbor list. But you're also gonna see A in E's neighbor list. So if we look down to E, yep, sure enough, there's A. So that's how an adjacency lists implementation works in code. And again, you can download the code and run this and try it out, spend a little more time getting acquainted with it. How about that? In this implementation of graphs, we use strings, lists, sets and dictionaries, four data structures that we covered in section one of this course. In the next lecture, we're gonna learn how to implement graphs using adjacency matrix. In this lecture, we're gonna look at how graphs can be implemented using the adjacency matrix method. So we walk through how the adjacency matrix works. Let's look at the code. Again, this code is posted on my GitHub site, both in a Python file and in a Jupyter Notebook file. You're welcome to download either one. This code, I'm gonna show here, is the Jupyter Notebook. It has a lot of comments in it. So first we have a vertex class. The vertex class is extremely simple in the matrix implementation because the matrix is basically in the graph class. So the vertex class really doesn't have to do anything. We have a constructor where we simply pass in the name of the vertex. So we assign that passed in name to a single attribute called name, and that's it. That's all a vertex has is a name. The graph class is a little more complicated. We have three key attributes here. We have a dictionary of vertices and then we have a matrix of edges. This is actually gonna be a two-dimensional list of edges. And then we have edge indices because the edges are unlabeled. All it is is a series of zeros and ones. So this is basically the labels that goes with the edges. So this is not the only way to implement this. This is how I chose to do it. You can implement a different way if you want if you find this too complicated. So we're gonna have a few different functions here or methods that are part of the graph class. We're gonna add vertex, which is gonna update all three of these attributes. We're gonna add an edge, which only really needs to update the edges matrix. And then we have a print function. When we add a vertex, we pass in that vertex. First, we're gonna verify that that's a valid vertex object and that it's not already in the vertices list. And if it's not, then we'll go ahead and add it. So the first step is to update the vertices dictionary by adding the name and vertex to that dictionary. So in other words, we'll have a and then the vertex object a. Next, we're going to use a loop to append a column to the rightmost side of that matrix. So the edges matrix. So we use a for loop to do that. We append a column of zeros to the rightmost end of the edges matrix. And then we need to put on the very bottom of the matrix, we need to append a row of zeros on the very bottom. So we do that using the self.edges.append zero times length of edges plus one. In other words, however many edges there are, we're going to add a zero at the very bottom row for each one of those. Now, since we just added a vertex, there's obviously no edges connecting to it yet. So we know these are all zeros, but as we add edges connecting to that vertex, then we're gonna flip them to a one. And the last step in adding a vertex is we update the edge vertices. We add that vertex name and the index that we can find that vertex at in the edges list. And when we add an edge, we pass in U and V, which is the from and to vertices for the edge. We have a default weight of one. That's normal to have a default weight of one, but you can override that by passing in whatever weight you want. So if you want to use weighted edges, it's pretty easy to do. We check to make sure that both vertices that we passed in are valid vertex names. In other words, they're actually in the vertices dictionary. So edges is a two-dimensional list. So we need two different indices to access a specific position in the matrix.  So we want to flip both of those for edges. Those two positions in this table set it equal to weight, whatever weight you passed in or one if this is a default. And lastly, when we print a graph, well, you'll see how it prints out down below. But we're starting out by printing the name of the vertex, and then we're going to print out the row of ones and zeros for that vertex. So our test code, we used exactly the same test code that we used for the other implementation of the graph using adjacency lists. The test code is identical. So we create a new graph. We have three different ways of adding a vertex to the graph. For A, we create a new vertex object, and then we do add vertex A. And then for B, we add vertex, and we'll create a new vertex inside the parentheses here. And then we use the for loop method where we iterate through the A through K, and we add them to the graph. And then to add an edge, I created a list of edges here. This is the same list we used in the other test code. For edge in edges, in other words, we're iterating through those edges. We add each edge one at a time. We pass into the add edge method two parameters. In this case, it would be the A and the B. So we're getting the left character and the right character. We're passing those in as the attributes. And then when we print out the graph, we see what this looks like. This is what our neighbor's matrix looks like at the end of this. So you can see where we have a one is a neighbor for A. So this would be B, C, D, E. So B and E are A's neighbors, right? Wherever there's a one. And then this is column A. So we can see that A has an edge to B and E. So in other words, you can see that it's symmetrical across this diagonal. And every place where there's a one reflects an edge between two vertices. So I recommend you download the code, test it out, try to use it and get familiar with it. These are two different implementations for graphs, and it's important to understand both because there is no one best method, as I already explained in different instances. Each method has its advantages. In this course, we've covered a lot of different data structures. We've learned how to use different data structures as well as how to implement and some of the pros and cons of each. Now you'll have a lot of new tools in your tool belt as a programmer to solve problems as you're developing code. Thank you for taking this course.