Oct 17, 2024


Graphs are a fundamental data structure used to model pairwise relations between objects. They consist of vertices (or nodes) and edges (or arcs) that connect pairs of vertices. Graphs can be directed or undirected, weighted or unweighted, and can have cycles or be acyclic.

Types of Graph Representation

Adjacency Matrix:

     # Adjacency Matrix Representation
     graph = [
         [0, 1, 0, 0],
         [1, 0, 1, 1],
         [0, 1, 0, 1],
         [0, 1, 1, 0]
     ]

Weighted Adjacency Matrix:

       # Weighted Adjacency Matrix Representation
       graph = [
           [0, 2, 0, 0],
           [2, 0, 3, 1],
           [0, 3, 0, 4],
           [0, 1, 4, 0]
       ]

Adjacency List:

     # Adjacency List Representation
     graph = {
         0: [1],
         1: [0, 2, 3],
         2: [1, 3],
         3: [1, 2]
     }

Weighted Adjacency List:

     # Weighted Adjacency List Representation
     graph = {
         0: [(1, 2)],
         1: [(0, 2), (2, 3), (3, 1)],
         2: [(1, 3), (3, 4)],
         3: [(1, 1), (2, 4)]
     }

Edge List:

     # Edge List Representation
     edges = [
         (0, 1),
         (1, 2),
         (1, 3),
         (2, 3)
     ]

Weighted Edge List:

     # Weighted Edge List Representation
     edges = [
         (0, 1, 2),
         (1, 2, 3),
         (1, 3, 1),
         (2, 3, 4)
     ]

Pros and Cons of Different Graph Representations

Adjacency Matrix

Adjacency List

Edge List

Each representation has its own strengths and weaknesses, and the choice of which to use depends on the specific requirements of the problem at hand, such as the density of the graph and the types of operations that need to be performed.

Coding Interview Questions

In the questions below we assume that the graph is represented as an adjacency list. Example:

graph = {
    0: [1],
    1: [0, 2, 3],
    2: [1, 3],
    3: [1, 2]
}

Graph Traversal:

     def dfs(graph, start, visited=None):
         if visited is None:
             visited = set()
         visited.add(start)
         for next in graph[start]:
             if next not in visited:
                 dfs(graph, next, visited)
         return visited

     def bfs(graph, start):
         visited = set()
         queue = [start]
         while queue:
             vertex = queue.pop(0)
             if vertex not in visited:
                 visited.add(vertex)
                 queue.extend(set(graph[vertex]) - visited)
         return visited

Cycle Detection for Directional Graph:

     def has_cycle(graph):
         def visit(vertex, visited, rec_stack):
             visited.add(vertex)
             rec_stack.add(vertex)
             for neighbor in graph[vertex]:
                 if neighbor not in visited:
                     if visit(neighbor, visited, rec_stack):
                         return True
                 elif neighbor in rec_stack:
                     return True
             rec_stack.remove(vertex)
             return False

         visited = set()
         rec_stack = set()
         for node in graph:
             if node not in visited:
                 if visit(node, visited, rec_stack):
                     return True
         return False

Cycle Detection for Bidirectional Graph:

     def has_cycle_bidirectional(graph):
         def visit(vertex, visited, parent):
             visited.add(vertex)
             for neighbor in graph[vertex]:
                 if neighbor not in visited:
                     if visit(neighbor, visited, vertex):
                         return True
                 elif parent is not None and neighbor != parent:
                     return True
             return False

         visited = set()
         for node in graph:
             if node not in visited:
                 if visit(node, visited, None):
                     return True
         return False

This solution uses a DFS approach similar to the unidirectional graph but includes an additional parent parameter to track the node from which we arrived at the current node. This prevents the algorithm from mistakenly identifying the bidirectional nature of edges as cycles.

Shortest Path for Unweighted Graph:

     from collections import deque

     def shortest_path(graph, start, goal):
         queue = deque([(start, [start])])
         while queue:
             (vertex, path) = queue.popleft()
             for next in graph[vertex] - set(path):
                 if next == goal:
                     return path + [next]
                 else:
                     queue.append((next, path + [next]))
         return None

Shortest Path for Weighted Graph:

     import heapq

     def dijkstra(graph, start, goal):
         # Priority queue to store (distance, vertex) tuples
         queue = [(0, start)]
         # Dictionary to store the shortest path to each node
         distances = {start: 0}
         # Dictionary to store the path taken to reach each node
         previous_nodes = {start: None}

         while queue:
             current_distance, current_vertex = heapq.heappop(queue)

             # If we reach the goal, reconstruct the path
             if current_vertex == goal:
                 path = []
                 while current_vertex is not None:
                     path.append(current_vertex)
                     current_vertex = previous_nodes[current_vertex]
                 return path[::-1]

             # Explore neighbors
             for neighbor, weight in graph[current_vertex].items():
                 distance = current_distance + weight

                 # If a shorter path to the neighbor is found
                 if neighbor not in distances or distance < distances[neighbor]:
                     distances[neighbor] = distance
                     previous_nodes[neighbor] = current_vertex
                     heapq.heappush(queue, (distance, neighbor))

         return None

This solution uses Dijkstra's algorithm to efficiently find the shortest path in a weighted graph. It leverages a priority queue to always expand the least costly node first, ensuring that the shortest path is found. The algorithm also keeps track of the path taken to reconstruct the shortest path once the goal node is reached.


Ming Dao School uses 1-1 coaching and group events to help high-tech professionals grow their careers and handle career transitions.

If you like to join our upcoming mock system design interview events or other coaching programs, please contact us on LinkedIn.