Introduction To Graph - Python
Introduction To Graph - Python
A graph is a set of nodes that are connected to each other in the form of a
network.
Vertex :
Edge :
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:
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
class Graph:
self.vertices = vertices
#Defining a list which can hold multiple LinkedLists
self.array = []
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.
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 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 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 :
Let’s break down the two new functions that we’ve implemented
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)
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.
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.
• Star Graph
• Acyclic Graph
• Path Graph
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: