Graphs Fundamentals
Introduction
Graphs are one of the most versatile and powerful data structures in computer science. They allow us to represent relationships between objects in a flexible way that's applicable to countless real-world problems.
A graph consists of:
- Vertices (also called nodes): These are the objects or entities in our graph
- Edges: These represent the connections or relationships between vertices
Unlike trees (which are actually a special type of graph), graphs can have cycles, multiple paths between nodes, and complex connection patterns that make them ideal for modeling many real-world scenarios.
Types of Graphs
Graphs come in several varieties, each with unique properties:
Directed vs. Undirected Graphs
- Undirected Graph: Edges have no direction. If vertex A is connected to vertex B, then B is also connected to A.
- Directed Graph (Digraph): Edges have direction. If vertex A points to vertex B, it doesn't necessarily mean B points back to A.
Weighted vs. Unweighted Graphs
- Unweighted Graph: All edges have the same importance or "weight"
- Weighted Graph: Edges have a value (weight) assigned to them, representing distance, cost, etc.
Other Types
- Complete Graph: Every vertex is connected to every other vertex
- Sparse Graph: Few connections between vertices
- Dense Graph: Many connections between vertices
- Cyclic Graph: Contains at least one cycle (a path that starts and ends at the same vertex)
- Acyclic Graph: Contains no cycles
Graph Representations
There are two primary ways to represent a graph in code:
Adjacency Matrix
An adjacency matrix is a 2D array where each cell [i][j]
represents whether there's an edge from vertex i
to vertex j
.
// Adjacency Matrix representation for an undirected graph
// 1 indicates an edge, 0 indicates no edge
const adjacencyMatrix = [
// A B C D
[0, 1, 0, 1], // A's connections
[1, 0, 1, 0], // B's connections
[0, 1, 0, 1], // C's connections
[1, 0, 1, 0] // D's connections
];
Pros:
- Simple representation
- Edge lookup is O(1)
- Good for dense graphs
Cons:
- Space inefficient (O(V²)) regardless of how many edges exist
- Iterating over edges from a vertex is O(V) even if there are few edges
Adjacency List
An adjacency list represents each vertex with a list of its adjacent vertices.
// Adjacency List representation
const adjacencyList = {
'A': ['B', 'D'],
'B': ['A', 'C'],
'C': ['B', 'D'],
'D': ['A', 'C']
};
Pros:
- Space efficient for sparse graphs (O(V+E))
- Iterating over edges from a vertex is efficient
Cons:
- Edge lookup is O(degree(v)) which can be slower
- Less efficient for dense graphs
Basic Graph Operations
Let's implement some fundamental operations on graphs:
Creating a Graph
Here's a simple class representing a graph using an adjacency list:
class Graph {
constructor(directed = false) {
this.directed = directed;
this.adjacencyList = {};
}
addVertex(vertex) {
if (!this.adjacencyList[vertex]) {
this.adjacencyList[vertex] = [];
}
}
addEdge(vertex1, vertex2, weight) {
// Add edge from vertex1 to vertex2
if (!this.adjacencyList[vertex1]) {
this.addVertex(vertex1);
}
if (!this.adjacencyList[vertex2]) {
this.addVertex(vertex2);
}
this.adjacencyList[vertex1].push({ node: vertex2, weight: weight || 1 });
// If undirected, add the reverse edge
if (!this.directed) {
this.adjacencyList[vertex2].push({ node: vertex1, weight: weight || 1 });
}
}
removeEdge(vertex1, vertex2) {
this.adjacencyList[vertex1] = this.adjacencyList[vertex1].filter(
edge => edge.node !== vertex2
);
if (!this.directed) {
this.adjacencyList[vertex2] = this.adjacencyList[vertex2].filter(
edge => edge.node !== vertex1
);
}
}
removeVertex(vertex) {
// Remove all edges connected to this vertex
while (this.adjacencyList[vertex].length) {
const adjacentVertex = this.adjacencyList[vertex].pop().node;
this.removeEdge(vertex, adjacentVertex);
}
// Delete the vertex
delete this.adjacencyList[vertex];
}
// Display the adjacency list
print() {
for (let vertex in this.adjacencyList) {
console.log(vertex + " -> " + this.adjacencyList[vertex].map(edge => `${edge.node}(${edge.weight})`).join(", "));
}
}
}
Example Usage
// Create a new undirected graph
const graph = new Graph();
// Add vertices
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addVertex("D");
// Add edges
graph.addEdge("A", "B", 4);
graph.addEdge("A", "C", 2);
graph.addEdge("B", "C", 3);
graph.addEdge("C", "D", 1);
// Display the graph
graph.print();
/* Output:
A -> B(4), C(2)
B -> A(4), C(3)
C -> A(2), B(3), D(1)
D -> C(1)
*/
// Remove a vertex
graph.removeVertex("C");
graph.print();
/* Output:
A -> B(4)
B -> A(4)
D ->
*/
Graph Traversal Algorithms
Traversal means visiting all vertices in a graph in a specific order. The two main approaches are:
Depth-First Search (DFS)
DFS explores as far as possible along each branch before backtracking. It uses a stack (often implemented using recursion).
class Graph {
// ... previous methods
depthFirstSearch(start) {
const result = [];
const visited = {};
const adjacencyList = this.adjacencyList;
(function dfs(vertex) {
if (!vertex) return null;
visited[vertex] = true;
result.push(vertex);
adjacencyList[vertex].forEach(neighbor => {
if (!visited[neighbor.node]) {
return dfs(neighbor.node);
}
});
})(start);
return result;
}
}
// Example usage:
const graph = new Graph();
// ... add vertices and edges
console.log(graph.depthFirstSearch("A"));
// Output: ["A", "B", "C", "D"] (exact order depends on your graph structure)
Breadth-First Search (BFS)
BFS explores all neighbors at the present depth before moving to nodes at the next depth level. It uses a queue.
class Graph {
// ... previous methods
breadthFirstSearch(start) {
const queue = [start];
const result = [];
const visited = {};
visited[start] = true;
while (queue.length) {
const currentVertex = queue.shift();
result.push(currentVertex);
this.adjacencyList[currentVertex].forEach(neighbor => {
if (!visited[neighbor.node]) {
visited[neighbor.node] = true;
queue.push(neighbor.node);
}
});
}
return result;
}
}
// Example usage:
const graph = new Graph();
// ... add vertices and edges
console.log(graph.breadthFirstSearch("A"));
// Output: ["A", "B", "C", "D"] (exact order depends on your graph structure)
Real-World Applications
Graphs are incredibly versatile and are used in numerous real-world scenarios:
-
Social Networks
- Each person is a vertex
- Friendships or connections are edges
- Facebook's "People You May Know" uses graph algorithms
-
Navigation and Maps
- Locations are vertices
- Roads are edges
- Weights represent distances or travel times
- GPS navigation uses graph algorithms like Dijkstra's or A* for shortest paths
-
Web Page Ranking
- Web pages are vertices
- Hyperlinks are edges
- Google's PageRank algorithm uses graph properties to rank pages
-
Recommendation Systems
- Products/movies/songs are vertices
- Similar items or user preferences form edges
- Netflix, Spotify, and Amazon use graph algorithms for recommendations
-
Network Routing
- Routers are vertices
- Connections are edges
- Routing algorithms find efficient paths for data packets
Example: Finding Routes in a City
Imagine a simple city map represented as a graph:
const cityMap = new Graph();
// Add locations
cityMap.addVertex("Home");
cityMap.addVertex("Grocery");
cityMap.addVertex("Park");
cityMap.addVertex("Work");
cityMap.addVertex("School");
cityMap.addVertex("Mall");
// Add roads with distances in kilometers
cityMap.addEdge("Home", "Grocery", 3);
cityMap.addEdge("Home", "Park", 2);
cityMap.addEdge("Grocery", "Work", 4);
cityMap.addEdge("Park", "School", 1);
cityMap.addEdge("School", "Mall", 5);
cityMap.addEdge("Work", "Mall", 3);
cityMap.addEdge("Park", "Work", 5);
// Find all reachable locations from Home using BFS
console.log(cityMap.breadthFirstSearch("Home"));
// Output: ["Home", "Grocery", "Park", "Work", "School", "Mall"]
Advanced Graph Algorithms
While beyond the scope of this introduction, there are many important graph algorithms to explore as you advance:
- Dijkstra's Algorithm: Finding shortest paths in weighted graphs
- Bellman-Ford Algorithm: Finding shortest paths with negative weights
- Floyd-Warshall Algorithm: Finding all-pairs shortest paths
- Kruskal's & Prim's Algorithms: Finding minimum spanning trees
- Topological Sort: Ordering vertices in a directed acyclic graph
Summary
Graphs are powerful data structures that model relationships between objects. Key points covered:
- Graphs consist of vertices (nodes) and edges (connections)
- Graphs can be directed/undirected and weighted/unweighted
- They can be represented using adjacency matrices or adjacency lists
- Basic operations include adding/removing vertices and edges
- Traversal algorithms include DFS (depth-first search) and BFS (breadth-first search)
- Graphs have countless real-world applications
Practice Exercises
- Implement a function to detect whether a graph contains a cycle
- Create a function to find all possible paths between two vertices
- Implement Dijkstra's algorithm to find the shortest path between two vertices
- Create a social network graph and find "friends of friends" for a given user
- Implement a function to check if a graph is bipartite (can be divided into two groups where no edges connect nodes in the same group)
Additional Resources
-
Books:
- "Introduction to Algorithms" by Cormen, Leiserson, Rivest, and Stein
- "Algorithms" by Robert Sedgewick and Kevin Wayne
-
Online Courses:
- "Algorithms, Part II" on Coursera by Princeton University
- "Data Structures and Algorithms" specialization on Coursera
-
Websites:
- LeetCode's graph problems section
- Visualgo.net for algorithm visualization
Graph algorithms are fundamental to computer science and essential for tackling complex problems efficiently. Keep practicing with different graph problems to build your intuition and understanding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)