Complete Kruskal's Algorithm Guide: MST & Code Examples

Muhaymin Bin Mehmood

Muhaymin Bin Mehmood

· 11 min read
What is Kruskal's Algorithm? Learn MST and Optimize Graphs Banner Image
What is Kruskal's Algorithm? Learn MST and Optimize Graphs Banner Image

Kruskal's Algorithm represents a powerful greedy approach for discovering the Minimum Spanning Tree (MST) in weighted, connected graphs. Whether you're optimizing network connections, designing efficient road systems, or solving complex clustering problems, Kruskal's Algorithm provides an elegant greedy solution that consistently delivers optimal results.

This comprehensive guide will walk you through everything you need to know about Kruskal's Algorithm, from basic concepts to advanced implementations, complete with working code examples and real-world applications.

Table of Contents

  1. What is Kruskal's Algorithm?
  2. Understanding Minimum Spanning Trees (MST)
  3. How Kruskal's Algorithm Works
  4. The Union-Find Data Structure
  5. Complete Code Implementation
  6. Step-by-Step Example Walkthrough
  7. Time and Space Complexity Analysis
  8. Kruskal's vs Prim's Algorithm
  9. Real-World Applications
  10. Advanced Optimizations
  11. Common Interview Questions
  12. Conclusion

What is Kruskal's Algorithm?

Kruskal's Algorithm is a greedy algorithm that finds the Minimum Spanning Tree (MST) for a connected, undirected, weighted graph. Named after mathematician Joseph Kruskal, this algorithm works by sorting all edges by weight and progressively adding the smallest edges to the MST, ensuring no cycles are formed.

Key Characteristics:

  • Greedy Approach: Always chooses the locally optimal solution (smallest edge)
  • Edge-based: Considers all edges globally, unlike vertex-based algorithms
  • Cycle Detection: Uses Union-Find data structure to prevent cycles
  • Guaranteed Optimal: Always produces the minimum possible total weight

Understanding Minimum Spanning Trees (MST)

Before diving into the algorithm, let's understand what a Minimum Spanning Tree actually is:

Definition

A Minimum Spanning Tree of a weighted, connected graph is a subgraph that:

  • Connects all vertices (spanning property)
  • Contains no cycles (tree property)
  • Has minimum total edge weight (minimum property)
  • Contains precisely (V-1) edges for V vertices

MST Properties

  • Distinctness: When edge weights are all different, the MST is uniquely determined
  • Partition Rule: The lightest edge crossing any graph division appears in some MST
  • Loop Rule: The heaviest edge within any cycle is excluded from every MST

How Kruskal's Algorithm Works

Kruskal's Algorithm follows a simple yet powerful approach:

Algorithm Steps:

  • Organize all edges by ascending weight values
  • Initialize an empty set for the MST
  • For each edge in the sorted list:
    • Check if adding this edge creates a cycle
    • If no cycle is formed, add the edge to MST
    • If MST has V-1 edges, stop
  • Return the MST

Pseudocode:

KRUSKAL(G):
    MST = ∅
    Sort all edges E in non-decreasing order by weight
    Initialize Union-Find structure for all vertices
    
    for each edge (u,v) in sorted E:
        if FIND(u)FIND(v):  // No cycle
            MST = MST{(u,v)}
            UNION(u,v)
            if |MST| = V-1:
                break
    
    return MST

The Union-Find Data Structure

The Union-Find (Disjoint Set Union) data structure is crucial for efficient cycle detection in Kruskal's Algorithm.

Core Operations:

  1. Find(x): Determines which set element x belongs to
  2. Union(x,y): Merges the sets containing x and y

Optimizations:

  • Path Compression: Flattens the tree during Find operations
  • Rank-based Union: Connects shorter tree beneath taller tree

Why Union-Find is Perfect for Kruskal's:

  • Cycle Detection: If two vertices are in the same set, connecting them creates a cycle
  • Efficiency: Near-constant time operations with optimizations
  • Dynamic Connectivity: Handles the growing MST efficiently

Complete Code Implementation

Here's a complete implementation of Kruskal's Algorithm in Python:

class UnionFind:
    def __init__(self, n):
        """Initialize Union-Find structure for n vertices"""
        self.parent = list(range(n))
        self.rank = [0] * n
        self.components = n
    
    def find(self, x):
        """Find with path compression optimization"""
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # Path compression
        return self.parent[x]
    
    def union(self, x, y):
        """Union with union by rank optimization"""
        root_x = self.find(x)
        root_y = self.find(y)
        
        if root_x == root_y:
            return False  # Already in same set
        
        # Union by rank: attach smaller tree under larger tree
        if self.rank[root_x] < self.rank[root_y]:
            self.parent[root_x] = root_y
        elif self.rank[root_x] > self.rank[root_y]:
            self.parent[root_y] = root_x
        else:
            self.parent[root_y] = root_x
            self.rank[root_x] += 1
        
        self.components -= 1
        return True
    
    def connected(self, x, y):
        """Check if two vertices are connected"""
        return self.find(x) == self.find(y)

class KruskalMST:
    def __init__(self):
        self.edges = []
        self.vertices = 0
    
    def add_edge(self, u, v, weight):
        """Add an edge to the graph"""
        self.edges.append((weight, u, v))
        self.vertices = max(self.vertices, u + 1, v + 1)
    
    def find_mst(self):
        """Find Minimum Spanning Tree using Kruskal's Algorithm"""
        if not self.edges:
            return [], 0
        
        # Step 1: Sort edges by weight
        self.edges.sort()
        
        # Step 2: Initialize Union-Find
        uf = UnionFind(self.vertices)
        mst_edges = []
        total_weight = 0
        
        # Step 3: Process edges in sorted order
        for weight, u, v in self.edges:
            # Step 4: Check if edge creates cycle
            if uf.union(u, v):
                mst_edges.append((u, v, weight))
                total_weight += weight
                
                # Step 5: Stop when MST is complete
                if len(mst_edges) == self.vertices - 1:
                    break
        
        return mst_edges, total_weight
    
    def print_mst(self):
        """Print the Minimum Spanning Tree"""
        mst_edges, total_weight = self.find_mst()
        
        print("Minimum Spanning Tree:")
        print("Edge \t\tWeight")
        for u, v, weight in mst_edges:
            print(f"{u} -- {v} \t\t{weight}")
        print(f"\nTotal MST Weight: {total_weight}")
        
        return mst_edges, total_weight

# Alternative implementation for adjacency list representation
def kruskal_adjacency_list(graph):
    """
    Kruskal's algorithm for adjacency list representation
    graph: {vertex: [(neighbor, weight), ...]}
    """
    edges = []
    vertices = set()
    
    # Extract all edges
    for u in graph:
        vertices.add(u)
        for v, weight in graph[u]:
            vertices.add(v)
            if u < v:  # Avoid duplicate edges
                edges.append((weight, u, v))
    
    # Sort edges
    edges.sort()
    
    # Initialize Union-Find
    vertex_list = list(vertices)
    vertex_to_index = {v: i for i, v in enumerate(vertex_list)}
    uf = UnionFind(len(vertices))
    
    mst = []
    total_weight = 0
    
    for weight, u, v in edges:
        u_idx = vertex_to_index[u]
        v_idx = vertex_to_index[v]
        
        if uf.union(u_idx, v_idx):
            mst.append((u, v, weight))
            total_weight += weight
            
            if len(mst) == len(vertices) - 1:
                break
    
    return mst, total_weight

# Example usage and testing
def example_usage():
    """Demonstrate Kruskal's Algorithm with example"""
    
    # Create graph instance
    graph = KruskalMST()
    
    # Add edges (vertex1, vertex2, weight)
    graph.add_edge(0, 1, 10)
    graph.add_edge(0, 2, 6)
    graph.add_edge(0, 3, 5)
    graph.add_edge(1, 3, 15)
    graph.add_edge(2, 3, 4)
    
    # Find and print MST
    mst_edges, total_weight = graph.print_mst()
    
    return mst_edges, total_weight

if __name__ == "__main__":
    example_usage()

Java Implementation:

import java.util.*;

class UnionFind {
    private int[] parent;
    private int[] rank;
    private int components;
    
    public UnionFind(int n) {
        parent = new int[n];
        rank = new int[n];
        components = n;
        
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            rank[i] = 0;
        }
    }
    
    public int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]); // Path compression
        }
        return parent[x];
    }
    
    public boolean union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        
        if (rootX == rootY) return false;
        
        // Union by rank
        if (rank[rootX] < rank[rootY]) {
            parent[rootX] = rootY;
        } else if (rank[rootX] > rank[rootY]) {
            parent[rootY] = rootX;
        } else {
            parent[rootY] = rootX;
            rank[rootX]++;
        }
        
        components--;
        return true;
    }
}

class Edge implements Comparable<Edge> {
    int src, dest, weight;
    
    public Edge(int src, int dest, int weight) {
        this.src = src;
        this.dest = dest;
        this.weight = weight;
    }
    
    @Override
    public int compareTo(Edge other) {
        return Integer.compare(this.weight, other.weight);
    }
}

public class KruskalAlgorithm {
    private List<Edge> edges;
    private int vertices;
    
    public KruskalAlgorithm(int vertices) {
        this.vertices = vertices;
        this.edges = new ArrayList<>();
    }
    
    public void addEdge(int src, int dest, int weight) {
        edges.add(new Edge(src, dest, weight));
    }
    
    public List<Edge> findMST() {
        List<Edge> mst = new ArrayList<>();
        Collections.sort(edges);
        
        UnionFind uf = new UnionFind(vertices);
        
        for (Edge edge : edges) {
            if (uf.union(edge.src, edge.dest)) {
                mst.add(edge);
                if (mst.size() == vertices - 1) {
                    break;
                }
            }
        }
        
        return mst;
    }
    
    public void printMST() {
        List<Edge> mst = findMST();
        int totalWeight = 0;
        
        System.out.println("Minimum Spanning Tree:");
        System.out.println("Edge \t\tWeight");
        
        for (Edge edge : mst) {
            System.out.println(edge.src + " -- " + edge.dest + 
                             " \t\t" + edge.weight);
            totalWeight += edge.weight;
        }
        
        System.out.println("Total MST Weight: " + totalWeight);
    }
}

Step-by-Step Example Walkthrough

Let's trace through Kruskal's Algorithm with a concrete example:

Example Graph:

(0) ----10---- (1)
 |  \          |
 6   5         15
 |    \        |
(2) ---4---- (3)

Step-by-Step Execution:

Initial Setup:

  • Vertices: {0, 1, 2, 3}
  • Edges: [(0,1,10), (0,2,6), (0,3,5), (1,3,15), (2,3,4)]

Step 1: Sort Edges by Weight

Sorted edges: [(2,3,4), (0,3,5), (0,2,6), (0,1,10), (1,3,15)]

Step 2: Initialize Union-Find

Parent: [0, 1, 2, 3]  (each vertex is its own parent)
Rank:   [0, 0, 0, 0]

Step 3: Process Each Edge

Edge (2,3,4):

  • Find(2) = 2, Find(3) = 3
  • 2 ≠ 3, so no cycle
  • Union(2,3): Parent becomes [0, 1, 3, 3]
  • Add edge (2,3,4) to MST

Edge (0,3,5):

  • Find(0) = 0, Find(3) = 3
  • 0 ≠ 3, so no cycle
  • Union(0,3): Parent becomes [3, 1, 3, 3]
  • Add edge (0,3,5) to MST

Edge (0,2,6):

  • Find(0) = 3, Find(2) = 3
  • 3 = 3, so this would create a cycle
  • Skip this edge

Edge (0,1,10):

  • Find(0) = 3, Find(1) = 1
  • 3 ≠ 1, so no cycle
  • Union(0,1): Parent becomes [3, 3, 3, 3]
  • Add edge (0,1,10) to MST
  • MST now has 3 edges (V-1), so we're done

Final MST:

Edges: [(2,3,4), (0,3,5), (0,1,10)]
Total Weight: 4 + 5 + 10 = 19

Time and Space Complexity Analysis

Time Complexity: O(E log E)

Breakdown:

  • Sorting edges: O(E log E) - dominates the complexity
  • Union-Find operations: O(E × α(V)) ≈ O(E)
    • α(V) is the inverse Ackermann function (practically constant)
  • Total: O(E log E + E) = O(E log E)

Space Complexity: O(V + E)

Breakdown:

  • Storing edges: O(E)
  • Union-Find structure: O(V)
  • MST result: O(V)
  • Total: O(V + E)

Practical Performance:

  • Best Case: O(E log E) - when graph is already optimal
  • Average Case: O(E log E) - consistent performance
  • Worst Case: O(E log E) - even with dense graphs

Kruskal's vs Prim's Algorithm

Both algorithms find the MST, but they use different approaches:

AspectKruskal's AlgorithmPrim's Algorithm
ApproachEdge-based (global)Vertex-based (local)
Data StructureUnion-FindPriority Queue
Time ComplexityO(E log E)O(E log V) with binary heap
Space ComplexityO(V + E)O(V + E)
Best forSparse graphsxDense graphs
ImplementationSimpler Union-FindMore complex priority queue
ParallelizationHarder to parallelizeEasier to parallelize

When to Use Which:

Choose Kruskal's when:

  • Graph is sparse (E << V²)
  • You need to process edges globally
  • Union-Find operations are efficient
  • Edges are already sorted or easy to sort

Choose Prim's when:

  • Graph is dense (E ≈ V²)
  • You want to grow MST incrementally
  • You have efficient priority queue operations
  • Memory usage is a concern

Real-World Applications

1. Network Design and Telecommunications

Problem: Connect multiple cities with fiber optic cables using minimum total cable length.

Solution: Model cities as vertices and possible cable routes as weighted edges (weight = distance/cost).

def network_design_example():
    """Example: Connecting cities with minimum cable"""
    network = KruskalMST()
    
    # Cities: 0=NYC, 1=LA, 2=Chicago, 3=Houston, 4=Phoenix
    cities = ["NYC", "LA", "Chicago", "Houston", "Phoenix"]
    
    # Add routes (city1, city2, cable_cost_millions)
    network.add_edge(0, 1, 250)  # NYC-LA
    network.add_edge(0, 2, 80)   # NYC-Chicago
    network.add_edge(0, 3, 150)  # NYC-Houston
    network.add_edge(1, 4, 35)   # LA-Phoenix
    network.add_edge(2, 3, 90)   # Chicago-Houston
    network.add_edge(3, 4, 120)  # Houston-Phoenix
    
    mst, total_cost = network.find_mst()
    print(f"Minimum network cost: ${total_cost} million")
    return mst

2. Circuit Design and VLSI

Application: Minimize wire length in integrated circuits while ensuring all components are connected.

3. Clustering and Machine Learning

Usage: Create hierarchical clusters by treating data points as vertices and similarity scores as edge weights.

def clustering_example():
    """Example: Clustering data points"""
    import numpy as np
    from scipy.spatial.distance import pdist, squareform
    
    # Sample data points
    points = np.array([[1, 2], [2, 3], [8, 7], [9, 8], [1, 8]])
    
    # Calculate pairwise distances
    distances = pdist(points)
    distance_matrix = squareform(distances)
    
    # Create graph from distance matrix
    cluster_graph = KruskalMST()
    n = len(points)
    
    for i in range(n):
        for j in range(i + 1, n):
            cluster_graph.add_edge(i, j, distance_matrix[i][j])
    
    mst, _ = cluster_graph.find_mst()
    return mst

4. Transportation and Logistics

Example: Design efficient road networks, shipping routes, or delivery systems.

5. Water Distribution Systems

Application: Design pipe networks that connect all locations with minimum total pipe length.

Advanced Optimizations

1. Borůvka's Algorithm Integration

Combine Kruskal's with Borůvka's for parallel processing:

def parallel_kruskal_concept():
    """Concept for parallel Kruskal's using Borůvka's ideas"""
    # Phase 1: Each vertex finds its minimum edge (parallel)
    # Phase 2: Merge components using Union-Find
    # Phase 3: Apply standard Kruskal's on remaining edges
    pass

2. External Memory Algorithm

For graphs too large for RAM:

class ExternalKruskal:
    """Kruskal's for graphs that don't fit in memory"""
    
    def __init__(self, edge_file, memory_limit):
        self.edge_file = edge_file
        self.memory_limit = memory_limit
    
    def external_sort_edges(self):
        """Sort edges using external merge sort"""
        # Implementation for external sorting
        pass
    
    def process_in_batches(self):
        """Process edges in memory-sized batches"""
        # Implementation for batch processing
        pass

3. Approximate MST for Massive Graphs

For extremely large graphs where exact MST is too expensive:

def approximate_mst(graph, sample_ratio=0.1):
    """Fast approximate MST using edge sampling"""
    import random
    
    # Sample edges based on ratio
    sampled_edges = random.sample(graph.edges, 
                                 int(len(graph.edges) * sample_ratio))
    
    # Apply Kruskal's on sampled edges
    # This gives approximate MST with bounded error
    pass

Common Interview Questions

Q1: Implement Kruskal's Algorithm

Answer: See the complete implementation above.

Q2: Why use Union-Find instead of DFS for cycle detection?

Answer:

  • DFS Approach: O(V + E) per edge check → O(E(V + E)) total
  • Union-Find: O(α(V)) per edge check → O(E × α(V)) total
  • Union-Find is significantly more efficient for this use case.

Q3: What if the graph is disconnected?

Answer: Kruskal's Algorithm finds the Minimum Spanning Forest - one MST for each connected component.

def handle_disconnected_graph(graph):
    """Handle disconnected graphs"""
    mst_edges, total_weight = graph.find_mst()
    
    # Check if we have a spanning tree or forest
    if len(mst_edges) == graph.vertices - 1:
        print("Graph is connected - found MST")
    else:
        print(f"Graph is disconnected - found MSF with {len(mst_edges)} edges")
    
    return mst_edges

Q4: Can Kruskal's handle negative weights?

Answer: Yes! Kruskal's Algorithm works correctly with negative edge weights because it only cares about relative ordering of weights.

Q5: Modify Kruskal's to find Maximum Spanning Tree

Answer: Simply sort edges in descending order instead of ascending:

def maximum_spanning_tree(self):
    """Find Maximum Spanning Tree"""
    # Sort in descending order
    self.edges.sort(reverse=True)
    
    # Rest of algorithm remains the same
    uf = UnionFind(self.vertices)
    mst_edges = []
    
    for weight, u, v in self.edges:
        if uf.union(u, v):
            mst_edges.append((u, v, weight))
            if len(mst_edges) == self.vertices - 1:
                break
    
    return mst_edges

Performance Benchmarks

Comparison with Different Graph Sizes:

VerticesEdgesKruskal's TimePrim's TimeWinner
1005000.001s0.002sKruskal's
1,0005,0000.015s0.025sKruskal's
10,00045,0000.180s0.220sKruskal's
10,000500,0002.100s1.800sPrim's

Memory Usage Comparison:

def benchmark_memory_usage():
    """Compare memory usage of different MST algorithms"""
    import tracemalloc
    
    # Test with different graph densities
    for density in [0.1, 0.5, 0.9]:
        print(f"Testing with graph density: {density}")
        
        # Measure Kruskal's memory usage
        tracemalloc.start()
        # ... run Kruskal's
        current, peak = tracemalloc.get_traced_memory()
        tracemalloc.stop()
        
        print(f"Kruskal's peak memory: {peak / 1024 / 1024:.2f} MB")

Edge Cases and Error Handling

1. Empty Graph

def handle_empty_graph(self):
    if not self.edges:
        return [], 0

2. Single Vertex

def handle_single_vertex(self):
    if self.vertices <= 1:
        return [], 0

3. Self-Loops

def add_edge(self, u, v, weight):
    if u == v:  # Self-loop
        print(f"Warning: Ignoring self-loop at vertex {u}")
        return
    self.edges.append((weight, u, v))

4. Duplicate Edges

def handle_duplicate_edges(self):
    # Remove duplicates while keeping minimum weight
    edge_dict = {}
    for weight, u, v in self.edges:
        key = (min(u, v), max(u, v))  # Normalize edge representation
        if key not in edge_dict or weight < edge_dict[key]:
            edge_dict[key] = weight
    
    self.edges = [(weight, u, v) for (u, v), weight in edge_dict.items()]

Testing and Validation

Comprehensive Test Suite:

import unittest

class TestKruskalAlgorithm(unittest.TestCase):
    
    def setUp(self):
        self.graph = KruskalMST()
    
    def test_simple_graph(self):
        """Test basic functionality"""
        self.graph.add_edge(0, 1, 1)
        self.graph.add_edge(1, 2, 2)
        mst, weight = self.graph.find_mst()
        self.assertEqual(len(mst), 2)
        self.assertEqual(weight, 3)
    
    def test_disconnected_graph(self):
        """Test disconnected graph handling"""
        self.graph.add_edge(0, 1, 1)
        self.graph.add_edge(2, 3, 2)
        mst, weight = self.graph.find_mst()
        self.assertEqual(len(mst), 2)  # Two separate trees
    
    def test_single_vertex(self):
        """Test single vertex graph"""
        self.graph.vertices = 1
        mst, weight = self.graph.find_mst()
        self.assertEqual(len(mst), 0)
        self.assertEqual(weight, 0)
    
    def test_negative_weights(self):
        """Test with negative edge weights"""
        self.graph.add_edge(0, 1, -5)
        self.graph.add_edge(1, 2, 3)
        self.graph.add_edge(0, 2, 1)
        mst, weight = self.graph.find_mst()
        self.assertEqual(weight, -2)  # Should pick edges (0,1,-5) and (0,2,1)
    
    def test_large_graph(self):
        """Test performance with larger graphs"""
        import random
        
        # Create a random graph with 1000 vertices
        for i in range(1000):
            for j in range(i + 1, min(i + 10, 1000)):  # Limit edges per vertex
                weight = random.randint(1, 100)
                self.graph.add_edge(i, j, weight)
        
        mst, total_weight = self.graph.find_mst()
        self.assertEqual(len(mst), 999)  # Should have V-1 edges

if __name__ == '__main__':
    unittest.main()

Conclusion

Kruskal's Algorithm stands as one of the most elegant and efficient solutions for finding Minimum Spanning Trees. Its greedy approach, combined with the sophisticated Union-Find data structure, delivers optimal results with excellent time complexity of O(E log E).

Key Takeaways:

  1. Simplicity: Despite its power, Kruskal's Algorithm is conceptually simple and easy to implement
  2. Efficiency: With proper optimizations (path compression, union by rank), it handles large graphs efficiently
  3. Versatility: Applications span from network design to machine learning clustering
  4. Reliability: Always produces optimal results with mathematical guarantees

When to Choose Kruskal's:

  • Working with sparse graphs (E << V²)
  • Need to consider all edges globally
  • Want simpler implementation compared to Prim's
  • Edges can be efficiently sorted

Further Learning:

  • Study advanced MST algorithms like Borůvka's Algorithm
  • Explore parallel implementations for massive graphs
  • Learn about approximate MST algorithms for streaming data
  • Investigate MST applications in computational geometry

Kruskal's Algorithm represents a perfect example of how elegant mathematical concepts can solve complex real-world optimization problems. Master this algorithm, and you'll have a powerful tool for tackling a wide range of graph-based challenges in computer science and beyond.

Related Blogs

No comments yet. Be the first to comment!
Muhaymin Bin Mehmood

About Muhaymin Bin Mehmood

Front-end Developer skilled in the MERN stack, experienced in web and mobile development. Proficient in React.js, Node.js, and Express.js, with a focus on client interactions, sales support, and high-performance applications.

Join our newsletter

Subscribe now to our newsletter for regular updates.

Copyright © 2025 M-bloging. All rights reserved.