Hi, and welcome to this lesson about queuing structures.
In the previous lessons, we’ve looked at the two main algorithms for graph traversal, DFS and BFS, and how to build and use routing tables to navigate their corresponding graphs.
In this lesson, we’ll see how these algorithms can be implemented in practice, using queuing structures.
A queuing structure is a memory structure that can be used to manage priorities between tasks.
For example, imagine that we have a list of documents, and we need to decide the order in which we will process them.
Queuing structures can be used to store the documents and manage the priority between documents.
There are only two operations that can be performed when using a queuing structure.
Namely, adding (or pushing) an element, and removing (or, popping) an element.
I’ll now introduce two queuing structures that can be used to manage priorities in different ways.
Stacks / LIFO / Last-In First-Out
A stack, which is known as a Last-In First-Out, or LIFO for short, is a memory structure for which the last element added in the stack is the first one that will be removed.
Let's imagine a list of documents we have to process.
Using a LIFO means putting all the documents in a pile, or stack.
You can only pick up the document on the top of the pile, which was the last one added.
Queues / FIFO / First-In First-Out
On the contrary, a queue, which is known as a First-In First-Out, or FIFO for short, is a memory structure for which the first element added will be the first one to be removed.
Imagine it as a queue in a shop.
Customers are served on a first-come, first-served basis.
A unified algorithm for graph traversal
A queuing structure can be used to manage the priorities with which we examine vertices in a graph when performing graph traversal.
When exploring a vertex, a queuing structure can be used to store the neighboring vertices, but only if those vertices weren't previously explored.
By repeating this principle until the queuing structure is empty, we obtain a DFS if we choose to use a LIFO, and BFS when we use a FIFO.
Indeed, when using a LIFO, we first explore the last vertices that were added to the queuing structure, and thus keep following the last direction.
However, when using a FIFO, we first explore the first added vertices, and thus proceed with increasing hops from the start.
The following unified algorithm will function for both a BFS and a DFS by simply changing the queuing structure.
In other words, using the LIFO for a DFS, and the FIFO for a BFS.
Once we have chosen a queuing structure, we first push the starting vertex.
Here, we are interested in building the routing table as well, so, in the queuing structure we push couples made of the vertex and its parent in the corresponding spanning tree.
Since the starting vertex has no parent, we use NULL instead here.
We also create the initially empty set of explored vertices, and the initially empty routing table.
Then, as long as the queuing structure is not empty, we pop a vertex and its parent.
If this vertex is not already explored, we mark it as explored and we update the routing table by associating the vertex with its parent.
Finally, we add all unexplored neighbors of the vertex to the queuing structure, with the current vertex as parent.
If the vertex we popped from the queuing structure was already explored, we simply ignore it.
The algorithm finishes when the queuing structure is finally empty.
Depending on the choice of the queuing structure: a LIFO or a FIFO, we obtain a DFS or a BFS.
Let’s illustrate this using an example.
Application with DFS
Let's perform a DFS from $v_1$.
We show the content of the LIFO as we traverse the graph.
The list of explored vertices is on the right.
We also show the routing table.
We begin by pushing the starting vertex $v_1$ into the LIFO, with NULL as its parent.
We pop a vertex and its parent from the LIFO, $v_1$, and NULL.
As $v_1$ is not in the list of explored vertices, we mark it as explored and update the entry corresponding to $v_1$ in the routing table by setting it to NULL.
Then, we push in the LIFO all the neighbors of $v_1$ that are not in the list of explored vertices, which are $v_2$ and $v_4$, with their parent $v_1$.
We remove an element from the LIFO, which is $v_4$, with $v_1$ as parent, because it's the last element that was added.
Vertex $v_4$ is not in the list of explored vertices, so we mark it as explored, and update the entry corresponding to $v_4$ in the routing table by setting it to $v_1$.
Next, we push all its unexplored neighbors into the LIFO.
As $v_1$ has already been explored, we push $v_2$, $v_6$ then $v_3$ in the LIFO, with $v_4$ as parent.
The next element to be removed is $v_3$ and its parent $v_4$.
As $v_3$ wasn't previously explored, we add it to the list of explored vertices and update the routing table accordingly.
Among the neighbors of $v_3$, only $v_2$ hasn't been explored, so we push $v_2$ in the LIFO, with $v_3$ as parent.
We remove $v_2$ and its parent $v_3$ from the LIFO.
Vertex $v_2$ wasn't previously explored, so we mark it as explored and update the routing table by setting the corresponding entry to $v_3$.
Among the neighbors of $v_2$, only $v_6$ hasn't been explored, so we push $v_6$ with $v_2$ as parent in the LIFO.
The next element to be removed is $v_6$ and its parent $v_2$.
Vertex $v_6$ wasn't previously explored, so we mark it and update the routing table.
The unexplored neighbors of $v_6$ are $v_5$ and $v_7$, so we push them into the LIFO with $v_6$ as parent.
We then remove $v_7$ with its parent $v_6$.
As $v_7$ wasn't previously explored, we mark it as explored and update the routing table.
The only neighbor of $v_7$ is $v_6$, which was previously explored, so nothing is pushed in the LIFO.
The next element that is popped from the LIFO is $v_5$ and its parent $v_6$.
Vertex $v_5$ wasn't explored, so we mark it as explored and update the routing table. Here again, the only neighbor of $v_5$ is $v_6$, which was already explored, so nothing is pushed in the LIFO.
Next, we pop $v_6$ with $v_4$ as parent.
As $v_6$ has already been explored, nothing happens.
We pop $v_2$ with $v_4$ as parent, and here again, $v_2$ has already been explored, so nothing happens.
Finally, $v_2$ with $v_1$ as parent is popped from the LIFO, but again, $v_2$ has already been explored, so nothing happens.
The queuing structure is now empty, so the algorithm finishes.
Notice that the fact that we’re using a LIFO directly corresponds to the fact that we keep exploring the graph until the search reaches a vertex without unexplored neighbors, in which case we go back to previously explored vertices.
Application with BFS
Let's now see how we can perform a BFS from $v_1$ by using a FIFO.
We start by pushing $v_1$ in the FIFO, with NULL as parent.
We pop a vertex and its parent from the FIFO, $v_1$ and NULL.
Vertex $v_1$ wasn't previously explored so we mark it, and update the routing table.
Then, we push in the FIFO all unexplored neighbors of $v_1$, which are $v_2$ and $v_4$, with their parent $v_1$.
We pop an element from the FIFO, which is $v_2$ with $v_1$ as parent, because it's the first element that was pushed.
Vertex $v_2$ wasn't previously explored, so we add it to the list of explored vertices and update the routing table accordingly.
We push all neighbors of $v_2$ that are not in the list of explored vertices: $v_4$, $v_3$ and $v_6$, with $v_2$ as parent.
The next element to be removed from the FIFO is $v_4$ with $v_1$ as parent.
As $v_4$ is not in the list of explored vertices, we mark it and update the routing table.
Among the neighbors of $v_4$, only $v_3$ and $v_6$ are not in the list of explored vertices, so we push them in the FIFO with $v_4$ as parent.
Next, we pop the element $v_4$ with $v_2$ as parent.
Vertex $v_4$ has already been explored, so we move on.
We pop the next element from the FIFO, $v_3$ with $v_2$ as parent.
Vertex $v_3$ wasn't explored, so we mark it and update the routing table accordingly.
All neighbors of $v_3$ have been explored, so we continue.
The next element to be popped from the FIFO is $v_6$ with $v_2$ as parent.
This vertex wasn't previously explored, so we mark it and update the routing table.
Among the neighbors of $v_6$, only two neighbors are unexplored: $v_7$ and $v_5$, so we push them in the FIFO with $v_6$ as parent.
We pop the element $v_3$ with $v_4$ as parent, but as $v_3$ was already explored, nothing happens.
Similarly, $v_6$ with $v_4$ is popped from the FIFO, but as $v_6$ was already explored, we move on.
Now, we remove from the FIFO the element $v_7$ with $v_6$ as parent.
Vertex $v_7$ was not previously explored so we mark it and update the routing table.
The only neighbor of $v_7$ is $v_6$, which was previously explored, so we continue.
Finally, we pop $v_5$ with $v_6$ as parent from the FIFO.
Vertex $v_5$ is not in the list of explored vertices, so we add it to this list, and update the routing table.
Here again, $v_5$ does not have any unexplored neighbors.
The queuing structure is now empty, so the algorithm finishes.
We’ve now successfully traversed the graph using BFS.
Concluding words
So, that’s how you can use queuing structures to implement DFS and BFS.
Thanks for watching!
I hope you enjoyed learning about queuing structures.
That’s the end of this week’s lessons.
Next week, we’ll see how to deal with weighted graphs.