DEV Community

Cover image for Data Structures: Graphs
Tamerlan Gudabayev
Tamerlan Gudabayev

Posted on • Updated on

Data Structures: Graphs

Introduction

Graphs.

Everyone talks about them.

They always ask them in interviews.

But you don't know them.

It doesn't have to be this way, that's why in this week we are tackling the graph data structure.

Learning Outcomes

Today, you will learn:

  • What is a graph.
  • Where are graphs used.
  • Types of Graphs
  • How to implement a graph
  • How to traverse a graph

Table of Contents

What is a graph?

A graph is a non-linear data structure that helps us describe entities and it's relationships to other entities.

For example, you may use the graph data structure to represent friends in a social media website.

Alt Text

Graphs are very useful because they can describe a great deal of problems we have in the real world.

But before we move on, we have to break down the graph even more.

Essential Terminology

Alt Text

A graph (V,E) is made out of two parts:

  • Collection of Vertices (V): Your basic node (i.e user, place, event, etc...)
  • Collection of Edges (E): They represent the relationships between nodes. In mathematical terms, they can be represented by coordinates of two nodes (u,v).

We can represent the graph above by:

V = {0, 1, 2, 3, 4}
E = {
     (0,1), (0,4), (1,0), 
         (1,2), (1,3), (1,4),
     (2,1), (2,3),
     (3,1), (3,2), (3,4),
     (4,0), (4,1), (4,3),
    }
G = {V, E} // A graph is a collection of vertices and edges
Enter fullscreen mode Exit fullscreen mode

Types of Graphs

While researching, I've come across many different types of graphs, but to save you time I've narrowed it down to two basic types:

  • Directed Graph
  • Simple Graph

Directed Graphs

Alt Text

A graph where all edges are directed from one vertex to another vertex is called a directed graph.

The connections are not bidirectional, meaning that if we have a connection from A to B, that doesn't mean we have a connection from B to A.

But you might ask "Why will we use something like this?"

Well, there are many use cases for directed graphs, here are a few:

  • Twitter: When you follow someone, that's a direct connection, the other person doesn't need to follow you.
  • Flights: You might be able to fly from Paris to London, but there's no guarantee that you can fly from London to Paris.
  • Road Networks: Some roads are one-way, others are two-way, you get the gist.

Simple Graphs

Alt Text

Graphs that are always two-way connections are called simple graphs. Our first example (at the beginning of the post) was a simple graph (also called undirected graphs).

PS. Through out this post, whenever I say graphs, I'm gonna mean simple graphs.

Weighted Graphs

Alt Text

So far we assume that every edge is equal to each other. Unfortunately, life doesn't work that way, the distance between France and Germany is shorter than France and the USA. This is why we add a weight to each edge. A graph with weighted edges is called a weighted graph.

Applications

Graphs are very useful, they can represent many real-life models.

Here are a few notable use cases:

  • Social Graphs: This helps showcase the relationship between you and the people, places and thing you interact with online.
  • Knowledge Graphs: This is notably used at Google, to connect data to each other. For example, the painting "Mona Lisa" is associated to "Leonardo Davinci", in which he is associated to his other painting, and the rest of his information.
  • Recommendation Engines: Graphs helps us build recommendation engines, for example in Netflix, if you like "The Witcher", you will be recommended other shows by people who also liked "The Witcher". Everything is connected to each other.
  • Path Optimization Algorithms: What's the best way to get from Tokyo to London, or from your local grocery store to your nearest home. These types of questions can be modelled and solved using graphs. This is already being used in most map applications such as Google Maps.

Types of Implementations

Now that we have the theory covered, let us move into more practical stuff.

There are three main ways to implement graphs:

  • Adjacency Matrix
  • Adjacency Lists
  • Adjacency Sets

Let us cover each implementation with it's pros and cons.

Adjacency Matrix

Alt Text

In adjacency matrix, we represent the graph in a two-dimensional array of V x V vertices.

If any element of graph[i][j] is 1, this means that there is an edge connecting vertex i with vertex j.

This approach if very fast for look-ups. Because we can instantly get the value of graph[i][j]

But it's also very heavy on memory because we have to reserve a space in memory for every possible edge (V x V).

Adjacency List

Alt Text

Adjacency list represents graphs as an array of linked lists.

The index of the array represent the vertex and each element in the linked list represents the vertices it connects to, making an edge.

This approach saves us a lot of space, but it's slower than adjacency matrix on hookups.

Adjacency Set

Alt Text

This is a new alternative to adjacency lists, instead of having an array of linked lists, have it be an array of sets.

But why?

Well the reason is that sets are ordered and do not allow for duplicates.

We can use binary search, making our search times O(log n).

Code Implementation

For this example, we are gonna use the adjacency list implementation because it's the most common.

We will also use python because I like it.

# A class to represent the adjacency list of the node
class AdjNode:
    def __init__(self, data):
        self.vertex = data
        self.next = None

# A class to represent a graph. A graph
# is the list of the adjacency lists.
# Size of the array will be the no. of the
# vertices "V"
class Graph:
    def __init__(self, vertices):
        self.V = vertices
        self.graph = [None] * self.V

    # Function to add an edge in an undirected graph
    def add_edge(self, src, dest):
        # Adding the node to the source node
        node = AdjNode(dest)
        node.next = self.graph[src]
        self.graph[src] = node

        # Adding the source node to the destination as
        # it is the undirected graph
        node = AdjNode(src)
        node.next = self.graph[dest]
        self.graph[dest] = node

    # Function to print the graph
    def print_graph(self):
        for i in range(self.V):
            print("Adjacency list of vertex {}\n head".format(i), end="")
            temp = self.graph[i]
            while temp:
                print(" -> {}".format(temp.vertex), end="")
                temp = temp.next
            print(" \n")

# Driver program to the above graph class
if __name__ == "__main__":
    V = 5
    graph = Graph(V)
    graph.add_edge(0, 1)
    graph.add_edge(0, 4)
    graph.add_edge(1, 2)
    graph.add_edge(1, 3)
    graph.add_edge(1, 4)
    graph.add_edge(2, 3)
    graph.add_edge(3, 4)

    graph.print_graph()
Enter fullscreen mode Exit fullscreen mode

Traversing a Graph

Most common interview questions that include graph will include some sort of traversal of it's very useful to know how to traverse one.

There are two main algorithms:

  • Breadth First Search
  • Depth First Search

Breadth First Search

Alt Text

Breadth-First Search (BFS) is a vertex-based technique for finding the shortest path. In a nutshell it everything in the topmost layer before moving down.

Here's the code:

def BFS(self, s):

    # Mark all the vertices as not visited
    visited = [False] * (max(self.graph) + 1)

    # Create a queue for BFS
    queue = []

    # Mark the source node as
    # visited and enqueue it
    queue.append(s)
    visited[s] = True

    while queue:

        # Dequeue a vertex from
        # queue and print it
        s = queue.pop(0)
        print (s, end = " ")

        # Get all adjacent vertices of the
        # dequeued vertex s. If a adjacent
        # has not been visited, then mark it
        # visited and enqueue it
        for i in self.graph[s]:
            if visited[i] == False:
                queue.append(i)
                visited[i] = True
Enter fullscreen mode Exit fullscreen mode

Depth First Search

Alt Text

Depth-First Search (DFS) is an edge-based technique to find the shortest path. It uses the Stack data structure, performs two stages, first visited vertices are pushed into the stack and second if there is no vertices then visited vertices are popped.

        # A function used by DFS
    def DFSUtil(self, v, visited):

        # Mark the current node as visited
        # and print it
        visited.add(v)
        print(v, end=' ')

        # Recur for all the vertices
        # adjacent to this vertex
        for neighbour in self.graph[v]:
            if neighbour not in visited:
                self.DFSUtil(neighbour, visited)

    # The function to do DFS traversal. It uses
    # recursive DFSUtil()
    def DFS(self, v):

        # Create a set to store visited vertices
        visited = set()

        # Call the recursive helper function
        # to print DFS traversal
        self.DFSUtil(v, visited)
Enter fullscreen mode Exit fullscreen mode

Conclusion

We covered a lot today, and I hope you got something new out of it. This is the ninth post in our data structures series, time flies by. Anyways, that we have covered most of the main data structures, it's time to move onto algorithms. So stay tuned, and I hope you have a nice day.

Additional References

Top comments (0)