Introduction To Graph - Python

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 10

What is a Graph?

A graph is a set of nodes that are connected to each other in the form of a
network.

Vertex :

A vertex is the most essential part of a graph. A collection of vertices forms a


graph. In that sense, vertices are similar to linked list nodes.

Edge :

An edge is the link between two vertices. It can be uni-directional or bi-


directional depending on your graph. An edge can also have a cost associated
with it.

Types of Graphs
There are two common types of graphs:

1. Undirected
2. Directed

Undirected Graph

In an undirected graph, the edges are bi-directional by default; for example, with
the pair (0,1), it means there exists an edge between vertex 0 and 1 without any
specific direction. You can go from vertex 0 to 1, or vice versa.

Directed Graph
In a directed graph, the edges are unidirectional; for example, with the pair (0,1),
it means there exists an edge from vertex 0 towards vertex 1, and the only way
to traverse is to go from 0 to 1.

Graph Terminologies

1. Degree of a Vertex: The total number of edges incident on a vertex. There are two
types of degrees:

• In-Degree: The total number of incoming edges of a vertex.

• Out-Degree: The total number of outgoing edges of a vertex.

2. Parallel Edges: Two undirected edges are parallel if they have the same end
vertices. Two directed edges are parallel if they have the same starting and ending
vertices.

3. Self Loop: This occurs when an edge starts and ends on the same vertex.

4. Adjacency: Two vertices are said to be adjacent if there is an edge connecting them
directly.

Graph Implementation

Here, we will implement a Directed Graph in java using an Adjacency List.

The Graph Class

Graph class consists of two data members:

• The total number of vertices in the graph


• A list of linked lists to store adjacent vertices

Let’s get down to implementation

class Graph:

def __Init__(self, vertices):

#Total number of vertices

self.vertices = vertices
#Defining a list which can hold multiple LinkedLists

#equal to the number of vertices in the graph

self.array = []

#Creating a new LinkedList for each vertex.index of the list

for i in range(vertices):

self.array.append(LinkedList())

As you can see in the code snippet above, we have a basic structure of Graph class.

Vertices: vertices to store the total number of vertices of the graph.

Adjacency List: to store an array of linked lists. Each index of the array represents a vertex of
the graph, and the linked list represents the adjacent vertices.

Methods:
print_graph() method prints the graph (adjacency list).

add_edge() creates a source and destination vertex and connects them with an edge.

Code Snippet

class Node:
def __init__(self, data):
self.data = data
self.next_element = None

class LinkedList:
def __init__(self):
self.head_node = None
def get_head(self):
return self.head_node
def is_empty(self):
return self.head_node is None
def insert_at_head(self, dt):
temp_node = Node(dt)
if self.is_empty():
self.head_node = temp_node
return self.head_node
temp_node.next_element = self.head_node
self.head_node = temp_node
return self.head_node

def insert_at_tail(self, value):


new_node = Node(value)
if self.get_head() is None:
self.head_node = new_node
return
temp = self.get_head()
while temp.next_element is not None:
temp = temp.next_element
temp.next_element = new_node
return

def length(self):
curr = self.get_head()
length = 0
while curr is not None:
length += 1
curr = curr.next_element
return length

def print_list(self):
if self.is_empty():
print(“List is empty”)
return False
temp = self.head_node
while temp.next_element is not None:
print(temp.data, end = “->”)
temp = temp.next_element
print(temp.data, “-> None”)
return True

def delete_at_head(self):
first_element = self.get_head()
if first_element is not None:
self.head_node = first_element.next_element
first_element.next_element = None
return

def delete(self, value):


deleted = False
if self.is_empty():
return deleted
current_node = self.get_head()
previous_node = None
if current_node.daat is value:
self.delete_at_head()
deleted = True
return deleted
while current_node is not None:
if value is curent_node.data:
previous_node.next_element=current_node.next_element
current_node.next_element = None
deleted = True
break
previous_node = current_node
current_node = current_node.next_element
return deleted

def search(linkedlist, value):


current_node = linkedlist.get_head()
while current_node:
if current_node.data == value:
return True
current_node = current_node_next_element
return False

def remove_duplicates(self):
if self.is_empty():
return
if self.get_head().next_element is None:
return
outer_node = self.get_head()
while outer_node:
inner_node = outer_node
while inner_node:
if inner_node.next_element:
if outer_node.data==inner_node.next_element.data:
new_next_element = inner_node.next_element.next_element
inner_node.next_element = new_next_element
else:
inner_node = inner_node.next_element
else:
inner_node = inner_node.next_element
outer_node = outer_node.next_element
return

class Graph:
def __init__(self, vertices):
self.vertices = vertices
self.array = []
for i in range(vertices):
self.array.append(LinkedList())
def add_element(self, source, destination):
if source < self.vertices and destination < self.vertices:
#As we are implementing a direct graph, (1, 0) is not equal to (0,1)
self.array[source].insert_at_head(destination)
#For undirected path
self.array[destination].insert_at_head(source)
#If we were to implement an undirected graph i.e. (1, 0) == (0, 1)
#We would create an edge from destination towards source as well
#i.e self.array[destination].insert_at_head(source)

def print_graph(self):
print(“>>Adjacency List of Directed Graph<<”)
for i in range(self.vertices):
print(“ | ”, i, end = “ | => ”)
temp = self.array [i].get_head()
while temp is not None:
print(“[”, temp.data, end = “ ] -> “)
temp = temp.next_element
print(None)

graph = Graph()
graph.add_edge(0, 2)
graph.add_edge(0, 2)
graph.add_edge(0, 2)
graph.add_edge(0, 2)
graph.print_graph()
print(graph.array[1].get_head().data)

Output :

>>Adjacency List of Directed Graph<<


| 0 | => [ 1 ] -> [ 2 ] -> None
| 1 | => [ 3 ] -> None
| 2 | => [ 3 ] -> None
| 3 | => None

Let’s break down the two new functions that we’ve implemented

add_edge(self, source, destination):

This function simply inserts a destination vertex into the adjacency linked list of the
source vertex by running the following line of code:

self.array[source].insert_at_head(destination)

One important thing to note is that we are implementing a directed graph, so


add_edge(0, 1) is not equal to add_edge(1, 0). In the case of an undirected
graph, we will have to create an edge from the source to the destination and
from the destination to the source, making it a bidirectional edge:

self.array[source].insert_at_head(destination)

self.array[destination].insert_at_head(source)

print_graph(self)

This function uses a simple nested loop to iterate through the adjacency list. Each linked list is
being traversed here.

Time Complexities#
Below, you can find the time complexities for the 4 basic graph functions.

Note : V means the total number of vertices and E means the total number of edges in the
Graph.

Adjacency Adjacency
Operation
List Matrix
Add Vertex O(1) O(V2)
Remove Vertex O(V+E) O(V2)
Add Edge O(1) O(1)
Remove Edge O(E) O(1)
Adjacency Adjacency
Operation
List Matrix
Search O(V) O(1)
Breadth First
O(V+E) O(V2)
Search(BFS)
Depth First
O(V+E) O(V2)
Search(DFS)

Adjacency List
• Adding an edge in adjacency lists takes constant time as we only need to insert at the
head node of the corresponding vertex.

• Removing an edge takes O(E) time because, in the worst case, all the edges could be
at a single vertex and hence, we would have to traverse all E edges to reach the last
one.

• Removing a vertex takes O(V + E) time because we have to delete all its edges and
then reindex the rest of the list one step back in order to fill the deleted spot.

• Searching an edge between a pair of vertices can take up to O(V) if all V nodes are
present at a certain index and we have to traverse them.

Adjacency Matrix

• Edge operations are performed in constant time as we only need to manipulate the
value in the particular cell.

• Vertex operations are performed in O(V2) since we need to add rows and columns. We
will also need to fill all the new cells.

• Searching an edge is O(1) because we can access each edge by indexing.

Comparison

Both representations are suitable for different situations. If your application frequently
manipulates vertices, the adjacency list is a better choice.

If you are dealing primarily with edges, the adjacency matrix is the more efficient approach.

What is a Bipartite Graph?


The bipartite graph is a special member of the graph family. The vertices of
this graph are divided into two disjoint parts in such a way that no two vertices in
the same part are adjacent to each other.
The bipartite graph is a type of k-partite graph where k is 2. In a 5-partite graph, we
would have 5 disjoint sets and members of a set would not be adjacent to each other.

Types of bipartite graphs


Some popular types of bipartite graphs are:

• Star Graph
• Acyclic Graph
• Path Graph

Graph Traversal Algorithms

There are many applications for graphs, such as the GPS navigation system,
shortest path finding, peer to peer networks, crawlers in the search engine,
garbage collection (java), and even social networking websites.

Depending upon the problem under, the way we traverse a graph is important,
since it can affect the time in which we reach the goal. There are two basic
techniques used for graph traversal:

1. Breadth First Search (BFS)

2. Depth First Search (DFS)

In order to understand these algorithms, we will have to view graphs from a


slightly different perspective. Any traversal needs a starting point, but a graph
does not have a linear structure like lists or stacks. So how do we give the graph
traversal a better sense of direction? This is where the concept of levels is
introduced. Take any vertex as the starting point. This is the lowest level in your
search. The next level consists of all the vertices adjacent to your vertex.
Therefore, a level higher would consist of the vertices adjacent to these nodes.

You might also like