Skip to content

Graph

A graph is a non-linear data structure consisting of a set of nodes (also called vertices) and a set of edges that connect pairs of nodes. Graphs are used to model relationships between objects. An edge can be directed (indicating a one-way relationship) or undirected (indicating a two-way relationship). A graph with directed edges is called a directed graph or digraph, while a graph with undirected edges is called an undirected graph.

Graphs are important because they can represent a wide variety of real-world problems, including:

  • Social networks: Nodes represent people, and edges represent relationships.
  • Road networks: Nodes represent intersections, and edges represent roads.
  • Computer networks: Nodes represent computers, and edges represent connections.
  • Dependency graphs: Nodes represent tasks, and edges represent dependencies between tasks.
  • State transition diagrams: Nodes represent states, and edges represent transitions between states.

Core Concepts and Terminology:

  • Node (Vertex): A data point in the graph.
  • Edge: A connection between two nodes.
  • Directed Edge: An edge with a direction, indicating a one-way relationship.
  • Undirected Edge: An edge without a direction, indicating a two-way relationship.
  • Weighted Graph: A graph where each edge has an associated weight (e.g., distance, cost).
  • Unweighted Graph: A graph where edges have no associated weight.
  • Adjacent Nodes: Two nodes connected by an edge.
  • Path: A sequence of nodes connected by edges.
  • Cycle: A path that starts and ends at the same node.
  • Connected Graph: A graph where there is a path between any two nodes.
  • Disconnected Graph: A graph where there are nodes that are not reachable from each other.
  • Tree: A connected graph with no cycles.
  • Adjacency Matrix: A 2D array representing the graph where matrix[i][j] is 1 if there’s an edge from node i to node j, and 0 otherwise.
  • Adjacency List: A list of lists where each inner list contains the neighbors of a node.

Use a graph when:

  • You need to represent relationships between objects.
  • You need to find paths between objects.
  • You need to determine connectivity between objects.
  • You need to model networks or systems.

Don’t use a graph when:

  • The relationships are simple and can be represented by other data structures (e.g., arrays, trees).
  • The problem can be solved more efficiently using a different algorithm.

Many graph algorithms follow a similar template:

  1. Representation: Choose a suitable representation (adjacency matrix or adjacency list).
  2. Initialization: Initialize data structures (e.g., visited array for traversal algorithms).
  3. Iteration: Iterate through the nodes and edges using a suitable traversal technique (e.g., Breadth-First Search (BFS), Depth-First Search (DFS)).
  4. Processing: Perform operations on nodes and edges during traversal (e.g., counting paths, finding shortest paths).
  5. Result: Return the result based on the processed data.

4. Code Implementations (Python, Java, C++)

Section titled “4. Code Implementations (Python, Java, C++)”

This example demonstrates Breadth-First Search (BFS) using an adjacency list.

from collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
visited.add(start)
while queue:
vertex = queue.popleft()
print(vertex, end=" ")
for neighbor in graph[vertex]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
# Example graph represented as an adjacency list
graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
}
bfs(graph, 'A') # Output: A B C D E F
import java.util.*;
public class BFS {
public static void bfs(Map<Character, List<Character>> graph, char start) {
Set<Character> visited = new HashSet<>();
Queue<Character> queue = new LinkedList<>();
queue.offer(start);
visited.add(start);
while (!queue.isEmpty()) {
char vertex = queue.poll();
System.out.print(vertex + " ");
for (char neighbor : graph.get(vertex)) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
}
public static void main(String[] args) {
Map<Character, List<Character>> graph = new HashMap<>();
graph.put('A', Arrays.asList('B', 'C'));
graph.put('B', Arrays.asList('D', 'E'));
graph.put('C', Arrays.asList('F'));
graph.put('D', new ArrayList<>());
graph.put('E', Arrays.asList('F'));
graph.put('F', new ArrayList<>());
bfs(graph, 'A'); // Output: A B C D E F
}
}
#include <iostream>
#include <vector>
#include <queue>
#include <unordered_set>
using namespace std;
void bfs(const unordered_map<char, vector<char>>& graph, char start) {
unordered_set<char> visited;
queue<char> q;
q.push(start);
visited.insert(start);
while (!q.empty()) {
char vertex = q.front();
q.pop();
cout << vertex << " ";
for (char neighbor : graph.at(vertex)) {
if (visited.find(neighbor) == visited.end()) {
visited.insert(neighbor);
q.push(neighbor);
}
}
}
}
int main() {
unordered_map<char, vector<char>> graph = {
{'A', {'B', 'C'}},
{'B', {'D', 'E'}},
{'C', {'F'}},
{'D', {}},
{'E', {'F'}},
{'F', {}}
};
bfs(graph, 'A'); // Output: A B C D E F
cout << endl;
return 0;
}

The complexity of graph algorithms depends heavily on the representation and the specific algorithm.

AlgorithmTime Complexity (Best)Time Complexity (Average)Time Complexity (Worst)Space Complexity
BFSO(V + E)O(V + E)O(V + E)O(V + E)
DFSO(V + E)O(V + E)O(V + E)O(V + E)
Dijkstra’sO(V + E)O(E log V)O(E log V)O(V + E)
Bellman-FordO(V + E)O(VE)O(VE)O(V)
Adjacency MatrixSpace: O(V^2)
Adjacency ListSpace: O(V + E)

Where:

  • V = number of vertices
  • E = number of edges
  • Choosing the right representation: Adjacency lists are generally preferred for sparse graphs (graphs with relatively few edges), while adjacency matrices are better for dense graphs.
  • Handling cycles: Be mindful of cycles when implementing graph traversal algorithms. Use a visited set to avoid infinite loops.
  • Avoiding infinite loops: Always ensure your traversal algorithms have a termination condition.
  • Weighted vs. Unweighted: Choose the appropriate algorithm based on whether your graph is weighted or unweighted. Dijkstra’s algorithm is for weighted graphs, while BFS is for unweighted graphs.
  • Disconnected Graphs: For disconnected graphs, you might need to run traversal algorithms multiple times, starting from each unvisited node.

Description: Given a reference of a node in a connected undirected graph. Return a deep copy (clone) of the graph. Each node in the graph contains a value (int) and a list (List[Node]) of its neighbors.

High-Level Approach:

Use Breadth-First Search (BFS) or Depth-First Search (DFS) to traverse the original graph. As you visit each node, create a corresponding node in the cloned graph. Maintain a map to track the mapping between nodes in the original graph and their clones in the new graph. When processing neighbors, use this map to connect the cloned nodes correctly. This ensures a deep copy where the structure and relationships are entirely replicated. Handle potential cycles by checking the map before creating new nodes.

This cheatsheet provides a foundational understanding of graphs. Further exploration into specific graph algorithms (like Dijkstra’s, Bellman-Ford, Minimum Spanning Tree algorithms) is recommended for deeper understanding and practical application.