0% found this document useful (0 votes)
19 views55 pages

An Introduction To Data Structures

Uploaded by

Alcides Netto
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
19 views55 pages

An Introduction To Data Structures

Uploaded by

Alcides Netto
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 55

Hello everyone and welcome to An Introduction to Data Structures.

My name is Steven, and


over the next 3 hours, we’ll be tackling the topic of data structures in relation to computer
Science. More specifically, we’ll be talking about what they are, going over some different types
of data structures, and discussing how we can use each of them effectively with examples.
This course will be a general overview of data structures and so we won’t be confining ourselves
to one specific language; however, this series will require you to have a basic understanding of
programming. If you are completely new to Computer Science, I would recommend our
Introduction to Programming series which will be linked in the description below or through the
card on your screen now in the top right corner. That video will give you all the information you
need to traverse these topics comfortably.

Now we’re almost ready to hop into things; however, there are a few housekeeping items I want
to go over beforehand for those wishing to enhance their learning experience. If you’re not
interested in that and want to skip right to the content, go to the time shown on the screen now.

For those staying though, there are just a few things I would like to mention.

Firstly, in the description you will find Time Stamps for each major topic covered in this video,
as well as TimeStamps taking you to every smaller section contained within those topics. So
please feel free to skip around if you are already comfortable with one topic or only interested in
a certain data structure.

Next, we’ve decided to include the script and visuals used for this series also in description
below. That way you can follow along if you like, or simply read the script if my soothing voice
isn’t your style.

Additionally, for each segment, I’ll be including the references and research materials used to
write the script for each topic. That way, if you ever feel as if I haven’t explained a topic well
enough, or would just simply like more information about a certain topic, you’ll have a readily
made list of articles and websites which you can use to supplement this series.

If you feel like you have any questions throughout the series about maybe something I said or a
certain visual, please ask questions in the comments below. I’ll try to be as responsive as
possible for the first few weeks on questions you guys may have about the series and such.

And finally in terms of housekeeping, I’d just like to say if you’re not already subscribed to our
channel, NullPointerException, then consider doing so if you enjoy this type of content, as me
and my co-host Sean regularly post videos in this style. We’re trying to hit 1,000 subscribers
before the Fall semester of college begins, so check us out if you end up enjoying the video.
With my shameless plug out of the way, we’re finally ready to tackle the topic of Data
Structures.
In this introduction segment, we’ll simply cover a general overview of what data structures are,
and then go over what type of information will be covered throughout the duration of this series.

So the obvious question to start with is what exactly is a data structure? Well in Computer
Science, a data structure is a way to store, organize, and manage information -or data- in a way
that allows you the programmer to easily access or modify the values within them. Essentially,
it’s a way for us to store a set of related information that we can then use for our programs. Data
Structures, and the algorithms used to interact, modify, and search through them provide the
backbone for many of the programs you’ll end up writing. I can almost guarantee that in 99% of
your programs, a data structure will be involved. Each of the data structures we’ll be talking
about are designed for the sole purpose of storing information and allowing the end user to
access and manipulate that information in an efficient and effective way, but each one differs in
the manner that they accomplish this.

If you have a basic understanding of programming, you probably know about a few different
data structures already such as the array and and the arrayList, also known as the List in Python,
but if you’re going to be pursuing a career in computer science, just knowing these two is
definitely not going to cut it.

While basic data structures such as the array are used frequently by companies throughout their
code, more advanced data structures are being put to use all around you. The undo/redo button in
google docs, google maps on your phone, even auto-complete through your text messages all
require the use of more advanced data structures, which makes them extremely useful to learn
and comprehend. Okay, I can’t see you per say, but I can just tell you’re jumping up and down in
your seat, and I’ve already got you hooked on data structures and how useful they can be, but
before we hop into things, let’s discuss the information that will be covered in this series.

Now before we talk about any specific data structures, we’re going to have a brief talk about
efficiency. We’ll discuss the metrics used to judge the speed and efficiency of different data
structures which will give you a better understanding of why one data structure might be used
over another one.

From there, we’ll start by diving headfirst into what I believe are the basic Data Structures, those
being Arrays and arrayLists. While you may already have a good understanding of what these
are, I highly suggest you still watch these segments, because we’ll be going into a little more
depth as to why they’re so useful based on the differences in how they’re stored in the
computer’s memory.
After that we’ll move on to what I’ll call the intermediate Data Structures. These are a little more
complicated than the basics, and have a few special attributes which make them stand out from
the rest. We’ll begin by taking a look at Stacks, which are the backbone for all recursive
processes in your computer. Then, we’ll look at the antithesis of a Stack, the Queue. Moving on
from there, we’ll be covering LinkedLists and their evolved form in the Doubly-LinkedList,
before moving on to the final of our intermediate data structures, the dictionary, which includes a
mini-lesson on Hash-Tables.

Then, we’ll wrap up the series talking about Trees and Tree-based data structures. Less linear
and more abstract Data Structures, beginning with the tree itself. We’ll then move on to Tries, a
very useful data structure used for a lot of word processing algorithms. And finally end off the
series with a discussion on Heaps and Graphs.

So in total, I’ll be taking you through 4 different segments containing 12 of the most common
Data Structures that are practically used, as well as providing examples of where and when to
use them. Section 1 on efficiency, section 2 on basic data structures, sections 3 on intermediate
data structures, and wrapping it up with section 4 on Tree-based data structures. With this
knowledge, you’ll be able to take charge of your programming career and hopefully gain a
competitive edge by implementing them when needed.

Like I said; however, before we jump straight into the different ways to store information, I’d
like to have a quick discussion on how we “score” the efficiency of these data structures using
what is known as Big O notation, so let’s jump right in.

Okay so before we talk about all these crazy data structures like maps and heaps, we want a
quantifiable way to measure how efficient certain data structures are at the different tasks we
might ask of it. If we’re going to be storing extremely large amounts of data, being able to search
through, modify, or access the information within a data structure needs to be fast and efficient.
As we briefly mentioned before, the industry standard for this kind of measurement is BigO
notation. So what exactly is BigO notation and how do we measure it for a specific data
structure? Well that’s what we’ll be covering in this next segment. For most of the basic and
intermediate data structures in this series we’re going to be spending some time talking about it’s
efficiency using BigO notation, so this is definitely a topic you’re NOT gonna want to skip. With
that being said, let’s begin.

So because there are so many different ways to store information, like we talked about in the
previous segment, programmers developed this idea of BigO notation as a way to basically
“score” a data structure based on a few different criteria. This criteria can change based on who
you ask, but for the purposes of this video, we will be using 4 criteria representing the most
common functions you might want from a data structure. The ability to access a specific element
within the data structure, search for a particular element within the data structure, insert an
element into the data structure, and remove an element from the data structure.

Now, by measuring how efficiently a certain data structure can do these 4 things we can
basically create a report card of sorts which measures how effective a certain data structure is. At
a glance, this gives us a pretty good overview of what a certain data structure is good at, and
what it is bad at, and can help us decide which one to use. If we need to store data that is easily
accessible to the end user; for example, we might choose a data structure which can access
elements the quickest. Vice versa, if accessing elements isn’t the most important thing to us, but
we need a data structure which can be easily added to and deleted from, we would go for one
which is most efficient in that specific functionality. By looking at a data structures “report card”
if you will, we can get a quick sneak peek at what they’re good at and what they’re bad at.

Now if we use BigO notation to basically create a “Report Card” like I said, there must be some
way to actually “grade” each of these functions. And there is. The 4 criteria mentioned,
accessing, searching, inserting, and deleting, are all scored using BigO notation Time
Complexity equations. What is a BigO notation time complexity equation? Well I’m glad you
asked.

Besides being an annoyingly long phrase, a BigO Notation Time Complexity equation - or just
Time Complexity equations from now on - works by inserting the size of the data-set as an
integer n, and returning the number of operations that need to be conducted by the computer
before the function can finish. The integer n is simply the size or amount of elements contained
within the data set. So for example, if we have an array, one of the data structures we’ll get to
soon, with a size of 10, we would place 10 into the different efficiency equations for accessing,
searching, inserting, and deleting that represent an array, and returned back to us would be the
number of operations that need to be conducted by the computer before completion of that
function. It does get more complicated than just this, but for the sake of keeping this series
simple, all you need to know is that these equations help represent efficiency amongst different
functions.

Also, an important thing to note here is that we always use the worst-case scenario when judging
these data structures. This is because we always want to prepare for the worst, and know which
data structures are going to be able to preform under the worst conditions. You’ll see what this
means in greater detail a little later on but for now just keep in mind that when judging data
structures, we use the worst-case scenario.

Moving on, the reason it’s called BigO notation is because the syntax for these particular
equations includes a “BigO”, and then a set of parenthesis. Inside the parenthesis will include
some function which will correctly return the number of operations needed to be run by the
computer. So, for example, let’s say we have a fake function, it can be really anything, the
purpose in this case is irrelevant. Now for this fake function, let’s say it’s time complexity
equation was the one shown on your screen now. We would pronounce this time complexity
equation as “O of 2”, meaning it takes 2 operations from the computer before our make-believe
function can finish. If the time complexity equation was O with a 5 in the parenthesis instead it
would be “O of 5” and so on.

Now these are examples of time complexity equations which run in constant time, meaning no
matter the size of our data-set, it will always take the same number of instructions to run. This is
usually not going to be the case though when it comes to the 4 criteria we want to measure for
our Data Structures. Most of the time, our integer n; which again contains the amount of
elements within the data-set, is going to have some adverse-effect on how many operations it
takes to, say, search through our data structure, which makes sense. A larger data set means it’s
going to take more operations from the computer to search through that entire data set.

Now we score these 4 functionalities in number of operations performed because measuring by


how long the function takes to run would be silly. If we measured by a metric such as time, our
results would be highly biased by the hardware used to run the function. A super-computer used
by Google is obviously going to be able to search through a data-structure much faster than a
laptop. Time Complexity equations level the playing field by instead returning the number of
operations to eliminate the bias in processing power that exists.

So to sum everything we’ve learned so far, we measure the efficiency or speed of a data structure
based on how well it can perform 4 basic tasks, accessing, searching for, inserting, and deleting
elements within itself. Each of these criteria is modeled by an equation which takes in the size of
the data structure in number of elements, n, and returns back the number of operations needed to
be performed by the computer to complete that task. By measuring these 4 metrics, we can get a
pretty good understanding of what the data structure is good at, and what the data structure is bad
at.

Now it’s important to note that this isn’t the end all be all for deciding on which data structure to
use in your program. As you’ll see as this video progresses, many of these data structures were
structured with specific quarks or features which separate them from the rest and provide
additional functionality. BigO notation is incredibly useful and something that you should
definitely take into consideration when determining which data structure to implement into your
program, but it shouldn’t be the only thing you use.

Okay cool, now that we know a little bit about how we measure the efficiency of a data structure
using Time Complexity equations, along with the 4 criteria used to actually measure them, let’s
dive straight into what the actual equations mean in terms of efficiency. That way when we start
grading Data Structure’s, you’ll have a good idea as to how good or bad each one is at that
particular metric. Basically, we’re going to cover the 6 most common Time Complexity
equations from most efficient to least efficient.

The absolute best a data structure can “score” on each criteria is a Time Complexity equation of
O(1). This essentially means that NO MATTER what the size of your data set is, the task will be
completed in a single step. If your data set has 1 element, the computer will finish the task in one
step, if your data has 100 elements, 1 step, 1 Million elements? 1 step. 800 Quadrillion elements?
Does Not Matter, the computer will finish the task in a single step. This is why when we look at
a graph of volume of data versus instructions required, the line remains constant at 1. No matter
the volume of data stored within the data structure, the computer can complete that task in a
single instruction, whether it be accessing, searching, inserting, or deleting. O(1) is the gold
standard, absolute best, top of its class time complexity equation. It is basically the Michael
Jordan of Big O Notation when it comes to Time Complexity equations.

The next fastest type of time complexity equation is O(log n). While not as fast as instantaneous
time, a function having a Time Complexity of O(log n) will still provide you with very fast
completion. Now if you don’t fully understand logarithms entirely, just know that this efficiency
is slower than instantaneous time, O(1), and faster than the next level of efficiency known as
O(n). Additionally, because the volume of data versus time graph follows a logarithmic curve,
the larger the data set you use, the more bang for your buck you’re going to get. Basically, as the
amount of data you try to perform one of our 4 criteria on increases, the rate of change of the
amount of operations it takes to complete that certain task decreases. So as you can see at the
beginning of the graph here, when we increase the volume of data, our number of operations
skyrockets, but when we do the same for larger and larger sets of data, our number of operations
increases much more slowly than before. A good example of a non data structures related
function which has a time complexity of O(log n) is the binary search. If you know what a binary
search is then you’ll understand how it is that when the data-set gets larger, the number of
instructions needed to find the item you’re searching for doesn’t increase at the same rate. If you
DON’T know what a binary search is but would like to know in order to better understand O(log
n), both a card in the top right, and a link in the description below will take you to the point in
our Introduction to Programming series where you can learn about it. If not, let’s move on to the
next equation.

O(n) is the next common time complexity equation type that’s going to come up frequently
during this lecture. The graph of volume of data versus instructions needed for this function is
linear, meaning for every element you add to the data set, the amount of instructions needed to
complete the function will increase by the same amount. So to perform a function with a time
complexity of O(n) on a data set with 10 elements, it will take 10 instructions. 50 elements will
take 50 instructions, 1,000 elements 1,000 instructions and so on. O(n) is really the last “good”
Time Complexity equation that exists. Anything above this is considered inefficient and not very
practical when it comes to data structures in Computer Science.
The next type of equation that will come up is O(n log n). This equation is the first that is
relatively bad in terms of efficiency. The graph of volume versus operations shows a somewhat
linear but increasing graph, meaning unlike O(log n) it won’t be better in terms of efficiency as
the size of the data-set increases. Instead, the slope actually increases as the volume of data does.

The last 2 types of equations are O(n^2) and O(2^n). These are incredibly inefficient equations
which should be avoided if at all possible. Because they are both exponential in structure, which
can be seen from their graphs of volume versus operations, the larger the data-set you use, the
more inefficient it will become.

While there are more Time Complexity equations that exist such as O(n!) which is even worse
then the 2 I just mentioned, the data structures we’ll be talking about will never have Time
Complexity’s equations that exist outside of the 5 we’ve just covered for our criteria, so we’re
going to stop there.

Now the final thing I want to mention is just a reiteration of something I said before. These time
complexity equations are NOT the only metric you should be using to gauge which data structure
to use. As we get deeper and deeper into the series, you’ll see we might have some Data
Structure which don’t seem that efficient at all based on their Time Complexity equations, but
provide some other functionality or feature which make them extremely useful for programmers.

Okay, now that we have some knowledge on how we “grade” these Data Structures in terms of
efficiency, let’s hop into our very first data structure.

The first data structure we’re going to be covering is the array. Now I understand the array is a
pretty common data structure taught in most programming classes, and that many of you might
already know about the properties and uses of an array, but in this segment, we’ll be going into
more detail on the Array as a data structure, including it’s time complexity equations, storage
methods and more, so check the description for a Timestamp to skip ahead to that section if you
already know the basics. If you’re not as versed in the array though, or just want a general
review, then stick around, because right now we’ll be discussing the basics of an array.

An array is fundamentally a list of similar values grouped together in a central location, which
makes sense, because if you remember, we use data structures to store sets of similar information
so that we can easily use that information. Basically, arrays can be used to store anything really,
usernames for an app, high scores for a video game, prices for an online shop, pretty much any
list of values which are fundamentally similar, meaning of the same type, so integers, Strings,
floats, objects, the list goes on and on. Any primitive or advanced data type you can think of can
be stored within an array.
Every item contained within an Array is referred to as an “element” of that array, and we call the
collective total of elements the array. This is going to be true for almost all the Data Structures
we talk about by the way, so keep that in mind.

An array will also have three attributes associated with it. The first being a name for the array,
the second a type for the array, and the third, a size for the array. Let’s start with the name.

The name, if you couldn’t tell, is just simply a name for the array, which can be used to reference
and interact with it. Say, for example, say we had an array called names which contained a list of
company employees. And then we also had an array called salaries which contained a list of
salaries for those employees. If we wanted to search through the list of salaries, we would do so
by referencing the salaries array with its name. This way, the computer knows that you want to
search through that specific array containing salaries, and not the names array instead. This
works vice versa as well of course, if we wanted to search through the names array instead.

Now, another thing to note really quickly while we have this up, is that these 2 arrays are also an
example of parallel arrays. Parallel arrays are 2 or more arrays containing the same number of
elements, and have the property wherein each element in one array is related to the element in
the other array or arrays of the same position. So, from our example, we can say that the first
element in the names array, John Smith, corresponds to the first element in the salary array, the
integer 10,000. This means John Smith has a salary of 10,000. This is really good because as
you’ll see later on, we can’t store differing types of variables in the same array. So we couldn’t
have the salary integers be stored in the same place as the name Strings, making parallel arrays
extremely useful for storing differing types of data about the same entity. Alright, tangent over,
let’s get back to the attributes of arrays, and the second one we’ll be talking about, the array’s
type.

Now an array’s type is simply what type of information is stored or will be stored within that
array. One of the biggest stipulations about arrays is that it HAS to hold all the same type of
information. So you cannot have an array containing integers AND strings, it would have to be
an array of only integers or only strings. This works for any type of information you would want
to store within the array, meaning every element, or spot, within the array must be of the same
type.

The third and final attribute that an array has is a size. This size is a set integer that is fixed upon
creation of the array. It represents the total amount of elements that are able to be stored within
the array. An array’s size cannot be changed. I’ll repeat that again because it’s extremely
important. An Array’s size CANNOT BE CHANGED once created. Once you write the line of
code which instantiates an array, you give it a defined size, or fill it with elements until it has a
defined size, and then at that point, The array’s size cannot be changed by conventional methods
past this point. This is actually pretty rare for a data structure, and it may seem really counter-
intuitive and useless, but later on when we talk about an array’s efficiency, you’ll see that this is
for a specific reason and not just to be annoying to the programmer.

So there you have it, the three attributes of an array. A name to reference it, a type to fill it with,
and a size to control it. Let’s now shift focus and talk about how we actually define an array
ourselves, how we reference and change values within an array, and then dive into the idea of 2-
dimensional arrays.

There are actually 2 different ways to instantiate an array in most languages. You can either
populate the array with the elements that you want contained within it right then and there, or
you can set a specific size for the array, and then slowly populate it later on as the program runs.
We’ll be discussing how to do both, starting with the first method, creating the array with
elements already stored.

Now defining and filling an array as soon as you create it is used mainly for when you already
know which values are going to be held within it. For example, let’s say you're creating an array
which is going to hold the salary of 10 different workers at a company, and that information is
being read from a text file. Well, you already know the salaries for those 10 workers from the
text file, and so you’re able to immediately populate the array when you create it. That way,
you’ll have the information available to you as the program runs.

The way you do this varies amongst different programming languages, but it’s shown here in 3
different languages, Java, Python, and C#, to give you a basic gist of how this is accomplished.
All three lines of code on your screen will create an integer array of size 3, containing the
numbers 1, 2, and 3. You can see that all of them involve the declaration of an array name, which
is then set equal to the values you want to include within the array encapsulated by some sort of
brackets or braces. This isn’t 100% how it’s going to be for all languages, but for the most part it
will follow this skeletal outline.

As you can see java and C# require you to define the array type, whereas python does not, but
for the most part the syntax is the same. Again that’s “Name of array, set equal to list of comma-
separated values encapsulated by brackets”. This also develops the size of the array for you
automatically, so since we populate the array with the numbers 1,2, and 3, the array would
instinctively have a size of 3, as it now contains 3 integers. If we were to say go back and ADD
in a 4th number where we define the array, the size would dynamically increase to 4 the next
time we run the code.

Now the second way in which we can instantiate an array is by setting an initial size for our
array, but not filling it with any elements, then slowly populating it as the program runs. This
would be used in the case that you don’t actually know what information you’re going to store in
the array, but will as the program runs through. The most common way in which this happens is
in the case where the end-user will enter information into the program, and then that information
will be stored inside the array. Since the information varies based on which user runs the code
and what they input, we have no way of populating the array as soon as it’s initialized, forcing us
to add the information later.

Shown on your screen now are 2 ways in which we can do this in Java, and C#. Now you’ll
notice there is no Python section because of the fact that creating populate-later arrays is not
conventionally possible in the base version of python without the use of more advanced data
structures or packages. You can see; however, with Java and C# that just as with the other
example, there are slight differences in the syntax on how we do this, but generally it follows
some set pattern. The type of array, followed by a name for the array which is then set equal to a
size for the array. Remember, this size is final and cannot be changed outside of this initial call.
If you create an array with a size of 10, but then later in the program find you need more space
for the array, you can always go back to where you instantiated it and change the size, but after
the line of code runs, there is no conventional way to increase its size. You may also be
wondering what the brackets mean when instantiating an array, and that’s just a way to signify to
the computer that we are indeed trying to create an array, and not just a variable.

Now that we know the 2 different ways to instantiate an array, we need to know how we as a
programmer actually get the information that is stored within the array so that we’re able to use
it. And the simplest way to answer that question is through the means of a numerical index. An
index is simply an integer which corresponds to an element within the array.

Now the most important thing to know about indexes is that for a certain array, they begin at 0
instead of 1. So if we have an array of 10 elements, the first “Index” would actually be index 0,
the second would be index 1, and so on all the way up to 9. It’s a little weird at first, but trust me
you get used to it.

Now to retrieve information from a certain position within the array, you would reference it
using both the array’s name, and the index number of the element you wish to retrieve. Say we
have our array called numbers here which contains the integers 1-10. To print out the element at
the fifth index, in this case the number 6, we would reference the array name, in this case
numbers, and then in a set of brackets in which we would place the index we want to retrieve, in
this case the number 5. What this piece of code is basically telling the Computer is print out the
fifth index of the number’s array, which again is the integer 6. Now because indexes start at 0
instead of 1, it’s important to note that to get the 10th element in the array, you would actually
need to reference it using the number 9, since there is no 10th index. If you do end up making
this mistake it will result in an arrayOutOfBounds error, as there is no 10th index in the bounds
of the numbers array.
Referencing an array’s index is also how we replace elements within an array. Continuing with
our numbers array, let’s say we wanted to change the last element, the integer 10, to instead be
the integer 11. What we would do is reference the 9th index of the numbers array, the one which
currently contains the integer 10, and set it equal to 11. This tells the computer to take the
element at index 9, and replace it with the integer 11, essentially swapping 10 for 11.

Now the last thing I want to talk about before we jump into the time complexity equations for an
array is the practice of putting array’s inside of arrays. An array with an array at each element is
known as a 2-dimensional array. Think of a 2-dimensional array as a matrix. It’s similar to any
other array except for the fact that instead of a primitive data type like an integer at each element,
we would instead have another array, with its own size and indexes. 2-dimensional arrays are
useful for a variety of purposes, such as if you were programming a chess-board, a bingo board,
or even an image where each element in the 2-dimensional array contains an RGB value which,
when combined, creates an image.

Now referencing an element within a 2-dimensional array is mostly the same as with 1-
dimensional arrays, except you now need 2 indexes. The first number would be the index of the
column you want to reference, and the second number would be the index of the row you want to
reference. By combining these two numbers we can single out an element within the 2-
Dimensional array that will then be returned to us.

As an example, let’s create a 2-dimensional array with 16 names contained within 4 rows and 4
columns. Now, to access the name Carl, you’d first single out the column which contains the
name you’re looking for. In this case it's in the 3rd column, meaning it’s in the 2nd index. Next
you would single out the row which contains the name you’re looking for. In this case the name
Carl is in the 3rd row, so we would again use the second index to reference it. Combining these 2
pieces of information gives us the index location 2,2. And if you look, Carl is indeed at index
location 2,2. Just as another example, Adam is in the 1st column, 2 rows down, and so to
reference Adam, you would simply call upon the element at location 0, 1.

2-dimensional arrays are just the beginning, you can also have 3-dimensional arrays, 4-
dimensional arrays and so on for containing a large amount of advanced relational data, but 2
dimensions is where we’re going to stop for today.

Alright. That concludes the background information on arrays. Now that we know what they are,
let’s actually talk about arrays as a data structure. This is where we’re finally going to be using
our BigO notation knowledge from the previous segment to judge the array as a data structure.
We’ll be going through the 4 criteria we talked about previously, accessing elements within an
array, searching for an element within an array, inserting an element into an array, and deleting
an element from an array, and scoring it based on how efficiently it can complete these 4 tasks
using Time Complexity equations.
Now accessing an element within an array can be conducted in instantaneous O(1) time. This
means that for any element within your array that you want to know the contents of, you can do
so immediately by simply calling upon it’s index location. This has to do with the way that an
array is stored within memory.

For example sake, let's look at a fake portion of memory in a computer. You can see we have
some information near the beginning, maybe some integers or strings we’ve initialized, that’s not
important. What’s important is that when we get to the part in our code which instantiates an
array, we already know exactly how large the array is going to be, either because we’ve already
populated it with elements or given it a definitive final size. This means we also know exactly
how much space we need to reserve in memory for that array. Combining these two pieces of
knowledge means we can instantaneously figure out the location of EVERY single element
within the array by simply taking the starting location of the array in memory, and then adding to
it the index of the element we’re trying to find.

So, let’s say our array contains 3 elements, each of them integers, and we want to know what the
element at the 1st index is. Well, we know that the array starts storing data here, and we also
know that every element within the array is stored contiguously together in memory, and so by
taking the starting location for the array in memory, and adding 1 to it, we now know that’s
where the 1st index of our array is going is going to be and can return that stored value
appropriately. This is the main reason why array sizes cannot be changed. All of the information
within an array must be stored in this one place so that we’re able to do this. The contiguous
structure of an array prevents you from adding space to it after it’s been initialized, because it
literally can’t without breaking up the data, and adding in the space to store new elements down
the road, away from the initial array. This would make accessing elements instantaneously not
possible.

Hopefully now you can see both why accessing an element within an array is O(1), as well as
why array sizes are final, because not many other data structures allow you to have instantaneous
accessing power, meaning array’s have a huge advantage on the others in this metric.

Searching through an array is O(n). This is because for the most part, you will be working with
unsorted arrays, those being arrays which are not in alphabetical, numerical, or some sort of
quantifiable order, meaning that to find an element within the array, you may have to search
through each element before you find it. This is known as a linear search, and if you want to
know more about it, click the card in the top right corner and it will take you to that section of
our Introduction to Programming Series, a link will also be provided in the description. Now
while there are faster ways to search through an array, those only work when the array is sorted,
and so for a basic array, searching through it to find a particular element is going to be o(N).
Now sure, there are going to be cases where the element you’re searching for is the first element
in the array, or even somewhere in the middle, but remember that when working with BigO
notation, we always use the worst-case scenario, and in this case, the worst-case scenario is that
the item you’re searching for ends up being the last element in the array, which for an array of
size N, will take n operations to reach.

Finally, inserting and deleting elements from an array both have a Time Complexity equation of
O(N).

For inserting, this is because adding an element into the array requires you to shift every element
that's after the index you want to insert the value at to the right one space. For example, if you
have an array of size 5, currently filled with 4 numbers, and you want to insert the number 1 into
the array at index 0. You would first need to shift every element to the right of the 0th index right
one, essentially freeing up the space for the new integer while also retaining all the information
currently stored in the array. Then and only then can you actually insert it. This requires you to
traverse through the whole array of size n, which is why the time complexity equation is o(n).
Now, sometimes you might not have to shift the whole list, say in the case you had the same
array of numbers, only this time the 4th index was free, and you wanted to insert the number 5 in
that open spot. Since you don’t have to move everything to the right 1 in order to make space for
this new element, does that make inserting into an array have a time complexity of O(1)? Well
no, because again, when we talk about a function's time complexity equation, we always refer to
it in the worst-case-scenario. The MOST amount of operations that we’re going to have to
conduct before we can insert an element within an array is n, the size of the list, making it’s time
complexity equation O(n).

Deleting an element from an array follows mostly the same idea. You shift every element to the
right of the one you want to delete down one index, and you essentially have deleted that
element. Again, let’s say we had an array of numbers 1-5, only this time, we want to delete the
number 1 at the 0th index. What we would do is set the 0th index to whatever is contained within
the 1st index, in this case 2. And then repeat this process until we get to the end of the array,
finishing it off by setting the last element in the array to be some null value. This deletes the
element from the array by basically replacing it with the value to the right, and then this process
is repeated until it frees up a space at the end. Essentially, we’re just moving the array down one.
Worst-case-scenario, we’re going to have to shift every element in the array of size N, making
deleting from an array also have a time complexity of O(n).

So there you have it, the 4 time complexity equations for an array. Accessing is O(1), and
searching, inserting, and deleting are all O(n).

Arrays are a really good data structure for storing similar contiguous data. Its ability to access
any item in constant time makes it extremely useful programs in which you want to be able to
have instant access to the information contained within your data structure, like a database. It’s
also a lot more basic than some of the other structures we’ll end up talking about, making it both
easy to learn, and reducing the complexity of your code. Some disadvantages of an array are the
fact that the size of the array cannot be changed once initialized, inserting and deleting from the
array can take quite a while if you’re performing the function on larger data sets, and if you have
an array which is not full of elements, you’re essentially wasting memory space until you fill that
index location with a piece of data. Overall, the array is a pretty reliable data structure, it has
some flaws as well as advantages. A program you write could use an array if need be, and it
would work fine, but oftentimes you might want some extra functionality, and that’s where more
advanced data structures come into play.

One of those more advanced data structures is what’s known as the ArrayList.

The arrayList, fundamentally, can be thought of as a growing array. We just finished talking
about the array, and how one of its major flaws was the fact that once initialized, an array’s size
could not be changed using conventional methods. Well, in contrast, an ArrayList’s size expands
as the programmer needs. If you take the arrayList on the screen screen now, full of 4 elements,
and you decide to add one to it, it will simply expand its size to fit 5 elements. As you can
probably tell, this is extremely useful, which begs the question of “why not just ALWAYS use
arrayLists”, I mean in comparison, the arrayList seems to provide all the functionality of an
Array and then some.. Well that’s definitely a valid question, and one we WILL get to that later
on in this section, but before we can do that, we need to cover the basics of an arrayList -
including some of the properties and methods associated with it. Alright, let’s hop right in.

So, as we’ve said, an arrayList is simply a resizable array, making them extremely similar in
structure. This is furthered by the fact that the arrayList is actually backed by an array in
memory, meaning that behind the scenes of your code, the arrayList data structure uses an array
as it’s scaffolding system. For this series we need not go further than that, but for us it just means
a lot of the functionality will be the same. Now this doesn’t mean EVERYTHING is going to be
exactly the same, which you’ll see later on, but it’s still important to make note of before we get
too deep into things.

The next thing I want to do before we talk functionality is actually go over how to initialize an
arrayList. To do this again is going to vary based on which language you’re using, so shown on
your screen now are 2 different ways to do so, Java and C#. You may notice that again there is
no Python on the screen, and this is because in the base version of Python, arrays and arrayLists
are actually not separate entities. They are frankensteined together into a single data structure
called Lists. Lists take some functionality from arrays, and some from arrayLists. It’s a lot more
complicated than that but for this series that’s all you need to know as to why we are not
including python in this section. We discussed initializing python Lists in the previous section so
if you’re interested you can go back and look at that.
Now, going back to arrayList initializations, another thing you may notice is that it looks a little
bit awkward in the way that these statements are structured, and that’s mainly due to the fact that
the arrayList is actually it’s own separate class, outside of the base version of these 2 languages.
Now, I’m not going to get into class hierarchy and object-oriented programming right now
because that’s a whole other topic with big concepts and even bigger words. For right now, this
just means that to create a new arrayList, we have to invoke the arrayList class when defining it,
which as you can see is done at the beginning of both initializations.

After that, you can see we give it a name, and then set it equal to new ArrayList with a set of
parentheses. Now in these parenthesis you have a few options, you can either enter in an integer,
which will then be used to define a size for the arrayList, or you can just leave it blank. Leaving
the parenthesis blank like that will automatically define a pre-set size of the arrayList at 10.
Again, remember it can be dynamically increased as time goes on if we add enough elements,
this is just a base size.
Now, you may be wondering if we can actually populate the arrayList with elements when
instantiating it, as we could with arrays, but arrayLists actually do not support this type of
declaration.

Moving on, let’s talk about functionality. Now, the arrayList can be thought of as pretty much
the evolved form of an array. It’s a bit beefier, has a little bit more functionality, and is overall
more powerful than an array. That’s certainly not to say it’s better in every case, but for the most
part the arrayList is going to be thought of as the older sibling amongst the two. This is attributed
to the fact that it belongs to that pre-built arrayList “class” which we talked about earlier. The
fact that the arrayList belongs to a class means it’s going to come with pre-built functions that
are already at our disposal from the moment we define and instantiate an arrayList. More
specifically, the arrayList comes with methods we can use to access, change, add to, or delete
from it easily. If you were using an array, you would have to program most if not all of these
methods by hand, and so having them pre-built into the arrayList class makes it especially useful.
You’ll see this is the case with a lot of data structures down the road, where having a data
structure belonging to its own class cuts out a lot of programming time you have to spend
making appropriate methods for functionality you might want.

The type of methods you’re going to get will vary based on language. For example, in Java,
you’ll have a variety of methods to use including ones to add elements to the arrayList, remove
them from the arrayList, clear the arrayList entirely, return it’s size, etc etc, as well as tons of
other more specific functions. In another language though, such as C#, you’ll have some of the
same methods as the Java arrayList, but you might also have some methods the Java version
does not, and vice versa. The C# version might not have some methods the Java version does.
The same is gonna apply for any other language you use which might implement the arrayList
data structure.
Because of the variability surrounding the arrayList amongst languages, in this series we’re
simply going to be covering 6 common methods that are both useful, and can be found in 99% of
the arrayList classes.

These 6 methods are the add method, the remove method, the get and set methods, the clear
method, and the toArray method. This may look like a lot, but remember, all of these are pre-
programmed for you, and so you don’t have to make them by hand, all you have to do is call
them on a pre-made arrayList and you are set to go. Speaking of pre-made arrayLists, before we
dive into each of these functions and how they work, let’s first create an example arrayList we
can manipulate to show how they work. Let’s call it exampleAList, and give it a size of 4 so that
it’s not pre-set to 10, and we’re set to go.

First up is the add method. Now the add method actually comes in two different types, one which
takes in only an object to add to the end of the arrayList, and one which takes in both an object to
add to the arrayList as well as an index value representing the index to insert the object at. Let’s
start with the simpler of the two, one which simply takes in an object.

This method is for more basic cases where you don’t care about where in the arrayList the object
you wish to add is going to end up. It will simply append the Object you pass in as an argument
to the end of the arrayList. So, let’s take our example arrayList and run the add method on an
integer of 2. Now, normally arrayLists only hold Objects, not primitive types like the integer 2
that we’re trying to pass in; however, the computer will automatically convert our primitive
integer into an Integer object with a value of 2, so that we can still add it easily. This is known as
auto-boxing and is going to be used throughout the rest of the series to modify data structures
with primitive types, so I just thought I’d mention it now so you’re not confused later on. Okay,
so when we run our code, since the arrayList is empty, and we ran the add method which doesn’t
care about location, it’s going to add the integer 2 at the first index, index 0. Now if we run
another add method, and this time pass in the integer 5 as an argument, since the 0th index is
already taken, it will be slotted in at the first available open index, that being the 1st. So as you
can see, pretty simple, takes in an object and will put it at the first open location available.

Moving on to the second type of add method, one which takes in an object to add to the arrayList
as well as an index to place it at. This one works very similarly to the previous, only it makes
sure the object you’re adding is appended at the index provided. Again, let’s say we now want to
add the number 1 to our example arrayList, but we want to make sure it’s placed in numerical
order, in this case at the 0th index. What we do is call the add method, providing the integer 1 as
an argument in addition to the index 0 we want to add to the arrayList. Once the code has run,
the arrayList will automatically shift our integers 2 and 5 to the right one, in order to make space
for the integer 1. This works for any index contained within the bounds of the arrayList. So if we
wanted to do it again, only this time insert the number 3 in at the 2nd index, so that the List
remains in numerical order, we’d call exampleAList.add, and then in the parenthesis pass in the
integer 3, and the index location 2. After the code has run, you’ll see we have added the Integer
into our arrayList. Now it’s important to note that because there are 2 integers being passed in as
arguments, you know which one your computer is treating as the integer, and which one the
index location. Mixing these up could cause the computer to try and insert the attempted index
location at a different location than the one you were attempting to insert at, so just be careful,
and knowledgeable as to the order your method’s arguments are in.

Now the next method that comes pre-packaged in the arrayList class is the remove method, and
this one also comes with two different types. The first takes in an integer as an argument and,
just as the name suggests, will remove the Object, if there is one, at that index location and return
it back to the user. The second takes in an Object, and will remove the first instance of that
object within the arrayList, if present, and return True or False whether an Object was removed.
So, if we wanted to remove the number 5 from our arrayList, we’d have two different options.
We could call exampleAList.remove, and inside the parenthesis place the index of the value we
want to remove, in this case 3, and the program will remove the object at index 3.

The other option would be to run another remove method, only this time pass in an Integer object
of 5. It has to be an Integer object because if we were to just use 5, the computer would try to
remove the 5th index of the arrayList which doesn’t exist. By creating an Integer Object, we can
ensure that when the code runs, the computer knows we want to remove the first instance of the
number 5 in our arrayList, and running this will return True, and remove the integer from our
list. Now, if there is no integer 5 in the arrayList, the remove method will simply return false. I
quite like the number 5, so i’m actually not going to permanently remove it from the arrayList.

Up next is the get Method. Now the get method is pretty much the same as referencing an index
for an array. It takes in an index location and will return back to you the value at that location. So
exampleAList.get with an argument of 0 would return 1, exampleAList.get with an argument of
2 would return 3 and so on.

The next method is the set method, which is how we replace elements within an arrayList. Much
like the name implies, it takes in an index and an object as arguments, and will set the index
location of the index you passed in, to the object you also passed in. So if we wanted to set the
number 5 in our arrayList to be 4, so that it matches nicely with the others. What we’d do is call
exampleAList.set, and within the parenthesis pass in the index location of the element we want
to set, in this case 3, and then also the Object we want to replace at that index, in this case 4. This
call will override the element at position 3 to be 4 instead of 5. Now you should be really careful
when you’re using this method, because you don’t want to accidentally override an important
element within your arrayList.

Next up is the clear method, for when you just hate your arrayList. This is perhaps the simplest
of them all. It does not take in any arguments and will just clear the arrayList, deleting every
element entirely. Calling exampleAList.clear() on our arrayList would delete all the Objects
within it, but I don’t really want to do that right now, especially with one more method to go, so
just for the sake of this series, let’s keep the arrayList filled with the values it currently has.

The final method we’ll be covering in this section is a little bit different than the rest, and that’s
the toArray method, which is used to convert an arrayList to an array. I thought I’d include this
one because it’s really useful for combining the strengths and weaknesses of arrays and
arrayLists. The toArray method takes in no arguments, and will simply convert the arrayList into
an array. Now, for this to work of course you need to set it equal to the creation of a new array
like shown on your screen now, but if done correctly, you’ll get a brand new array which
contains all the contents of the old arrayList.

You may notice though, that instead of an array of Integers, it’s an array of Objects. This mostly
has to do with that Object oriented programming stuff we talked about in the beginning, but for
now, it won’t make too much of a difference. We can still treat it as a regular array, printing out
indexes to the console, replacing elements within the array, typical array functionality, the only
thing that changes is that it contains Integer objects instead of primitive Integer types.

So there they are, the 6 major functions that come with any given version of the arrayList class
Having these at your disposal will account for much of the functionality you might use an
arrayList for, making them extremely valuable to know.

Let’s now move on to the arrayList as a Data Structure. Again we’re going to be looking at it’s 4
time complexity equations for accessing, searching, inserting and deleting. Now if you remember
back to the beginning of this segment, we mentioned that the arrayList is backed by an array, and
this means that just like the array, it too will have O(1) accessing power. Essentially, this means
that when we use our get method which takes in an index location, it will return to us the value at
the index provided in instantaneous time. Now, you may be wondering how this is possible,
since the data stored within an arrayList is not contiguous. Well, this is actually due to a really
interesting reason, so interesting that before scripting this series, I actually had no idea was the
case. So, because it’s my series, I’m going to take a moment to talk about it.

If we pull up our example arrayList in memory, you can see that it looks a little bit different, let’s
break down what's going on. Instead of storing the actual objects which are contained within
itself, an arrayList actually stores references, or pointers, to the locations of those objects in
memory. So the 0th index, based on the arrayList, is stored at the 87th location in memory,
which is currently storing the integer 1. Checking back to our example arrayList, you’ll
remember that is indeed what was stored at the 0th index of the example arrayList. This goes for
every element within the arrayList. The 1st is stored at the 91st location, the 2nd at the 100th,
and so on. So as you can see, while the actual data is not stored contiguously, the references to
that data are. So when we run our get command, all it has to do is return the value stored
wherever the index location points towards. It’s a bit more complicated than that, especially the
way that these references get stored, but that covers the gist of it. This is the reason our arrayList
can have instantaneous accessing power without having the data stored contiguously. Alright
tangent over, let’s get back to BigO notation.

So accessing is O(1), and searching is going to be O(n). This is for the same reason that arrays
were O(n). If we want to find out if an object is contained within our arrayList, and that Object is
at the last index, we’re going to have to search through each and every element within the
arrayList of size n to make sure, because remember, we always go based on worst-case scenario.

Inserting into the arrayList is going to be O(n), because worst-case scenario, if we are inserting
an element at the beginning of the arrayList, we need to shift every element after the index we’re
inserting at to the right 1 just like we needed to do for the array. This requires a number of
operations equal to the size of the array, making inserting O(n).

Deleting is O(n) for the same reason. If we want to delete the first element within the arrayList,
we then have to shift every element down one to save space. Additionally, if we want to delete
an Object contained at the last index, we have to search through the whole list to find it. Either
way it will be O(n).

Alright, so there are our 4 Time Complexity equations. Accessing is O(1), and searching for,
inserting, and deleting are all O(n). If that sounds kind of familiar, that’s because these are the
same as the array’s Time Complexity equations, see I told you they were similar. This does bring
back up the question we posed at the beginning of the episode. Why even use an array in the first
place? In comparison to the arrayList, the array just seems not as useful. Well, let’s get into that
by sizing up these two data structures against each other mano e mano.

Let’s compare. An array is a collection with a fixed size meaning it cannot be changed, whereas
an arrayList has a dynamic size, which can be updated to fit the needs of the programmer. Arrays
can store all types of data, meaning both primitives and advanced data types, whereas arrayLists
can only store Objects, not primitives. Now this problem is mostly solved through the auto-
boxing situation I talked about previously, but the fact still stands. Moving on, an Array is built
into most languages, meaning it doesn’t have any methods pre-made for you to interact or
modify it, whereas an arrayList is it’s own class, meaning it comes with useful methods to help
you utilize it. Finally, an array is very primitive in nature, and doesn’t require a lot of memory to
store or use, whereas an arrayList is, again, a class, meaning it requires more space and time to
use than an array will.

Hopefully now you can see that while the arrayList is more powerful, it still does have some
drawbacks which make using an array sometimes more appropriate. In general, you want to use
arrays for smaller tasks where you might not be interacting or changing the data that often, and
arrayLists for more interactive programs where you’ll be modifying the data quite a bit.

So that’s the arrayList, as a review, it is a dynamically increasing array which comes with a slew
of methods to help work it. As the arrayList is a hefty topic in Computer Science, if you feel I
didn’t do a good enough job explaining some of the concepts in the video, the sources used to
write the script for this video will be linked in the description below. But if not, let’s move on to
our next data structure.

The next Data Structure we’ll be talking about is the Stack.

Now at this point in the video, we’ll be diverging from what are known as random access data
structures, i.e arrays and arrayLists, and diving into sequential access data structures. What’s the
difference? Well, if you remember back to our discussions on arrays and arrayLists, we were
able to access any element within the data structure by calling upon it’s index location. This
would then result in the computer instantaneously returning the value at that location. Each
element was independent of the one before or after it, meaning obtaining a certain element did
not rely on any of the others contained within the Data Structure. That basically describes the
gist of a random access data structure, one where each element can be accessed directly and in
constant time. A common non-computer science example of random access would be a book.
Getting the information contained on a certain page doesn’t depend on all the other pages within
that book, and getting an element contained within a random access data structure doesn’t
depend on all the other elements contained within that data structure.

In contrast, elements in a sequential access data structure can only be accessed in a particular
order. Each element within the data structure is dependent on the others, and may only be
obtainable through those other elements. Most of the time, this means accessing a certain
element won’t be instantaneous. A common non-computer science example of sequential access
would be a tape measure. To get to the measurement of 72 inches, you would first have to go
through inches 1 through 71. There’s no way to just instantly get to 72 inches without first
breaking every law of thermodynamics and probably the space-time continuum. These are also
sometimes called limited access data structures, because unlike random access, the data is
limited by having to obtain it through a certain way.

So there you have it. Random access data structures which allow instantaneous accessing power
and independent elements such as the Array and ArraList. And then you have sequential access
data structures which only allow accessing through a particular order with dependent elements.
Through the next few segments, we’ll be covering a few of the popular sequential access data
structures, such as Stacks, Queue’s, Linked List, and so on, beginning of course with the Stack.
We’ve already danced around the subject long enough so let’s finally talk about what a Stack
actually is.
Now the stack, by definition, is a sequential access data structure in which we add elements and
remove elements according to the LIFO principle. The LIFO principle, which stands for last in
first out, means that whichever element we added to the Stack last, will be the first one we
retrieve. Hence, last in. First Out.

Think of this as a stack of books. Each time you add another book to the top, you do so by
“stacking” it on top of the others. Then, if we wanted to get a book in the middle of that stack,
we would first have to take off all the books on top of it. We can’t just grab that book from the
Stack and expect the entire thing not to fall in on itself like some sort of literaturistic Jenga.

The same goes for the stack as a data structure. We add elements to the top of a stack, and
retrieve them by taking them off the top of the stack. There’s only one way in, and one way out
for the data. We can’t simply access or modify any element within the stack willy-nilly like we
were able to do with the array and arrayList. This might seem like a weakness, limiting the flow
of data to one point, but remember what I said during the segment on Time Complexity, many of
these data structures have a built-in functionality which gives them an edge over the others, and
the stack, with it’s LIFO property, is a prime example of that.

Let’s now talk about some of the methods commonly associated with the Stack.

The Stack class will ALWAYS come with two methods built into its class, those being push and
pop. These are the fundamental methods which we use to interact and modify with the stack,
making them extremely important to comprehend. In addition to those 2, I’m also going to cover
2 more methods, peek and contains, which are both useful and can be found in the Stack class
associated with most programming languages.

Just like we had an example arrayList, let’s also create an example Stack to help us show off
these methods, and just like that we’re ready to roll.

Now push is a method which “pushes” an object onto the top of the Stack. All it takes in as an
argument is an Object to add to the stack, and will return no value. When we do this, that Object
becomes the forefront of the Stack, and it’s size is dynamically increased by 1. Let’s start
running some push commands on a variety of different Strings. You’ll see that the Stack slowly
gets built. “Now” “Channel” “This” “To” “Subscribe”. A series of completely random words
seamlessly pushed onto the stack. As you can see, we’re only adding information to one spot, the
top. There is no insert method where we can jam a String into the middle of the Stack.

Moving on, the Pop method is used to remove an element from the top of the Stack. It doesn’t
take in any arguments, and will return the element that is popped off the Stack back to the user.
Once a pop method has run, the element that was at the top of the Stack is removed and returned,
and the element which was 2nd from the top becomes the new top of the Stack. So, on our
exampleStack, if we popped off each element, you’d see that each time, one of those random
Strings is taken from the Stack and returned back to the user, until we are left with a sad empty
stack with no shameless plug. Let’s add the Strings back, obviously just for the sake of the next 2
methods we need to cover, and not for continuing the free advertising.

So push and pop are how we add and remove the data within our Stack, so they’re fundamentally
the backbone of the Data Structure. The next 2 I want to talk about, peek and contains, are more
so used to interact with the data inside the stack without actually changing it. Not as useful for
modifying data within the Stack, but still extremely important

The first of these is the peek method. The peek method allows you to get the value at the top of
the list without removing it. It just takes a peek. We talked about before that the only way to
access elements in a Stack was through the top, and this method is simply a way to look at what
that top value is without having to pop it off. It takes in no arguments and will just return back
the contents of the top element. In our case, if we ran it on our exampleStack, “Subscribe” would
be returned to the user. Now, if we popped off the top element and ran it again, “to” would be
returned instead. You get the idea. Again, let’s push Subscribe back onto the top for...education
purposes.

Now the final method I’ll be covering is the contains method. This one is used for searching
through the Stack. It takes in an Object as an argument and will return a boolean of whether or
not that item is contained within the Stack. Essentially, this method allows us to search through
the Stack without popping off every element until we find the one we’re looking for, as the
contains method does not modify the Stack in any way. So for example, exampleStack.contains
with an argument of Subscribe would return true, exampleStack.contians with an argument of
This would also return true, but exampleStack.contains with an argument of hello would return
false.

So there they are. 4 common Stack functions which are going to be vital if you ever want to
construct a Stack-based program.

Moving on to Time Complexity. For accessing, the Stack has a Time Complexity equation of
O(n). This is because in order for us to reach a certain element within the Stack, we first have to
pop off every element that’s above it. Think of it like this, if we had a stack of stones, and
needed to get to the bottom one, we’d first have to take off every stone on top of it. So, worst-
case scenario, if the element we want is at the bottom of the Stack, we first have to pop off every
element above it. This would require a number of operations equal to the size of the stack,
making the Time Complexity O(n).
This is one of the major drawbacks to using Stacks. With arrays and arrayLists we could access
any element within the data structure instantaneously, and with the Stack that’s just not possible
because of the way it’s structured.

Searching is going to be O(n) for the same exact reason. Worst-case scenario, if we’re searching
for an element that's at the bottom of the Stack, we have to go through the whole thing just to
find it.

Now inserting and deleting make up for this by having Time Complexity equations of O(1). This
essentially boils down to the fact that using our push and pop methods really only requires 1
operation. Since the data only flows in and out of a single point, inserting or removing an Object
from that point can be done immediately. For the Push method we simply add it to the top of the
Stack, and for the pop method we just take it off from the top, it’s not rocket science. Actually
it’s computer science but that’s besides the point. There’s no need to rearrange data or move
elements around like there was for the array and arrayList, because we don’t have to. The data
we’re modifying is right there on top.

So there you go. The Time Complexity equations for the Stack. Now, you might be wondering if
there are even uses for a First In First Out data structure, because it seems kinda out there. I
mean. Limiting your data to a single entry point? But you would actually be mistaken. Stacks are
used everywhere, both in the actual writing of other code, as well as in real-world situations.

In fact, one of the most fundamental processes of programming, uses Stacks as a way of keeping
track of active functions or subroutines. Of course I’m talking about recursion. Now, I won’t get
into recursion too much here, but basically it’s the process of functions repeatedly calling
themselves. When the function calls itself, that call is added to a stack of processes, and when
that stack finally reaches what’s known as a base case, the functions are then all popped off one
by one. It goes much much deeper than that, but we don’t have time for a full-blown lesson on
recursion. If you want that, you can click the card in the top right corner of your screen, or the
link in the description below which will take you to the part in our Introduction to Programming
series where we cover it. Either way, Stacks are the backbone for recursion, and recursion is the
backbone for a plethora of Computer Science related functionality such as traversing data
structures, keeping track of active sub-routines in code, and much much more.

Some examples of Stack-based functions outside of Computer Programmer which you use every
day include the undo redo button in word processors and the back-paging on web engines. Both
use Stacks similarly. They continually add tasks you’ve completed to a stack, either web pages
you’ve visited or words you’ve typed, and then when you press undo, or press the back button in
a web browser, all we have to do pop whatever the last task was off the stack and boom, you’re
right back to where you were a second ago. It’s like magic, but better.
As you can see, the Stack has a LOT of real-world applications on both the consumer side and
the client side. You interact and use Stacks every day without even realizing, and will use Stacks
frequently as you continue along your Computer Science journey, and so by learning them
you’re opening up a world of opportunities.

That concludes our discussion on the Stack. To review, it is a sequential access data structure in
which we use the LIFO principle to add and remove elements from. And again that stands for
Last in First Out.

Up next, we’ll be talking about an equally useful data structure that functions very differently
than the stack while also working very similarly.

And that data structure is known as the Queue.

So now that we’ve talked about the Stack, a sequential access data structure which follows the
LIFO principle, we need to cover the opposite, which in Computer Science we call a Queue

By definition, a Queue is a Sequential access data Structure which follows the FIFO principle, or
First in First Out. The Stack and Queue are very much a dynamic duo when it comes to the world
of Comp Sci, so you’ll notice a lot of similarities between the two in the way they’re structured
and how they work. Today, and in this segment, we’ll cover a lot of the same topics as we did
with the Stack. Now, time stamps can be found in the description correlating to these topics, but
if you’re sticking around let’s dive deeper into the Queue.

Well the Queue, like the Stack, is a sequential access data structure, meaning we can only access
the information contained within it a certain way. Now if you remember back to Stacks, this
certain way was the LIFO methodology, or Last in First Out, where the last element pushed onto
the Stack would always be the first one we popped off, similar to a stack of books that we add to
and remove from. In contrast, the Queue follows what’s known as the FIFO methodology, or
First In First Out, where the first element added to the Queue will always be the first one to be
removed. We can think of this as a line to your favorite amusement park ride, mine was always
the Ring of Fire so let’s use that as an example. The first person to get there, assuming we don’t
have any cutters, will always be the first one who gets to go on the ride. The later you show up,
the longer you’ll have to wait. This is the strategy used for Queues when adding and removing
Objects. The first element to be added will also be the first one to be removed.

Another big difference between Stacks and Queues is the location we add and remove elements
from. You might remember that with the Stack, we added and removed elements from one spot,
the top. With the Queue; however, this is different. We add elements to the back, also known as
the tail, and we remove them from the front, also known as the head. This allows us to make sure
that we 100% follow the FIFO methodology.
So, there’s your background information on the Queue. Sequential Access, FIFO methodology,
add elements to the back and remove from the front. Got it. Good. Now, let’s dive headfirst into
how we can actually use a Queue by jumping into common Queue methods.

So just like the Stack, we’re going to have 2 methods used to add and remove elements from the
Queue. For the Stack, we added elements with Push, and removed them with pop. In contrast,
with the Queue, we add elements using enqueue and remove them using dequeue. In addition to
these 2 methods, we’re also going to cover peek and contains, which if you watched the previous
segment, should look pretty familiar. Alright, let’s start.

Enqueue is the first method, and the one we use to add elements to the tail of the Queue. It takes
in an Object to put at the end of the Queue, and simply adds that Object, while also increasing
the size of the Queue by 1. Let’s pull up an exampleQueue, which you can see currently has a
size of 0. But, say we call enqueue on a completely random String, let’s say “now”, the String
would be added to the tail, and the size would increase by 1. Now because there’s only one
element in the Queue at this point, the String “now” is acting as both the head and tail for us.
Let’s fix that by enqueuing a few more completely random Strings. If we add the string “video”
the size goes to 2. We can add “this”, and it goes to 3, “like” makes it 4, you get the idea. Now
we have a fully functional Queue. As you can see, “like” is the tail, being that it was the last
String added, and “now” is the head, which makes sense considering it was the first one added.
Pretty neat.

Moving on, dequeue is the method we use to remove elements from the head of our Queue. It
takes in no arguments, and will return the element that was removed from the Queue back to the
user. So, if we ran a Dequeue command on our example Queue, you’d see that “now” is both
returned back to the user and also removed from the Queue. Additionally, the size of our Queue
has been dynamically decreased by 1. If we run it again, “video” is returned and removed from
the Queue, and the size goes down by 1 yet again. You get the idea. We can keep doing this for
every element in the Queue until it’s empty, but the next methods we’re going to talk about need
some information to work with, so for now refill the Queue back to its original 4 elements.

The next method I’ll discuss is peek. Now we’ve actually covered peek just a few moments ago
in our segment on Stacks, but if you forget or didn’t watch that particular part, peek returns the
Object that’s at the forefront of our Queue. It doesn’t take in any argument and simply returns
the foremost object of the Queue without actually removing it - the keyword there being without.
This method allows you to look at the head of the Queue before you actually dequeue it. There
are a multitude of reasons you might want to do this, maybe to make sure the element you’re
about to dequeue is the correct one, or to check to see if an element you need is still there, etc
etc. Whatever the case is, we can use the peek method to fulfil our need. If we were to run it on
our exampleQueue, you’d see that the String “now” is returned. But, if we dequeue the first 2
elements and run it again, you’ll see that the String “this” is returned. Pretty simple, but
extremely effective. Again let’s add “video” and “now” back into the Queue for our next and
final method.

That method being the contains method. The name pretty much says it all, the contains method
will take in an Object and will return a boolean of whether or not the Queue contains that Object.
Running it on our exampleQueue with an argument of “Queue” would return false, because as
you can tell there is no “Queue” String contained within our Queue. However, if we ran it on a
String such as “video” it would return true because as you can see, the String “video” is in the
Queue.

And there they are all together now, Enqueue, Dequeue, Peek, and Contains. 4 methods which
will help you utilize a Queue to it’s maximum efficiency. Speaking of efficiency, that takes us
perfectly into the Time Complexity equations for the Queue.

Accessing an element within a Queue is going to be O(n). Let’s say you had some Queue full of
3 elements. If you want the Object at the tail, you first have to dequeue every element off the
front until the one you are looking for is the head of the Queue. Then and only then can you
actually get the value contained. Since this may require you to go through the entire Queue of
size n, accessing is O(n). Remember, Queues are sequential access data structures and not
random access, meaning we can’t just grab an Object from the middle of a Queue, that’s just not
how things work.

Searching is gonna be O(n) for the same reason. Trying to find an element contained at the tail of
a Queue requires you to iterate across that entirety of that Queue to check for it. So in that
scenario we have to check every element within that Queue of size n, making the Time
Complexity O(n).

Inserting and deleting to and from a Queue are both going to be an instantaneous O(1). This is
because just like with the stack, we’re only ever enqueueing at a single point, and we’re only
ever dequeuing at a single point. This means that it’s never going to matter how large the size of
the Queue is. It will be the same number of operations for any magnitude.

And there they are in all their glory, the Time Complexity Equations for the Queue. You may
notice that they’re identical to the Stack, which if you’ve been paying attention is the truth for
most properties of a Queue. They are very much a Yin and Yang one and the same type deal,
differing slightly in terms of the methodology you use to add and remove Objects from them.
You’ll often see these 2 Data Structures talked about together frequently just because of how
similar their functionality is.
The final thing I want to cover on the topic of Queues are just some common uses for them
within programming, what are these things actually used for. And the answer is quite honestly a
lot. On your computer right now, queues are being used for what’s known as Job Scheduling, the
process through which the computer determines which tasks to complete for the user and when,
like opening up a web page or a computer program. It’s used many times in printers to keep track
of when multiple users try to print, and determining who’s documents get printed first. Heck, if
you’re looking for real-world uses, Google uses Queues in their new Pixel phones to enable
what’s known as Zero Shutter Lag, in which they strategically use Queue to eliminate the time
between when you take the picture, and what the phone actually captures. So yeah, in terms of
functionality? The Queue can be used in a variety of fields, so it’s good now that you know what
they are, and how to use them.

This also concludes our discussion on the Queue. To review, the Queue is a sequential access
data structure which follows the FIFO principle, or First in First Out, to add elements to the back
and remove elements from the front. Up next, we’ll continue on the sequential access train and
talk about one of my personal favorite data structures.

That’s right, next we’ll be covering the Linked List, and it’s a good one so strap onto your seats.
Let’s just jump into things by talking about the elephant in the room, what exactly is a
LinkedList?

Well, to answer that, a Linked List is a sequential access linear data structure in which every
element is a separate object called a node. Each Node has 2 parts, the data, and the reference,
also called the pointer which points to the next node in the list.

Wow, that’s a pretty big sentence with lots of ideas within it so let’s break it down part by part.

The sequential access part of that statement means that the information contained within the
LinkedList data structure can only be obtained through a certain methodology. We talked about
this in our segment on the Stack. If you remember, during that segment we compared them to a
Tape Measure because similarly, you can only get measurements from a Tape Measure through a
specific method.

The linear part of the definition simply means that the information, or data, is organized in a
linear order in which elements are linked one after the other.

Now when we state that every element is a separate object called a node, this means that unlike
an array or an arrayList where every element is just, let’s say a number, each element in a
LinkedList will be an object which can have multiple attributes. Now, I won’t dive too far into
Object Oriented Programming right now, if you want a good introduction to that you can check
out the link in the description below which will take you to our Introduction to Object-Oriented
Programming lecture. For this series, we essentially mean that the Objects or Nodes stored
within each element of our LinkedList will hold 2 separate pieces of information. These 2 pieces
of information come together to form the node, and these nodes are what make up our
LinkedList. More specifically, those 2 pieces of information are the actual data or information
stored within that node, and the reference or pointer to the next Node in the LinkedList. The data
is where our Strings or Integers or Boolean values are kept, the actual contents of our data
structure. The other piece to the puzzle is the pointer. This reference points to where the next
Node in the LinkedList is stored in memory. This helps “link” all the Nodes in a linkedList
together to form one long chain of information. Kind of like a Computer Science conga line.

You should now be able to understand what a LinkedList is. Again, it’s a sequential access linear
data structure in which every element is a separate object called a node, in which each node
contains both data and a reference to the next Node in the linkedList. This cumulatively creates a
string of Nodes which we call the LinkedList. Feel free to rewatch this explanation if you are still
confused cause it can get pretty hectic pretty fast. The next thing I want to do is just visualize
how one of these LinkedLists is set up. That way you can actually see what I mean when I say
Node’s, pointers, etc.

So every LinkedList starts with what’s known as the head Node of the List. This is just an
arbitrary label which simply represents a Node containing some form of data, and also a
reference to the next node in the LinkedList. For now, let’s just store the Integer 1 in our head
Node. Now since this is the only node so far in our LinkedList, the head node will simply point
towards a null value, which is just to say it’s not pointing anywhere. Essentially, it’s pointing to
nowhere, and just being used as a placeholder until we give it something to point towards. Let’s
do that by adding another Node to the LinkedList, and store the Integer 2 inside of it. Instantly,
you can see that now our head Node points to this second Node instead of a null value. You’ll
also notice that this new Node, which I'll call the 2 Node, points to a null reference value just like
the 1 Node used to, as it’s the last Node in the LinkedList. This is a pattern you’ll notice as we
continue adding Nodes. The last Node in a LinkedList, also known as the tail node will always
point towards a null value. This is our way of telling the computer that we’ve reached the end of
our LinkedList, and that there are no more Nodes to traverse towards. Anyways, let’s now create
our third Node, and inside store the integer 3. This node now becomes the tail and points towards
a null value, and the second node we created, the one that used to point to a null value, now
points to this new Node which contains the integer 3. Essentially, we’re just adding a Node to the
tail of the LinkedList, and then setting the reference of the previous Node to point towards that
new Node. If you can understand that concept, you pretty much comprehend the gist of how
these Nodes interact. The two main takeaways are that every Node has both information and a
pointer, and that the last Node in the LinkedList points towards a null value. That’s pretty much
the setup for a LinkedList. Definitely more abstract and fluid than say an array or arrayList, but
hopefully not too complicated.
Next up we’re going to be discussing how we add and remove these Nodes from a LinkedList.
Unfortunately for us, adding and removing elements from a LinkedList isn’t going to be as
simple as it was with the other Data Structures such as the Stack or the Queue. This has to do
with the simple fact that we are actually able to insert and remove elements easily within a
LinkedList at any location. With a Stack or a Queue, we weren’t able to do this because the data
could only flow in and out of a few specified points. This made it so we couldn’t remove an
element from the middle of a Stack, or jam an element in the center of a Queue. The way that
they’re structured just doesn't allow for this. Using LinkedLists; however, we can easily remove
elements from the middle, or jam elements in the center, and so today we’ll be covering the
different methods used to do so. Specifically, we’ll be covering the 3 different ways to both
insert and remove Nodes from a LinkedList. Adding to and removing a Node from the head, the
middle, and the tail of a LinkedList. These are all going to revolve around those pointer’s we
talked about at the beginning of the episode, because whenever we change up a Node in a
LinkedList, we also have to change its pointer, and that can get pretty complicated. Luckily,
that’s why I’m here. I’ve set up a basic LinkedList on your screen with 3 Nodes we can use to
play around with. Each Node has a value of course, representing the data inside the Node, and
also a pointer which points it to the next Node. The green and red coloring on the Nodes with
integers 1 and 3 simply indicate which Node is the head Node and which is the tail. Green means
Head Node and Red means it’s the Tail Node. Now in an actual LinkedList, these pointers would
be locations in memory, but for the purpose of this series, we’ll be representing the pointers
visually. Perfect, so let’s cut the chit-chat and jump right into it.

The first method we’re going to be covering is adding and removing Nodes from the head of a
LinkedList. Lucky for us this is pretty simple.

To add a new Node the head of a LinkedList, literally all we have to do is make that new Node’s
pointer point to whatever the current head Node of the LinkedList is. By doing this we simply
take away the title of Head Node from the current head and bestow it upon this new Node that
we’re adding. We don’t even have to move anything else around or rearrange data. It looks a
little something like this. Let’s take a Node with the Integer 0 and add it to the Head of the
linkedList. All we do is set it’s pointer point towards the current Head Node. Now, you’ll see
that none of the other nodes have changed in the slightest, the only thing that has changed is that
this new Node with Integer 0 is now the head Node, and it points towards the old Head Node.
Extremely simple, and the best part is that it works in reverse as well.

Removing a Node from the head of a linkedList is just as simple. All we have to do is set the
Head Node’s pointer to some null value. Once we do, the head Node will get cut off from the
flow of information, essentially removed from the linkedList. If we did this on our example
LinkedList, you’d see that once we set the 0 Node’s pointer to null. Now, because it no longer
points to the 1 Node, and no node points to its location, it has been cut off from the rest of the
Nodes, and exiled from the LinkedList. The old Head node regains its position, and it’s as if the
Integer 0 Node never even existed.

Moving on, the next methods we’re going to cover are inserting and deleting a Node from the
middle of the LinkedList. These 2 methods are definitely the most difficult of the bunch. This is
because we need to insert the Node in such a way that the pointers get readjusted accordingly
without losing any of the information. If we accidentally set the pointers wrong, or do things out
of order, the data could get lost forever, but luckily I’m here to teach you how to do it right.
We’ll start with adding a Node to the middle of a LinkedList.

Adding to the middle of a linkedList is a 2 step process. We first make the pointer of the new
Node point to the Node after the location we want to insert at. Then, we set the Node before the
location we want to insert at to point towards the new Node. So if we wanted to insert a Node
with the double value 1.5 after the Node containing integer 1, but before the Node containing
integer 2, what we would first do is set the new Nodes pointer to point to the Node containing the
Integer 2. Then, we would make the Node containing the Integer 1 to point towards our new
Node, which contains the Double 1.5. By adjusting the pointers of the Node’s before and after
the location we want to insert at, we can strategically jam this Node in the correct place without
moving or modifying the entire list. As you can see now, we have a LinkedList of length 4 where
the link has not been broken and is also still continuous.

Removing a Node from the middle of a LinkedList is even simpler. All we have to do is make
the pointer of the Node previous to the one we’re removing, now point to the Node after the one
we’re removing. Then, if we set the pointer of the Node we want to remove equal to a null value,
we again cut the Node off from the LinkedList and it is removed, simple as that.

So, following these steps, if we now wanted to delete our 1.5 Node. We’d make the 1 Node again
point towards the 2 Node instead of the 1.5 Node. Then, if we delete the pointer of the 2 Node by
setting it equal to null, it gets cut off from the flow of the LinkedList. No changes externally to
the rest of the List. Just one, lonely Node removed from the List.

The final type of insertion and deletion we’ll be covering is inserting to and deleting from the
end of a LinkedList. Doing this simply requires you to modify the tail Node of the LinkedList,
the one which currently points towards some null value.

For adding a Node to the tail, you simply make the current tails pointer, which is currently set to
null, point towards the Node you want to add. So if we wanted to add a Node with the Integer 4,
we would just make the 3 Node point towards this new Node. Then, by setting the 4 Node’s tail
to point towards Null, we’ve made this new 4 Node the tail of the LinkedList, and the old tail
Node, with Integer 3, now points to the new tail.
Removing the tail of a LinkedList is just as simple. If we want to remove the tail, we just set the
previous tail to point towards a null value instead of the current tail. This leaves the tail Node
with no connection to the LinkedList, isolating it from the pack.

Doing this on our list would look like making the 3 node point towards Null. Because no Node
now points to our tail node containing the Integer 4 anymore, it gets cut off from the LinkedList
and essentially removed, making the old tail Node, the one containing 3, the current tail Node
once again.

And so, after a bit of pointer readjustment, hopefully now you can understand the Pseudocode
behind inserting and removing elements from a LinkedList with ease.

Up next are the Time Complexity equations for a LinkedList.

Now accessing an element within a LinkedList is going to be O(n). This is because LinkedLists
again are sequential access data structures. This should be all too familiar to you if you watched
the sections on Stacks and Queues, but if not just as a simple review. Sequential Access data
structures can only be accessed through a particular way, meaning we can’t get any element
within instantaneously. Resulting from this is the stipulation that if we want to get a certain
element within a LinkedList, we need to start at the beginning of the list, and cycle through every
Node contained within it before we can access the one that we want.. We do this by using the
pointers as a little map for the computer. First go to Node 1, Node 1’s pointer gives you the
location of Node 2 in memory, and Node 2’s pointer will take you to the memory location which
contains the Node with the information you want. It’s like a computer science treasure map in a
way. For a LinkedList of size n, this means you could have to traverse the entire LinkedList
before finding your information, making it’s Time Complexity equation O(n).

Searching is O(n) for the same reason. We check a Node, and if it’s not the one we want, we use
that Nodes pointer to take us to the next Node and check that one. We do this for every Node
until we either find the Node containing the value we want, or get to a Node which points to null,
indicating we’ve reached the end of the LinkedList, and the value we are searching for isn’t
contained within that particular LinkedList.

Inserting and deleting from a LinkedList is a little bit complicated. Since LinkedLists usually
only store the head and sometimes the tail node’s location in memory permanently, if we want to
insert or delete an element at the beginning or the end of a LinkedList, we can do so
instantaneously using the methodology we talked about previously. However; if we want to
insert a Node within the LinkedList, things become a little more complicated. In order to insert a
Node within the LinkedList, we must first traverse to the location we want to insert at. This
means following that treasure map until we reach the insertion point, and then and only then, can
we actually follow the instructions to insert or delete a Node. So, depending on how and where
you want to insert or delete a Node at, it’s Time Complexity equation will be either O(n), or
O(1). A little confusing yes, but necessary to mention for when it inevitably comes up.

So, in review. Accessing, searching, and sometimes inserting and deleting are all going to be
O(n), and other times inserting and deleting will be instantaneous. Cool. Now, let’s finish up by
talking about some real-world applications of LinkedLists.

Something I haven’t mentioned before but would be scolded by people in the comments for not
mentioning is that LinkedLists can actually be used in the backing of other data structures. What
do I mean by this? Well, basically we can actually use LinkedLists to make Stacks, Queues, and
some other data structures we haven’t talked about. This is in the same vein as earlier when I
mentioned the arrayList uses the array as a backing support system in memory. Now this goes a
little above an introductory series, but it’s one of, if not the most important use of a LinkedList in
Computer Science so I thought I’d mention it here. Basically, because of the way it’s structured,
having a Stack or a Queue use the Nodal methodology we talked about during this episode that
comes with the LinkedList to be the backend of it’s structure makes a lot of sense. If you’re
curious, I’ll have an article linked below explaining this idea in better detail, I just thought it was
a bit above the complexity of an introductory course.

Now that’s not to say LinkedLists can’t be useful in other ways. A queue on Spotify, for
instance, where each song in the queue doesn’t contain just an Integer or a String, but an entire
song with .wav data, a title, a length, etc. Then, when the track is done, it automatically points to
the next song in the playlist. Another example could be a photo viewing software, where each
Node is an image, and the pointers simply point to the next photo in the List.

Like I said the main functionality of a LinkedList might be to back other data structures, but it
also has its uses in other areas of expertise.

In review, a LinkedList is a sequential access linear data structure in which every element is a
separate object called a node, containing 2 parts: the data, and the reference pointer which points
to the next Node that comes in the LinkedList.

These pointers allow us to easily shift each Node around, adding or removing it, without having
to move mass amounts of data like we would have to do in the case of an Array or some other
data structure.

One thing I haven’t mentioned yet about LinkedLists is that this pointer system does come with
one big drawback. With a normal LinkedList, we can only go forward with our pointers, never
backwards. From the computer’s eyes, once we follow a pointer to a certain Node’s location in
memory, there’s no way to undo or go back to the previous one. Much like a Shark, we can and
will only ever go forward. This problem is fixed; however, through the use of a data structure
known as the doubly-linked List, which coincidentally is the subject of the next segment of this
video. Small World. All jokes aside next up we’re going to be covering the doubly-linked List,
so strap into your seats.

A doubly-linked Linked List is almost exactly the same as a LinkedList. That is to say it’s a
sequential access data structure which stores data in the form of Nodes, except doubly-
linkedLists have one small difference. With a doubly-linked List we’re able to traverse both
forwards to the next Node in the List, and backwards to the previousNode in our list, again using
pointers. How? Well let me explain.

With regular LinkedLists, each element was a Node composed of both a data section, and then a
pointer, which would take you to the memory location of the next Node in the list. Using this, we
were able to traverse a LinkedList easily to search for information, access data within a
LinkedList, or add and remove Nodes from within the list with ease. A doubly-linked list simply
builds upon this by also having a pointer which points to the previous Node’s location in
memory. It’s an undo button of sorts which allows you to fluidly go through the LinkedList in
either direction, instead of just limiting yourself to going one direction. This is great, since it
allows us to jump around the LinkedList and have a bit more flexibility when it comes to
modifying information.

Now because this can get very confusing very quickly, I want to do a visualization. Before that
though, let’s use some lingo to help consolidate the terminology I’ll be using. When I refer to a
Node’s “next”, I’m referring to that particular Node’s pointer which points to the next object in
the List, whether that be another Node or a null value. Similarly, when I refer to a Node’s
“previous'', abbreviated to “prev” on the visuals, I’m talking about it’s pointer which points to
the previous object in the LinkedList, again either another Node or a null value. Doing this just
helps keep things simple, because as you’ll see, having both a previous and a next pointer makes
things a little bit more complicated.

Now onto what these doublyLinked Lists look like. Just like with a regular LinkedList, every
doubly-Linked List is going to start with a head Node. Since it’s the first Node in the List, both
it’s previous pointer and it’s next pointer will point towards a null value. This is of course
because it can’t point to information which isn’t there. We can fix this though, by adding another
Node. Once we do, you can see that a few things have changed. The head Node’s next pointer
now points towards this new Node instead of null. The new Node’s previous pointer now points
to the Head Node. And the new Node’s next pointer now points towards a null value. As you can
see, a bit more complicated than adding a Node to a regular LinkedList in which we only had to
worry about 1 set of pointers, as opposed to 2, but still manageable. Let’s add one more Node for
demonstration purposes and you’ll see the same thing happens. The 2nd Node now points to this
new third Node, and this third Node gets initialized with a pointer which points both to the
previous second Node, and also forward to some Null value.
You can see that with any 2 Nodes, the next pointer of the first and the previous pointer of the
second come together to form a sort of a cyclical connection which ties the 2 Nodes together.
Then, at the head and tail of each list, there’s a connection which ties them both towards some
null value. Most of this should be common sense but it’s still good to go over. DoublyLinked
Lists are a little scary at first, but once you break them down, you realize they’re just an evolved
form of LinkedLists.

Up next, we’re going to talk about adding and removing Nodes from a Doubly-LinkedList, again
using the 3 different methods we talked about in our previous segment, adding and removing
from the head, middle, and tail. I’ve also set up a basic doubly-LinkedList with 3 Nodes
containing 3 Strings to help us out with this next segment. Finally, just like last episode, the
green on top of a Node signifies it’s the head Node, and the red signifies it’s the tail Node.
Alright, with all that being said, let’s get into it.

Now adding to the head of a doubly-LinkedList is quite simple. The first step is to take our new
Node that we want to insert, and set it’s previous to null. Second, we set the new Nodes next to
point towards the current head of the linkedList. Then, all we do is set the current head’s
previous to point towards this new node instead of a null value, and we’re set to go. Doing this
rearranges the pointers in such a way that the new node we want to add becomes the head of the
doubly-LinkedList. Pretty simple. So, if we wanted a new Node with the String “Abe” to be the
head of our doubly-LinkedList, we would just follow the steps. First, we set the Abe Node’s next
to point towards the “Adam” Node. Then we set it’s previous to point towards a null value. And
finally, by setting the “Adam” Node’s previous to point towards the “Abe” Node, we have
successfully added that Node to the head of the list, making it the head Node.

Removing a Node from the head is even simpler. We first set the head Nodes next to point
towards a null value. And then, by setting the second Node’s previous to also point towards a
null value, we can easily remove the head Node from the List. This is how it would look in our
example. We’d first set the “Abe” node’s next to null. Then, we would set the “Adam” node’s
previous to also be null. And we’re done. Now, because the “Abe” Node doesn’t have anything
to point towards, nor does it have anything pointing towards it, it will be deleted from the List.
The “Adam” Node will regain its position as the head Node, and the program will carry on.

Up next is inserting and deleting from the middle of a doubly-LinkedList. This is where things
get really tricky so make sure you’re paying attention.

To insert a Node into the middle of a doubly-LinkedList, we follow a 3-step process. The first
step is to take the Node we want to add, and set it’s previous to point towards the Node previous
to the position you want to insert at. Then, you take that same Node, the one you want to add,
and set its next to point towards the Node after the position you want to insert at. Finally, you set
the next on the Node at the position before the one you’re inserting, and the previous on the
Node at the position after the one you’re inserting, to both point towards this new Node. That’s a
lot of words, some of which might not even make sense to you, so let’s open up the space on our
screen real quick and do an example with our list.

Let’s take a new Node with the String “Chris”, and insert it in between the “Adam” node and the
“Ben” node by simply following the steps. First we want to set the new Node’s previous to point
towards the Node previous to the position we want to insert at. This means setting the “Chris”
node’s previous to point towards the “Adam” Node, simple enough. Second, we set the new
Node’s next to point towards the Node after the position we want to insert at. This entails setting
the “Chris” Node’s next to point towards “Ben”. So far so good. Then the last step is to set the
next on the Node before we’re inserting, and the previous on the Node after we’re inserting to
both point towards the new Node. So in this case, the “Adam” Node’s next, and the “Ben”
node’s previous both get set to the new “Chris” node. This completes the addition into the List.

Obviously a little more complicated than inserting or deleting from the head, but you can see that
we’ve now inserted the new Node in its rightful place. It may be hard to see since the pointers
are a little messy, but the flow of the List has remained constant without any breaks in the
pointers, which is the most important part.

Removing a Node from the middle of a doublyLinked-List is also a 3 step process. First, we set
the Node before the one we want to removes next to point towards the Node after the one we
want to remove. Then, we set the Node after the one we want to remove’s previous to point
towards the Node before the one we want to remove. The final step is to set both pointers of the
Node we want to remove to point towards a null value. Again, a little complicated so let’s take it
to our example List and try it out.

Let’s delete the “Chris” Node just for the sake of keeping things consistent. Following the steps
we’ve laid out, we would first set the “next” of the Node before the one we want to remove to
point towards the Node after the one we want to remove. So in this case, we set the “Adam”
node’s next to point towards the “Ben” Node, essentially skipping over the Chris Node. Then we
set the “previous” of the Node after the one we want to remove to point towards the Node before
the one we want to remove. So, we set the “Ben” node to point towards the “Adam” Node, again
skipping over the Chris Node. Finally, we have to set the “Chris” node’s pointers to point
towards null values. Now that we’ve done this, because no Node’s point towards it, and it
doesn’t point towards any Node’s, the “Chris” node gets deleted from the doubly-LinkedList.
The list is back to its original form with no breaks or errors.

Adding a Node to the tail of a doubly-LinkedList is also a 3 step process. Step 1 entails setting
the next pointer of the current tail to point towards the new Node we want to become the tail.
Step 2 is setting the previous of the new Node that we’re adding to point towards the current tail
of the List. And step 3 is making the new Node’s next point towards a null value. Again, let’s do
an example, where we add a new Node containing the String “Ethan” to the tail of the
doublyLinked List. Following our steps, we first set the next pointer of the current tail, the
“Carl” Node, to point towards the “Ethan” Node. Then, we make the “Ethan” node’s previous
point towards “Carl”, and it’s “next” to point towards a null value and we’re all set. “Ethan” has
successfully been added as the new tail of the list in 3 quick and easy steps.

Removing from the tail of a List is even easier, and only requires 2 steps. We first set the tail
Node’s previous to point towards null. And then we set the second to last Nodes next to also
point towards null.

On our example list it looks like this. We first set the “Ethan” Nodes previous to point towards
null. Then we set the “Carl” node’s next to also point towards null. This isolates any pointers
going to or from the “Ethan” Node, and deletes it from the list, making the “Carl” Node the tail
once again.

Adding and removing from a doubly-linkedList might sound like a hassle. But remember, we
only have to use this pseudocode to program these functions once in whatever language we’re
implementing them in, and then we’re able to use them an infinite amount of times.

Now for Time Complexity equations. Since the doublyLinked-List is really just an extension of
the LinkedList, it’s Time Complexity equations are going to be exactly the same as they were for
the LinkedList, and for the same exact reasons. O(n) for all 4 cases, and sometimes O(1) for both
inserting and deleting. If you didn’t watch the LinkedList segment and are wondering how we
arrived at these equations, you can check the description for a TimeStamp which will take you to
the discussion we had on LinkedList Time Complexities.

Finally in terms of doublyLinkedLists, I just want to talk about some uses of a doubly-
LinkedList, because there are a ton. The back and forth functionality of a doubly-linkedList lends
itself to be implemented in a lot of Stack-like functionality. IE cases where you want to go back
and forth between information that we’re storing for the user.

Some examples of this could be the browser caches which allows you to go back and forth
between web pages, the undo-redo functionality in many programs, applications which allow you
to utilize an open recent functionality, the list goes on and on. Basically any case in which you
need to store a list of Objects with multiple attributes, a doubly-LinkedList is going to be a safe
bet to get it done.

Linked Lists and their evolved form in the doubly-LinkedList are a great way to store
information because of the adaptability of the nodal structure. Since we’re not working with raw
information like primitive types, and we’re storing all information inside of a shell, it makes it a
lot easier to move around information. This combined with the pointer system allows for non-
contiguous but still fast operating speed, making these 2 data structures a staple in Computer
Science.

Up next we’ll be covering dictionaries and with that a mini-lesson on Hash Tables. This is the
last of what I referred to as the Intermediate data structures at the beginning, and is honestly
personally one of my favorites, so let’s not wait any longer and just jump into things.

Before we get too far though, we should probably clarify that when we say dictionary, we’re not
referencing that THICK book you probably have lying around your house and haven’t used in
years. Actually, dictionaries are one of the most abstract data structures which exist in Computer
Science, and can be used for a variety of purposes. Another thing I’ll clarify real quick is that
dictionaries are also sometimes called both Maps and associative arrays by the Computer
Science community. This more-so has to do with language specifics and preferences and not any
functional differences, since all of them work in almost identical ways, but for this series we’re
going to be referring to them as dictionaries just to keep things simple and organized.

Now, a dictionary in computer science, by definition, is an abstract data structure which stores
data in the form of key/value pairs. This means that we have 2 main parts to each dictionary
element, the key and the value. Each value within a dictionary has a special key associated with
it, and together they create a pair which is then stored in the dictionary data structure as an
element. Think of a key/value pair like a social security number. Each social security number is a
“key” which is then paired with a “value”. That value being an individual person. These social
security number key/value pairs then come together to form a dictionary of every human living
in the United States.

This is very different from many of the data structures we’ve talked about previously because we
index dictionaries using these keys instead of a numerical index. For example, with an array, we
would index each element within the data structure according to a numerical value which started
at 0 and ran the length of the array. With a dictionary; however, we index each element by using
it’s key, instead of some arbitrary integer, and obtain information through that index instead.

So what exactly are these key/value pairs going to look like? Well, they can be just about
anything. The key’s in a key/value pair can be any primitive data type that you can think of. We
can have a dictionary which has integers as the keys, one with Strings as the keys, one with
doubles as the keys, there’s really a lot of flexibility. As for the values? Well we have even more
flexibility with those. We can have keys in a dictionary correspond to pretty much anything.
Strings? Sure, Booleans? of course. Another dictionary with it’s own set of key/value pairs? You
can do that too. This allows us to have tons of combinations in the way we store our data,
making dictionaries extremely powerful.
As powerful as they are though, there are 2 extremely important restrictions that we have to
cover when it comes to dictionaries. And they are as follows. Every key can only appear once
within the dictionary, and each key can only have one value. Let’s start with the first one.

Each key has to be unique and cannot be replicated, duplicated, cloned, copied, or anything else
that would cause there to be 2 keys of the same value in a dictionary. Think of it like this, when
you create a key/value pair, the computer creates a little locked box in memory to store the value.
Then, the computer spends days and weeks creating a customized hand-crafted, one of a kind
key that corresponds directly to that locked box. This key cannot be replicated in the slightest,
and that’s the mindset you should use when working with dictionaries. NO 2 KEYS CAN BE
THE SAME. If you were to try and make a dictionary with 2 similar keys, you would be thrown
an error by the computer.

The second stipulation is that each key can only have one value. Think back to our custom key
example. It wouldn’t make sense for this one-of-a-kind key to be able to open multiple boxes.
The same is true for our keys in computer science, they can only correspond to one value.

Now one rule that we don’t have to follow is that there can be duplicates of values within a
dictionary, meaning we can have two separate keys both point towards equivalent values. As
long as the keys are different, the computer does not care.

Alright now that we know what dictionaries are and how they work. Let’s jump into the Time
Complexity equations for a dictionary. Or at least try to. Let me explain.

Now for a dictionary, the Time Complexity equations are a little bit funky. Previously we talked
about LinkedLists, and how they are sometimes used as the “Backing” of other data structures in
memory. For example, a Stack might implement the LinkedList structure as it’s way of storing
information. Well, the most common way to initialize a dictionary is to implement it with what’s
known as a hash table. Hash tables are a little more advanced than the content I wanted to cover
in this series, but they are a huge part of dictionaries, especially when it comes to Time
Complexity, so we’re going to give a little mini-lesson on them today. Doing this will help you
better understand the Time Complexity equations for a dictionary, as well as give you some
background on an advanced Data Structure. Alright let’s begin with a thought experiment.

Imagine this scenario for a second. Say we have a dictionary, which contains a few key/value
pairs like shown on your screen now. Let’s just assume that to store all this information in the
computer’s memory, we’d use an array like how it is shown on your screen now, where the
position that a cell is in the table, also corresponds to the key in a key/value pair stored in our
dictionary. So the 0th cell would contain a key with the integer 0 that leads to a value, the 4th
cell would contain a key with the integer 4 which leads to a value, and so on. Any cell which we
don’t have a key for is empty, or what computer scientists refer to as “nil”. Why doesn’t this
work? Why can’t we just use an array like this to back our dictionary in memory, since it looks
like actions such as accessing and searching would run in constant time since all the keys
correspond to their table values in memory? All we’d have to do to get a value is reference the
key at the index we’re looking for.

Well this actually isn’t the case, because it’s based upon the simple assumption that the size of
the array is not too large. Sure, this might work great for this dictionary where we have 10 or so
elements. But let’s say instead of 10 elements evenly spaced out, we want to have one which has
10 values which are sporadically placed from 1 to a billion. We want to keep the keys as being in
the same index position as their values so we can know exactly where they are and reference
them easily, but by my count, that is 999,999,990 nil values just taking up space in memory, and
that is not good whatsoever. There’s gotta be a better way to do things right?

This is where Hash tables come in. Hash tables are fundamentally a way to store this information
in such a way that we’re able to cut down the amount of nil values, while also allowing for the
information to be stored in a way that is easily accessible. Basically, using a hash table, we’d be
able to store those 10 keys in our dictionary, which range from 1 to a billion, in a table with only
10 elements while also being able to keep constant time. How is this possible? Well, through the
use of a trick programmers use known as a hash function.

A hash function will take all the keys for a given dictionary and strategically map them to certain
index locations in an array so that they can eventually be retrieved easily. Essentially, by giving a
hash function both a key and a table, it can determine what index location to store that key at for
easy retrieval later. These hash functions can be pretty much anything which takes in a value,
and returns an index location. The goal of a good hashing function is to take in a key, whatever
that may be, and reliably place it somewhere in the table so that it can be accessed later by the
computer.

So, with that being said, let’s jump into an example, because I know a lot of that might be
confusing. Let’s say we have our dictionary, which contains keys in the form of integers from 1
to a million by a factor of 10. So the keys are 1, 10, 100, 1000, 10,000, 100,000 and so on. A
good hash function for this would be to take the key, divide it by itself, then multiply the result
by the number of digits in the key minus 1.

So, to find out where to store the value corresponding to the 1,000,000 key, we would take a
million, divide it by itself, which yields 1, and then multiply that by the number of digits in that
integer minus 1, in this case (7-1) or 6. That means we’d store the 1 Million key at index location
6. If we do this for every key in the dictionary, we can see that each key in the key/value pair is
stored at some index from 0-9. We have consolidated the 10 keys from 1 to a billion into 10
index slots instead of 1 billion, a pretty good improvement. You might think this is a little
overcomplicated, but remember sometimes we might be working with thousands of keys at a
time, ranging in the billions, or even Strings as keys which is a whole other story.

Now the best part about these hash functions? Let’s say that now we want to get the value which
is paired with the 1 million key. All we need to do is put 1 million back into our hash function,
and it’ll tell us where the value tied to that key is stored within the table, in this case at the 6th
index. Doing this allows us to pretty much instantaneously find where any key/value pair is
stored within the table.

All this information may be pretty confusing. If you still aren’t 100% I would recommend
watching this part over again. However, in layman's terms, and for this series. All I want you to
be able to comprehend is that dictionaries are built upon these hash tables, and the key’s in our
key/value pairs are stored in these hash tables at indexes which are determined by a hash
function. And if you can understand that, you’ve got a pretty good base for understanding hash
tables.

Now the final thing I want to talk about in regards to hash tables, and the reason dictionary Time
Complexity equations are so funky has to do with one small problem. What happens when we
run two different dictionary keys into a hash function, and the computer tells us to store them at
the same index location?

For example, let’s say we have two dictionary keys with the Strings “Steven” and “Sean”, and
when we run both of them through our hash function, the Computer instructs us to store both of
them at the 9th index location. This should be impossible, how are we supposed to put both of
them at index location 9? This little problem is what’s known as a “Hash Collision” and can be
solved one of two ways, Open Addressing or Closed Addressing.

With open addressing we just put the key in some other index location, separate from the one
returned to us by the hash function. This is usually done by looking for the next nil value in the
table, i.e. the closest location which contains no key. So with our example, the key “Steven”
would get hashed to the index location 9, mostly because it’s a better name, and the inferior key
“Sean” would get stored at index location 10, because it’s the closest open location available.
This does make it harder to interact with the data in our dictionary later on, and can lead to
problems if we eventually have a value which hashes to the index location 10, which is why
Computer Scientists developed Closed Addressing.

Closed addressing uses linkedLists to chain together keys that result in the same hash value. So,
the key’s “Steven” and “Sean” would get stored together in a linkedList at index location 9. The
main drawback to this is that whenever we want to interact with the values stored in the
key/value pair for either “Steven” or “Sean”, we end up having to look through the linkedList for
the piece of data that we want. If there are 200 keys hashed to one index, that’s 200 keys we
might have to search through to find the one we want, and that’s not good.

And with that concludes our mini-lesson on hash tables. Now, we can get back into dictionaries
and finally talk about their Time Complexity equations.

Now remember back to the segment on Time Complexity really quickly. Back then, I mentioned
that for these Time Complexity equations, we generally measure a data structure based on it’s
worst-case scenario, but, when it comes to dictionaries, if we are to assume the worst-case
scenario, things get a little outrageous. We basically end up assuming that our hash function
makes it so that every key/value pair ends up in the same index, meaning each of our keys gets
stored in a linkedList, assuming closed addressing is used. Then, worst-case scenario, we have to
assume that every operation functions how it would for accessing, searching for, inserting, or
deleting from a linkedList, which if you remember, is O(n) for all 4. This, of course, is
preposterous, and would probably never happen with the most bare minimum decent hash
function, which is why in addition to the worst-case scenario Time Complexity equations, I’ll
also go over the average Time Complexity for each of these 4 operations.

Now lucky for us, they’re all going to be O(1). This has to do with those hash functions we
talked about before. To access, search for, insert, or delete a key/value pair from our dictionary,
all we need to do is run that key through our hash function and it’ll tell us what index in the hash
table to go to in order to perform that operation. There’s no time wasted looking through pre-set
indexes because we the programmer generate the indexes ourselves, and that is the power of the
hash table in the flesh.

Overall, dictionaries are a very useful data structure when it comes to Computer Science for a
few reasons. They differ from the rest in quite a few big ways. The fact that they don’t use a
numerical index to retrieve values, rather a pre-set key. The notion that those the keys in the
key/value pairs can be a range of types from Strings, to integers, to chars and so on. The
implementation of the hash table which allows for super quick utilization that is sure to come in
handy in any program you write. In conclusion, the dictionary is just a solid data structure that
can be used to fill in the gaps in your data structures knowledge.

With that, we have now completed all of the Intermediate data structures if you will. Next, we’ll
be moving on to Trees and tree-based Data-Structures. This is the point in the series where we
will be dropping our discussion on Time Complexity equations, because if you thought
dictionary Time Complexity equations were complicated with Hash Tables and Hash Functions,
Trees, and their many variants only further complicate things to the point where I no longer feel
comfortable including in an introductory course. We might mention that certain tree-based data
structures are good for accessing or searching, but as for discussions on all 4 Time Complexity
equations, dictionaries are where we leave things. With that being said, let’s move on to the final
few data structures as we round out this Introduction to Data Structures series.

Next up on our list of Data Structures is the Tree, not those big green things that inhabit the
outside world and that you can climb on and get fruit from, rather the Computer Science data
structure, which is way more fun in my opinion.

Now, before getting into trees, we need to talk about data structures in general. Every data
structure we’ve covered up until this point has been stored linearly. Arrays, Stacks, LinkedLists.
All of these had a definitive start and end to their data, and you could point them out easily if
told to do so. Even the dictionary could be laid out in such a way to be represented linearly.
Everything was nice and neat and easy to visualize. On the other hand, Trees store data
hierarchically as opposed to linearly. What does that even mean? Well, besides being an
extremely hard word to pronounce, it’s an alternative way to store data that we’re going to be
using for the remainder of this series. It’s a little hard to visualize, so I’m going to start with an
example.

The most common real-world example of hierarchical data would be a family tree. Each person
would be an element of the family tree, and connections wouldn’t be based on a simple linear
fashion, rather, connections would be more abstract and could lead to multiple paths or branches
instead of just a single data point. Each generation is ordered on a hierarchy, which is where the
name comes from, and as you can see there’s no definitive end to the family tree. Sure, there are
the ones at the bottom, which you could argue are the ends of the family tree, but which one of
them is definitively the end? There is none. Another example could be a file structure on your
computer. You’d have a base folder such as your desktop, and then inside of that you might have
multiple other folders for different types of information, and then inside of those you might have
more folders representing more types of information. And then finally, you finally have your
documents. Again, it sets up a network of these files on different levels just like how we had with
the family tree. Trees, and the tree's many variants, are all dependent on storing data
hierarchically, which of course begs the question of what actually a tree is.

Well, a tree is an abstract data structure which contains a series of linked nodes connected
together to form a hierarchical representation of information. The actual information is stored in
these nodes and the collection of Nodes along with the connections between them is what’s
known as the tree. This is sort of like a linkedList, only instead of each node only pointing to one
location, it has the option of pointing towards multiple, and also branching off on it’s own
pointing to no other Nodes. Each of the Nodes in a tree can also be called vertices and the
connections between vertices, what links 2 Nodes together, are called edges. Now the free-
flowing structure of a tree lends itself to a lot of different configurations, and with that comes a
plethora of terminology about certain nodes, vertices, and just the structure of a tree in general.
So, what we’re now going to do is go over a lot of the terms associated with specific nodes based
on where they are in the tree and how they’re connected to other nodes, while also visualizing
the general structure of a tree at the same time. 2 birds with one stone. Okay, let’s start.

Let’s begin with the things we’ve already talked about. A Vertice is a certain Node in a tree, and
an edge is a connection between Nodes. The first new thing is that every tree starts with what’s
known as a root node. This is always going to be the topmost node of a tree. Let’s add a Node
with the integer 50 to serve as our root node. Next up, let’s connect 2 vertices to our root node
using 2 edges. These 2 nodes we just added are known as child nodes since they are both
connected to the node containing the integer 50. Child Nodes can thus be defined as a certain
node which has an edge connecting it to another Node one level above itself. Vice Versa, the
root node, the vertice containing the integer 50, is now what’s called a parent Node to these 2
child nodes. Thus, we can define a parent Node as any node which has 1 or more child nodes.
Think back to our family tree, if we were using people instead of integers, it’d make sense that
the nodes directly connected to each other have some sort of familial relationship.

Let’s continue on by adding 2 child nodes to the Node containing the integer 20. When we do
this, the 30 node becomes a parent node, and the 2 nodes we’ve just added become child nodes.
We have now branched off from the 20 node completely, in that the two child nodes, containing
integers 10 and 15 respectively, share no direct association with it.

Speaking of the 20 node, since it does not have any children, it is what’s known as a leaf node, or
a node in a tree which doesn’t have any child nodes. In this context the 10 and 15 Nodes would
also be leaf Nodes. Finally, let’s add one more node as a child of the 10 node and there is our
tree in all its glory.

As a quick review, the 50 Node is the root node. The 30 and 20 Nodes are children of the root
node and the root node thus is the parent of the 30 and 20 node.

Then, the 10 and 15 nodes are children of the 30 Node, and the 30 Node is a parent Node to both
the 10 and 15 nodes.

The 5 Node is a child of the 10 Node and the 10 Node is a parent to the 5 Node.

And finally, the 5, 15, and 20 Nodes are all leaf Nodes because they do not have any children.

As you can see, one node, or vertice on our tree, can have many different titles depending on
where it is in the tree and what other Nodes are connected to it. For example, the 30 node is both
a parent node to the 10 and 15 nodes, but also a child of the 50 node. The 15 Node is both a child
of the 30 node and also a leaf node as it has no children. This terminology really comes in handy
when we start talking about trees which contain thousands of vertices and edges, and the data
becomes very complicated to order and maintain.
Moving on, the next 2 pieces of terminology I want to go over with you guys are the height and
depth of a tree. The height is a property of a particular tree init of itself, and the depth is a
property of each individual node. Let’s start with the height.

The height of a tree is the number of edges on the longest possible path down towards a leaf. So,
in our tree example, since the longest path in our tree which leads to a leaf is from the 50 node to
the 5 node, and there are 3 edges in that path, the height of the tree would be 3.

The depth of a certain node is the number of edges required to get from that particular node to
the root node. For example, let’s take our 30 Node. Since there’s only one edge connecting it on
the way up to the root node, it’s depth would be 1. For the 15 Node; however, since there are 2
edges which separate it from the root node, the 15 node would have a depth of 2.

That’s pretty much all you need to know about the terminology associated with trees in
Computer Science. As a review. We have the height and the depth obviously. Then we have
vertices, edges, root nodes, parent nodes, and leaf Nodes. Now there’s probably something that’s
been scratching at the edge of your mind for quite a while now, and that’s why the heck are they
called trees, they look nothing like trees. Regular trees are not upside down like this Data
Structure would lead you to believe. So who named Trees and why?

Well there is actually a simple answer to this. The tree is said to have been invented during the
1960’s by two Russian inventors. And the last time that a Computer Scientist got up from their
chair and went outside to actually see a real tree, is rumored to have happened back in 1954.
Ever since then, it’s just been grinding away at screens, watching hours upon hours of
NullPointerException YouTube Videos (Change Name Based on Channel), so please forgive
them for the confusion when it came to naming conventions, they must have just forgotten trees
aren’t upside down.

Now regular trees like the ones we just created are great for storing hierarchical data, but their
power can really be heightened when you start messing around with how the data is actually
stored within them. By imposing rules and restrictions on what type of data is stored within a
tree, as well as WHERE, we can effectively use the tree structure to its full potential. I could talk
about the different types of trees for a long time, so long that many of them are full segments in
this lecture, but for now I just want to cover one popular variant. This will be a good introduction
to how Trees can vary without simply diving into a big deviation from what we know already.
More specifically, the tree variant I want to talk to you guys about is the binary search tree.

A binary search tree is a simple variation on a standard tree which has three restrictions on it to
help organize the data. The first is that a node can have at most 2 children. This just helps make
it easier to search through a tree as we don’t have to spend time looking through each of the 8
children for a particular node. Keeping it limited to 2 helps us do this. The second restriction, or
property, is that for any given parent node, the child to the left has a value less than or equal to
itself, and the child to the right has a value greater than or equal to itself. This might seem weird,
but it comes with certain advantages and disadvantages over using normal trees which we’ll get
to in a bit. The final restriction put on binary search trees is that no 2 nodes can contain the same
value. And this is just to prevent weird things from happening when we begin searching through
the tree. Now, how do imposing these restrictions on a tree actually help us?

Well, the biggest advantage of Binary Search Trees is that we’re able to search through them in
Logarithmic time. Because there is a natural order to the way that the data is stored, it makes it
extremely easy to search for a given value. Logarithmic time, if you remember back to the
segment on Time Complexity, is the equation in which we get more bang for our buck the
greater the number of elements, or nodes we have in our data structure.

It works like this. All we have to do when searching is tell the computer to go left if the value
we’re searching for is less than the current node, and right if it’s greater than the current Node.
We can then wash, rinse, and repeat this strategy until we find our desired node. This makes
binary search trees really popular for storing large quantities of data that need to be easily
searchable. Of course this also translates to inserting, deleting, and accessing elements within the
data structure, but for the most part searching efficiency is what really sets the binary search tree
apart from the rest.

Stepping away now from binary search trees and into trees in general, let’s now talk about
common uses for them in computer science.

The most common uses for trees in Computer Science include storing data with a naturally
hierarchical structure. These are like the examples we touched upon at the beginning of the
segment. Data-sets such as file structure-systems, family trees, a company's corporate structure,
all of these could be stored and implemented using the tree data structure easily.

These are all general examples though. As I mentioned before, when we put restrictions on trees
like in the case of the binary search tree, we can expand its uses even further. A tree’s base
structure is incredibly useful, and it can be modified in so many ways which only add to its
functionality. One of these ways is through what’s known as a trie, and is the next data structure
on our agenda, so stay tuned for that.

Next up we’ll be talking about another one of the special trees with restrictions. We just finished
discussing the binary-search tree. And with that we mentioned how Trees usually become even
more useful once you start setting restrictions on how and where data can be stored within them.
Well, a trie is another one of these special trees which have special restrictions put in place to
help store the data in an effective manner. This data structure is often overlooked since it is only
used in specific situations, but without the use of tries within those specific situations, some
important features of your computing life would be a whole lot harder. So we get it Steven, a trie
is a special tree with restrictions, but what are those restrictions and how can they help us?

Well let’s start with the basics. A trie is a tree-like data structure whose nodes store letters of an
alphabet in the form of characters. We can carefully construct this tree of characters in such a
way which allows us to quickly retrieve words in the form of strings by traversing down a path
of the trie in a certain way. These are also sometimes called digital trees or prefix trees by the
way, but we’ll be calling them tries for today’s lecture. Tries are used in the retrieval of data in
the form of words, which is actually where the name trie comes from, as it’s smack dab in the
middle of the word retrieval. Essentially, in layman's terms, we use tries to retrieve words
extremely fast by traversing down a tree of stored characters. This is hard to explain without
actually showing you, so let’s do an example of what a trie might look like.

Now just like a normal tree, every trie starts with a root node. Only in our case that root node
will be empty, with either some null value or a blank String. Now, also stored within this Node
will be a set of references, all stored within an array. These references are set to null at the
beginning of the trie’s existence, but can slowly be filled with references to other Node’s. For
example, let’s say we were creating a trie to store words that start with the letter D. The root
node would contain an array which contains a reference to a Node containing the character D,
signifying the start of our word. Now imagine for a second that this D Node also has an array
containing references, only this time it contains references that point towards all characters in the
English alphabet which serve as the first two characters of a word in the English dictionary. So
Da serves as the first two characters in a lot of English words such as Dad or Dao or Dab if you
really want to go that far, and so the array contained within the “D” Node would hold a reference
to an “A” node. Now, since there are no English words which start with Db that I know of, we
wouldn’t have a reference to a B node, since we know we will never have to retrieve a word
from our trie which starts with db.

We continue this process for all letters in the alphabet, only including references to characters
which serve as the first two characters in an English word, but that’s a lot to visualize so for now
let’s just put up on the screen 2 Nodes this first “D” Node would point towards. More
specifically, the Node we’ve already added, the “A” node, as well as an “E” node.

Now, to continue building our trie, we would simply repeat the process for all of the Nodes that
the “D” node points towards. So, we’d store pointers in the “A” node which point towards
characters in the English alphabet that serve as the first 3 characters of a word in the English
dictionary. So you’d have a “B”, a “D”, a “Y” and many many more nodes as well, but we’re
just going to stick with those 3 for now. For “E”, you’d have pointers which point towards nodes
containing characters like “N” and “W”. Obviously there’d be more nodes than the ones shown
on your screen right now for all levels of Nodes, but what we’ve created here is a very simple
trie. As you can see it’s exactly like I said it was in the beginning, a tree-like data structure which
stores characters that can be used to make words by traversing down paths. As we travel down
different paths of our tree, we can make different words. Dad, Dab, Day down the one path. Den
and Dew down the other. By following the nodes downwards, we can easily build Strings of
words, which as you’ll learn towards the end of the episode can be extremely useful.

We can even take this process further by having one path contain multiple Strings. For example,
if we isolate the Den path, we could continue the process to go to words like Dense, or Denver,
or Dent, the list goes on and on. We wouldn’t have to just stop at the word den, since den is a
prefix for numerous other words, and that’s what makes storing the data in character format so
useful. One path down a trie can represent multiple words depending on where you stop along
the process.

This does provide a bit of a problem for computer scientists though. How do we know when a
word has ended? Let’s take the Denver path as an example. If we wanted to retrieve the word
“Den” from this trie, how would we know how to stop at the N node, and not continue along to
“Denver”.

Well, there is actually a pretty simple fix to this, and it’s known as “flagging”. We simply mark
the end of the word by having it also point towards a “Flag” to let the computer know that the
end of a word has occurred. So in our Denver example, not only would the “N” node’s array
contain pointers to whatever characters follow it, it would also contain a pointer to a Node with a
flag to tell the computer to stop there, in this case I’ve just chosen a period as the flag. This way,
we can construct tries in such a way wherein each word is marked by an ending point and the
different words that may branch off from that prefix can continue to use that word as a prefix in
whichever retrieval program ends up getting used.

Okay so now it’s time to talk about the general use cases of a trie. If you remember back to the
beginning of this segment, I said that the use cases for a trie were limited but extremely effective,
and now we’re going to talk about what that statement means.

Now have you ever used the autocomplete feature on your iphone? Or spell-check on a google
doc? Because if so, you have already experienced the overwhelming power of tries, as they are
used for both of these extremely useful features.

This mainly has to do with the fact that for big programs like IOS or google docs, they’re not just
storing a trie containing a few words or even all words starting with a certain letter like we tried
to replicate, they’re storing the entire English dictionary. Storing the entire dictionary in a trie
seems like a tall task, but it can and has been done. Each Node would simply have 26 nodes
connected to it and those 26 would have as many Nodes connected to them as needed and so on.
Now I can hear you asking, how does this help us with autocomplete and spell-check?

Well, let’s say you’re typing a word out. For the sake of this video let’s say it’s the word
subscribe. You start with the S. At this point the computer has already eliminated 95% of words
that you could be typing. We know it’s not going to be a word which starts with N, or with A, or
O, or H, or L, etc. etc. You type the letter U and bam, another 95% of possible options get
deleted. With each character you type, you eliminate millions of possible words that you could
be working towards. Using this fact, as well as popularity data which could also be stored in the
Node, the computer can start making educated guesses using that information as well as context
from previous words to make a suggestion as to what word you might be trying to type out. The
AI that works this feature is immensely complicated, but it is backed by the trie data structures
and helps autocomplete work easily on your phone.

As for spell check, well, when you spell a word wrong, a spell checking algorithm can use the
root of your word and compare it against trie data to make an educated guess as to what you
were trying to spell, as slightly misspelled words will usually have a similar path from the root of
the trie. If you accidentally type Chocolata instead of Chocolate, the computer can take the first
few characters of the word you typed incorrectly, and see where you may have gone wrong.
Basically, by comparing the misspelled word to certain paths of the dictionary trie, it can pretty
accurately detect which word you were meaning to say, and correct you accordingly.

So there you have it, the trie. Like I said it is often underrated in the Computer Science world
since it can only be used in certain situations, but as I hope this segment has shown you, those
situations are still important nonetheless.

We are nearing the end of our Introduction to Data Structures series now. Only 2 more left
before we part ways, so stay tuned because now we’re going to talk about Heaps.

Now back in our segment on Tree’s we talked about the binary search tree, a special type of tree
which has a few properties. Each Node can only have 2 children. The child to the left must have
a value less than the parent node, and the child to the right must have a value greater than the
parent node, and no two Nodes can contain the same value. Well, a Heap is sort of like this but a
little bit different.

More specifically, by definition, a Heap is a special tree where all parent Node’s compare to their
children Node’s in some specific way by being more or less extreme, i.e greater than or less than.
This “specific way” determines where the data is stored, and is usually dependent on the root
Node’s value. There are two different methodologies generally used in Computer Science and
they are known as Min-Heaps, and Max-Heaps.
In a min-heap the value at the root node of the tree must be the minimum amongst all of its
children, and this fact must be the same recursively for any and all parent Nodes contained
within the heap. So, each parent Node must have a value lower than all of its children Nodes. As
you can see from our example on the screen now, 10 is the root node, and also the minimum
value in the tree. In addition to this fact, if we pick any parent node on the tree and look at it’s
children and their children and so on, that parent node will have the lowest value of them all.
Take 14 for example, it’s value is less than 26, 31, 44, 35, and 33. This must be the case for
every single sub-tree in the Heap.

Max-heaps on the other hand are the exact opposite. In a max-heap, the value at the root node of
the tree must be the maximum amongst all of its children, and this fact must be the same
recursively for any and all parent Nodes contained within the heap. If you take a look at the
example Max heap we have on the screen, again you’ll see that this is the case. 44 is the root
node, and also the largest value within the heap. If you take a look at, say, the subtree which is
parented by the 35 Node, you’ll see that 35 is the maximum value amongst all Nodes in that
subtree, both 19 and 27.

When we store data like this in a Heap, whether that be a min-heap or a max-heap, it makes it
extremely easy to insert and remove from. This lends itself to a lot of useful implementations
within Computer Science. Now to show you this, I’d like to build an example max-Heap, the one
which has the greatest integer stored in the root-node. For the sake of keeping things simple, let’s
pull up an array of 7 elements with integers ranging from 0 to 100, and simply convert it into a
heap one by one. This can be done in a few easy steps. Step 1 is we add the first integer in as the
root node. So, 70 would get inserted into our tree as the root node. Then, we add another Node to
the tree in the bottom leftmost position available. So we would first insert a Node as a child of
the root Node to the left. For OUR heap, this means adding the integer 4 as a child of the 70
Node. The final step is to recursively go up the Heap, and swap Node’s if necessary. Now, when
we say “if necessary” we mean that if the Node we just added is more extreme, either greater
than or less than the node above it depending on the type of heap we’ve created, we swap them
to maintain order amongst the heap.

Since we’re building a max heap, and 70 is greater than 4, no swaps are necessary. Now, we just
repeat steps 2 and 3 until we’ve built our heap. So next, we want to add the integer 90, and since
the left-most slot on the tree is already taken by our 4 Node, the right slot ends up being the left-
most location on our tree. Then, since 90 is greater than 70, we swap the 90 and 70 Nodes. Doing
this keeps the max-heap property intact, where every level is greater than the one below it. Let’s
keep going.

Next we add 45 to the heap. Now since we’ve run out of space on the second level, we move on
to a third level and add 45 as a child of the 4 Node. Then, we compare it to 4, which it is greater
than, so we swap the Nodes. Now we have to compare this Node to the Node above it again,
because remember we recursively go up the tree until we reach the root or don’t need to swap.
Now 45, is not greater than 90, so it stays put.

Next up, we add 23 as another child of the 45 Node, only this time on the right side. We
compare, and since it’s not greater than 45, we keep it as is.

Moving on, we next insert 76 into the tree as a child of the 70 Node. Then we compare and swap
the 76 and 70 Nodes as 76 is indeed greater than 70. We then compare 76 and 90, and since 90 is
greater than 76, keep the 76 Node in place for now.

The next Node we add is the 100 node. We compare it to the 76 Node and see that it’s greater, so
it gets swapped. And then, we compare it again, this time to the 90 Node, and since it’s also
greater than that integer, it gets swapped yet again to become the root Node, signifying it is the
greatest Node in the Heap.

As you can see it’s a pretty simple concept. You add a Node, and then keep trying to swap up
until the Node you’ve added is in its rightful place.

Deleting from a Heap is also pretty simple, at least in our case. This is because the type of
deletion I want to talk about is just the process of removing the root node from the heap, and
you’ll see why later on. To delete the root Node from a heap, you follow a 3-step process.
Step 1 is actually removing the root node from our Heap, so in our case we’d delete the 100
Node. What you do with it is up to the programmer, you can return it back to the user, store it
somewhere else, etc. etc. Either way, then, step 2 is replacing it with the Node furthest to the
right, in this case, the 76 Node. Finally, for step 3, we do what’s known as a “heapify” to fix up
the heap. We start with the root Node and compare it to its children, to see if we need to swap
any values. So, for the 76 Node, we compare it to it’s two child Nodes, and since 76 is less than
90, we end up swapping the 2. Then, we wash/rinse/repeat for every subtree that we have. So on
the right side, we swapped 90 with 76, but since 76 remains the greatest integer on that particular
sub-tree it stays in the same spot. On the left side, we didn’t change anything but 45 is still the
greatest integer amongst the 3 Nodes in that subtree, so no swapping is necessary on that branch
of the Heap. And so we’ve completed our heapify and all Heap properties are still intact.

That’s inserting and deleting Nodes in a nutshell, now let’s talk about how we can use this to our
advantage.

Heaps are most commonly used in the implementation of HeapSort. Heapsort is a sorting
algorithm which takes in a list of elements, builds them into a min or max heap, and then
removes the root Node continuously to make a sorted list.
Because heaps always start with the minimum or maximum value contained within them at the
root Node, we’re able to simply just remove the root node over and over again, heapifying the
data structure after every pass. After every element has been removed we are left with a sorted
list. On the screen, you’ll see we have an unsorted list on the left. In the middle we’ve inserted
all of those integers and in doing so created a Max Heap. And then finally on the right we have
continuously removed those elements from the root Node into a now-sorted list.

HeapSort is a really cool algorithm, and will be part of our upcoming series on sorting
algorithms which is kicking off very soon, so if you’re interested in that, make sure you
subscribe (check out our channel) so that you don’t miss it.

Another extremely popular use of Heaps is through the implementation of priority Queues.
Priority Queues are an advanced data structure which your computer uses to designate tasks and
assign computing power based on how urgent the matter is. Think of it like a line at the hospital.
You wouldn’t want your line to follow a regular Queue methodology which implements first in
first out, since then you could have patients with extremely urgent matters like heart attacks
waiting behind people coming in for a routine check-up. In the same way, you wouldn’t want
your computer to update an application before it finishes rendering a video, otherwise your
progress would be lost. Priority Queues take care of all the task scheduling done by your
computer, and the Heap data structure is used as the backing system for it.

And with that ends our discussion on Heaps. To review, they are a special tree in which each
level contains Node’s with values more extreme, either greater than or less than, the Node’s on
the level above it. Next up is unfortunately the last segment in this series, on yet another tree
based data structure, the Graph.

The graph is arguably the most dynamic data structure in our Introduction to Data Structures
series, and an absolute banger to go out on, so let’s just hop right into it.

Before we get into the nitty-gritty details. I first want to do a short little exercise. Visualize for a
second a few of your favorite places to eat on a map around your town. For me personally it'd be
places like Five Guys, Chick-fil-a, Panera, Wawa, and Dominos. Now, imagine for a second that
you are ravished, absolutely starving and so your plan is obviously to drive around to each of
your favorite places to eat and order an ungodly amount of food from each location. Each food
location has a few possible paths going to and from it obviously, and so we add those to the map
as well. Now you can see that we now have what looks like a network of delicious foods. We can
start anywhere, and all we have to do is make sure to hit all 5. You may not know it, but what
we’ve done is set up a simple graph. Essentially, graphs are composed of pieces of information
like the restaurants, and the paths that run between them.
Of course this is just generally. By definition, Graphs are a non-linear data structure consisting of
nodes and edges. There are a finite set of these nodes, or vertices, which are connected by edges.
Nodes and edges should be familiar to you if you watched the segment on trees. The big
difference between trees and graphs; however, is that with a tree we had a specific starting point.
Sure, there were multiple paths down the tree that branched off from the initial starting point, but
you always had to begin at the root node. In contrast, with a Graph there is no specified starting
point. We can begin from any Node and traverse to any Node. Just like how in our food example
we were able to start at any restaurant. Graphs are a huge concept, and escape the bounds of
Computer Science, often being used in many places you wouldn’t even think of. But before we
get into anything crazy though, like the difference between an directed or undirected graph or
acyclic versus cyclical graphs, let’s get down the basics.

Now, every Graph is composed of these Nodes or Vertices and the Edges that connect them.
Let’s pull up a sample Graph really quickly and talk about it. We represent graphs visually like
this a lot because it makes it way easier to comprehend, but notation-wise, a Graph actually
looks like this, which is much harder to comprehend, so let’s break it down.

First we have the Vertices set, which contains a comma-separated list of all vertices within the
Graph, that’s the simple part. Each comma-separated value simply represents a Node within the
Graph. Then, we have the edge set which is a little more complicated. Each element of the edge
set is an ordered pair which describes a relationship between Nodes. For example, the first one
describes a relationship between the 6 and 4 Nodes. The 5th indicates a relationship between the
5 and 2 Nodes, and so on. Using these two sets we’re able to visualize a Graph pretty easily by
laying down both the information, and the connections that fall between them.

One final thing I want to mention about Graphs is about the relationships that occur between 2
Nodes. If we have an edge which connects two different Vertices, they are known as adjacent to
one another. So, for example the 5 Nodes would be adjacent to the 4, 2, and 1 Nodes.

Okay, now that we have the basics down, I now want to jump into the different attributes that a
particular Graph might have, starting with Directed versus Undirected.

An undirected graph is one in which the direction you traverse the Nodes isn’t important. This is
most prominently indicated by a lack of arrows pointing to specific Nodes, such was the case
with our first example Graph or even the food example from the beginning of the episode. We
can hop between Node’s, or even back and forth between them without problem. A good way to
visualize undirected Graphs is like a network of friends on facebook, where each edge indicates a
“friendship” if you will. Because of the fact that when somebody accepts to be your friend on
facebook, you both are added to each other’s friends list, the friendship goes both ways, and
direction is unimportant.
In contrast, a directed graph is one in which the direction you traverse the Nodes is important.
This is usually indicated by arrows representing which Nodes a certain Node is able to traverse
to. The edges could point both ways, but they don’t have to. It’s very possible the edge only
points one way. A good way to visualize directed Graphs is by thinking of them as a network of
friends on Instagram. I can follow famous celebrity Will Smith, but the odds that he follows me
back? Fairly low, and so in that case the relationship only goes one way.

Undirected and Directed graphs both have their uses, as we discussed with the social media
example. Both provide different functionality which will be useful to you in your Computer
Science journey, just like the next type of property a Graph can be classified as, either cyclic or
acyclic.

A cyclic Graph is one which contains a path from at least one Node back to itself. So you can see
by the example on your screen that the 4 Node leads to the 3 Node, which then leads to the 2
Node, which leads to the 1 Node, which finally leads back to the 4 Node, forming a cycle.
Essentially, we’re able to follow at least one path that eventually leads back to our starting point.
A small thing to note here is that all undirected Graphs end up being cyclical. The bidirectional
nature of the Nodes within undirected Graphs theoretically forms a cycle between any 2 Nodes.
So judging by that logic, all undirected Graphs end up being cyclic.

An acyclic Graph is one which contains no path from any one Node which leads back in on
itself. This property can really only apply to undirected graphs like we mentioned previously.
Essentially, this means that for any given Node, there is no path which will eventually lead back
to itself.

Undirected, Directed, Cyclic, and Acyclic are all properties we can use to classify types of
Graphs based on their Nodes, but the last property I want to talk about actually applies to the
edges of a Graph instead, and is the process of weighting.

Weighting the edges of a Graph means associating a numerical value with each edge. Often
called the “cost” of that edge, each weight represents some property of the information you’re
trying to convey. For example, again going back to our food location scenario, since the
information we’re trying to convey is a good route which takes us to each location, a good
weight for our edges in that scenario could be the distance between Nodes. This comes in handy
a LOT, especially with navigation, such as is the case with our restaurant, as we of course always
want to find the path of least “cost” or “weight” between the different Nodes.

So there are the major properties of a Heap that the different Node’s and Edge’s can have.
Directed or Undirected, Cyclic or Acyclic, and weighted or Unweighted. There are a couple
more obscure ones out there, but those 6 are what we will be covering. Combining these 3
properties together leaves us with numerous types of graphs which all have different strengths
and weaknesses. It would take a while to talk about each of these and their implementations, so
for now I’ll just pick out three types of Graphs which are used in the most popular cases.

Now, probably the most famous implementation of the Heap Data Structure is through the
Undirected Cyclical Heap with Weighted edges. This one gets a lot of use, specifically through
its implementation in Dijkstra's shortest path algorithm. This algorithm, given a graph and a
source vertex within that graph, compiles a list of the shortest possible paths from that source
vertex to all other Nodes. As you might be able to tell just from its description, this has a
multitude of uses across the entire tech world. Google uses this algorithm for Google Maps, it’s
used in the process of IP routing, and can even be implemented in telephone networks.

Another type of graph which you probably use quite often is the unweighted cyclical graphs,
both undirected and directed, as these make up the follower system of a majority of social media
websites. We already talked about these in the cases of facebook, which would use cyclical
representations, as well as Instagram, which would use an acyclic representation; however, this
example encapsulates much more than just those 2. Snapchat, Twitter, TikTok even. All these
platforms can represent your follower/following base through a Graph, and oftentimes do.
Facebook even has a Graph API which you can use to interact with the models that they use to
illustrate each user’s “Web” of friends.

As you can see, Graphs and their many different forms provide a lot of functionality that you
interact with in everyday life, contributing to almost any facet of the internet.

And with that concludes our discussion on Graphs. As a review, a Graph is a data structure
which is represented by a set of Nodes and Edges. These come together to form a visualization of
data. Whether that data be food locations on a map or friends on social media, the different types
of Graphs provide a multitude of implementations in Computer Science.

And with that concludes our Introduction to Data Structures Series. After around 2 and a half
hours and 14 data structures, you should now have the basic knowledge on data structures to take
with you as you continue along your computer science journey

And if you’ve made it this long, you obviously liked what you saw, so stick around for an extra
30 seconds and I’ll pitch to you why you should check out my personal channel.

Now I introduced myself at the beginning of the series, but again my name is Steven. I’m an 18
year old CS Student who runs a Y0.

souTube channel called NullPointerException with one of my good friends Sean. We’ve been
producing content like this seriously for about 4 months now and have grown a good audience of
around 800 subscribers which we couldn’t be happier about. If you’d like to join us you can
check out our channel linked in the description below, and just try out a few of our other videos,
and if you like what you see, you can subscribe.

That ends my pitch, and with that I have nothing else to stay. I hope you enjoyed this series as
much as I have enjoyed making it, thank you so much for watching. Peace.

You might also like