AlgoDaily

Kruskal's Algorithm: Navigating Minimum Spanning Trees

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Introduction

Kruskal's Algorithm: Your Coding Interview Compass for Navigating Minimum Spanning Trees

Welcome to the fascinating realm of graph algorithms! Today, we're diving into Kruskal's Algorithm—a topic that often surfaces in coding interviews. Why? Because it's like a Swiss Army knife for showcasing your mastery of computer science basics.

Introduction: Why Kruskal's Algorithm?
Your Ticket to a Minimum Spanning Tree

Imagine you're an architect, and you need to build a network of roads that connects several cities but with the minimum amount of asphalt. Kruskal's algorithm helps you do just that, but for graphs!

  • How: It calculates the minimum spanning tree (MST) of a connected, weighted graph.
  • Why: The MST is the subset of edges that connects all vertices with the smallest total weight.

A Coding Interview Goldmine

Picture Kruskal's algorithm as a multi-level video game that tests various skills.

  • How: Implementing it involves grappling with graphs, greedy algorithms, union-find data structures, and time complexity.
  • Why: Navigating through these challenges proves you're well-versed in fundamental computer science concepts, making you a desirable candidate.
Background: Setting the Stage

What is a Minimum Spanning Tree?

Think of an MST as a power grid that connects all houses in a neighborhood with the least amount of wire.

  • How: An MST is a tree extracted from a connected, weighted graph, where the sum of edge weights is minimized.
  • Why: It ensures all vertices are connected without any cycles, optimizing the total cost.
The Real-World Magic of MSTs

Imagine you're tasked with laying down internet cables across a city or designing a circuit board. Here's where MSTs come to the rescue!

  • How: MSTs have a wide range of applications in networks, circuit design, and facility location.
  • Why: They allow you to connect various nodes (like cities or circuit components) while minimizing cost or resource usage.

So, ready to learn how to implement Kruskal's algorithm and ace your coding interviews? Let's roll up our sleeves and get started!

Fill In the Blank

An MST is a tree extracted from a connected, weighted graph, where the sum of edge weights is _________.

Overview to Implementation

Kruskal's Algorithm: A Hands-On Guide with Visuals

Ready to dig into Kruskal's Algorithm? Let's break it down, from the big picture to the nitty-gritty, with code snippets and ASCII diagrams to help visualize each step.

Quick Recap: The Three Pillars of Kruskal's Algorithm
Core Principles
  • Empty Canvas: Start with no edges, just vertices.
  • Bargain Hunt: Add edges by ascending weight.
  • No Loops: Avoid cycles.
Why It Works
  • Strategy: Greedy approach—grab the cheapest edge.
  • Outcome: A Minimum Spanning Tree (MST) without cycles.
Step-By-Step: Building the MST

Let's use a sample graph for this walkthrough. Here's our original graph:

  A--2--B
  |     |
  4     5
  |     |
  D--3--C
1. Sort Edges by Weight

First, list edges in ascending order by weight.

edges = [(2, 'A', 'B'), (3, 'C', 'D'), (4, 'A', 'D'), (5, 'B', 'C')]
edges.sort()
ArrayList<Edge> edges = new ArrayList<Edge>();
edges.add(new Edge(2, "A", "B"));
edges.add(new Edge(3, "C", "D"));
edges.add(new Edge(4, "A", "D"));
edges.add(new Edge(5, "B", "C"));
Collections.sort(edges);
let edges = [['2', 'A', 'B'], ['3', 'C', 'D'], ['4', 'A', 'D'], ['5', 'B', 'C']];
edges.sort();
vector<tuple<int, string, string>> edges {
  {2, "A", "B"},
  {3, "C", "D"},
  {4, "A", "D"},
  {5, "B", "C"},
};
sort(edges.begin(), edges.end());
List<Tuple<int, string, string>> edges = new List<Tuple<int, string, string>>(){
  Tuple.Create(2, "A", "B"),
  Tuple.Create(3, "C", "D"),
  Tuple.Create(4, "A", "D"),
  Tuple.Create(5, "B", "C"),
};
edges.Sort();
type Edge struct {
    weight int
    start, end string
}
edges := []Edge{{2, "A", "B"}, {3, "C", "D"}, {4, "A", "D"}, {5, "B", "C"}}
sort.Slice(edges, func(i, j int) bool {return edges[i].weight < edges[j].weight})
2. Add Edges While Avoiding Cycles

Go through the sorted list and add edges to the MST, skipping any that would create a cycle.

mst = set()

# For each edge in sorted list
for weight, u, v in edges:
    # Check for cycle and add if safe
Set<Integer> mst = new HashSet<>();

// For each edge in sorted list
for (Edge edge : edges) {
    int weight = edge.getWeight();
    int u = edge.getU();
    int v = edge.getV();
    // Check for cycle and add if safe
}
let mst = new Set();

// For each edge in sorted list
for (let edge of edges) {
    let weight = edge.weight;
    let u = edge.u;
    let v = edge.v;
    // Check for cycle and add if safe
}
std::set<int> mst;

// For each edge in sorted list
for(auto &edge : edges){
    int weight = edge.weight;
    int u = edge.u;
    int v = edge.v;
    // Check for cycle and add if safe
}
HashSet<int> mst = new HashSet<int>();

// For each edge in sorted list
foreach (Edge edge in edges)
{
    int weight = edge.Weight;
    int u = edge.U;
    int v = edge.V;
    // Check for cycle and add if safe
}
mst := map[int]struct{}{}

// For each edge in sorted list
for _, edge := range edges {
    weight := edge.Weight
    u := edge.U
    v := edge.V
    // Check for cycle and add if safe
}

For example:

  • Add edge A-B (weight 2). MST becomes:
  A--B
  • Next, add edge C-D (weight 3). MST becomes:
  A--B

  C--D
  • Add A-D: This edge will connect the two separate trees and does not form a cycle.
  A--B
  |
  D--C
  • Skip B-C: This would create a cycle, so it's skipped.
3. Terminate When MST is Complete

Stop once the MST has ( V - 1 ) edges.

if len(mst) == len(vertices) - 1:
    break
if (mst.size() == vertices.size() - 1) {
    break;
}
if (mst.length === vertices.length - 1) {
    break;
}
if (mst.size() == vertices.size() - 1) {
    break;
}
if (mst.Count == vertices.Count - 1) {
    break;
}
if len(mst) == len(vertices) - 1 {
    break
}
MST In Action: Seeing the Pieces Come Together

As you add each edge, the MST takes shape:

  • After adding A-B:
    text A--B

  • After adding C-D:
    ```text A--B

C--D ```

  • After adding B-C:
    text A--B | D--C

With these steps, you've built an MST from scratch.

Implementing Kruskal's Algorithm

Implementing Kruskal's Algorithm: A Practical Guide

Let's start by discussing the major components you'll need.

Harness the Power of Union-Find

Think of the union-find data structure as a network of social circles, helping us keep track of connected components.

  • How: Maintain disjoint sets to verify whether vertices are connected.
  • Why: This enables us to quickly determine whether adding an edge will form a cycle.
# Union-Find initialization
parent = {vertex: vertex for vertex in vertices}

def find(vertex):
    if parent[vertex] != vertex:
        parent[vertex] = find(parent[vertex])
    return parent[vertex]

def union(u, v):
    root_u = find(u)
    root_v = find(v)
    if root_u != root_v:
        parent[root_u] = root_v
// Union-Find initialization
HashMap<Character, Character> parent = new HashMap<>();
for (char vertex : vertices) {
    parent.put(vertex, vertex);
}

char find(char vertex) {
    if (parent.get(vertex) != vertex) {
        parent.put(vertex, find(parent.get(vertex)));
    }
    return parent.get(vertex);
}

void union(char u, char v) {
    char root_u = find(u);
    char root_v = find(v);

    if (root_u != root_v) {
        parent.put(root_u, root_v);
    }
}
// Union-Find initialization
let parent = {}
for(let vertex of vertices) {
    parent[vertex] = vertex;
}

function find(vertex) {
    if (parent[vertex] !== vertex) {
        parent[vertex] = find(parent[vertex]);
    }
    return parent[vertex];
}

function union(u, v) {
    let root_u = find(u);
    let root_v = find(v);

    if (root_u !== root_v) {
        parent[root_u] = root_v;
    }
}
// Union-Find initialization
std::map<char, char> parent;
for (char vertex : vertices) {
    parent[vertex] = vertex;
}

char find(char vertex) {
    if (parent[vertex] != vertex) {
        parent[vertex] = find(parent[vertex]);
    }
    return parent[vertex];
}

void union(char u, char v) {
    char root_u = find(u);
    char root_v = find(v);

    if (root_u != root_v) {
        parent[root_u] = root_v;
    }
}
// Union-Find initialization
Dictionary<char, char> parent = new Dictionary<char, char>();
foreach (char vertex in vertices)
{
    parent[vertex] = vertex;
}

char find(char vertex)
{
    if (parent[vertex] != vertex)
    {
        parent[vertex] = find(parent[vertex]);
    }
    return parent[vertex];
}

void union(char u, char v)
{
    char root_u = find(u);
    char root_v = find(v);
    if (root_u != root_v)
    {
        parent[root_u] = root_v;
    }
}
// Union-Find initialization
parent := make(map[rune]rune)
for _, vertex := range vertices {
    parent[vertex] = vertex
}

func find(vertex rune) rune {
    if parent[vertex] != vertex {
        parent[vertex] = find(parent[vertex])
    }
    return parent[vertex]
}

func union(u, v rune) {
    root_u := find(u)
    root_v := find(v)

    if root_u != root_v {
        parent[root_u] = root_v
    }
}
Storing Edges: The Priority Queue Way

Imagine a priority queue as your shopping basket, always giving you the cheapest item first.

  • How: Store edges in a priority queue, sorted by weight.
  • Why: This ensures we always pick the edge with the lowest weight.
import heapq

# Initialize the priority queue with edges
priority_queue = [(weight, u, v) for u, v, weight in edges]
heapq.heapify(priority_queue)
import java.util.PriorityQueue;

// Initialize the priority queue with edges
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>((a, b) -> a.weight - b.weight);
for(int i = 0; i < edges.size(); i++){
    priorityQueue.add(edges.get(i));
}
let Edge = require('collections/edge');

// Initialize the priority queue with edges
let priorityQueue = new PriorityQueue(function(a, b) {
  return a.weight - b.weight;
});
edges.forEach(edge => {
  priorityQueue.add(edge);
});
#include <queue>
#include <vector>

// Initialize priority_queue with edges
std::priority_queue<Edge, std::vector<Edge>, compare> priority_queue;
for(Edge edge: edges) priority_queue.push(edge);
using System.Collections.Generic;

// Initialize the priority queue with edges
var priorityQueue = new PriorityQueue<Edge>();
foreach (var edge in edges)
{
    priorityQueue.Add(edge);
}
import "container/heap"

// Edge is a struct of an edge
type Edge struct {
    u, v, weight int
}

// Priority Queue
type PriorityQueue []*Edge

// Set up functions for heap interface
func (pq PriorityQueue) Len() int { return len(pq) }

func (pq PriorityQueue) Less(i, j int) bool {
    return pq[i].weight < pq[j].weight
}

func (pq PriorityQueue) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
}

func (pq *PriorityQueue) Push(x interface{}) {
    item := x.(*Edge)
    *pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    old[n-1] = nil  // avoid memory leak
    *pq = old[0 : n-1]
    return item
}

// Initialize the priority queue with edges
var pq PriorityQueue
heap.Init(&pq)
for _, edge := range edges {
    heap.Push(&pq, edge)
}
The Edge-Adding Logic

It's like solving a puzzle; you only fit in the pieces that don't disrupt the existing picture.

  • How: Check if adding an edge connects two different trees in the forest of disjoint sets.
  • Why: This prevents cycles while ensuring connectivity.
mst = set()

while priority_queue:
    weight, u, v = heapq.heappop(priority_queue)
    if find(u) != find(v):
        mst.add((u, v, weight))
        union(u, v)
Set<Tuple> mst = new HashSet<>();

while (!priorityQueue.isEmpty()) {
    Tuple tuple = priorityQueue.poll();
    int weight = tuple.getWeight();
    String u = tuple.getU();
    String v = tuple.getV();
    if (find(u) != find(v)) {
        mst.add(new Tuple(u, v, weight));
        union(u, v);
    }
}
let mst = new Set();

while (priorityQueue.length) {
    let {weight, u, v} = priorityQueue.shift();
    if (find(u) !== find(v)) {
        mst.add({u, v, weight});
        union(u, v);
    }
}
std::set<std::tuple<int, std::string, std::string>> mst;

while (!priority_queue.empty()) {
    auto [weight, u, v] = priority_queue.top();
    priority_queue.pop();

    if (find(u) != find(v)) {
        mst.insert({weight, u, v});
        union(u, v);
    }
}
var mst = new HashSet<(string, string, int)>();

while (priorityQueue.Count > 0)
{
    var (weight, u, v) = priorityQueue.Dequeue();
    if (Find(u) != Find(v))
    {
        mst.Add((u, v, weight));
        Union(u, v);
    }
}
mst := make(map[Edge]struct{})

for len(priorityQueue) > 0 {
    edge := heap.Pop(&priorityQueue).(Edge)
    weight := edge.weight
    u := edge.u
    v := edge.v

    if find(u) != find(v) {
        mst[Edge{u, v, weight}] = struct{}{}
        union(u, v)
    }
}

Optimizing the Algorithm: Performance Tweaks

Using Union by Rank

Think of union by rank as a merger between companies, where the smaller company gets absorbed into the larger one.

  • How: Maintain a rank variable for each set to make the union operation faster.
  • Why: This keeps the tree balanced, reducing the time complexity.
Time Complexity: How Fast is Fast?
  • How: The algorithm's time complexity is $$O(E \log E)$$.
  • Why: Sorting edges takes $$O(E \log E)$$ and with optimized union-find, checking for cycles is $$O(\log E)$$ per operation.

Kruskal's Algorithm Complexity Analysis

Time Complexity
  • $$O(E \log E)$$: Dominated by sorting edges and union-find operations.
Space Complexity
  • $$O(V)$$: Mainly for storing the parent pointers in the union-find data structure.

Pros, Cons, and Alternatives: The Final Verdict

Pros: The Good Stuff
  • Conceptually Simple: The algorithm is intuitive and provides an optimal solution.
Cons: The Not-So-Good
  • Potentially Slow: If you sort all edges upfront, it could be a bottleneck.
Alternatives: Meet the Cousins
  • Prim's Algorithm: Another option for finding a Minimum Spanning Tree, with different trade-offs.

Kruskal Decision Flow

Before moving on, anchor the implementation to the actual decision loop: sort edges by weight, then ask Union-Find whether each edge connects two different components.

In the visual, notice the two possible outcomes for every edge: accept it if it merges components, or skip it if both endpoints already share the same root.

Kruskal Decision Flow

Kruskal State Snapshot

Before wrapping up, pause on the two moving parts working together: the sorted edge list decides what we try next, and union-find decides whether that edge safely connects two separate components.

In the visual, notice the repeated rhythm: peek smallest edge → compare roots → accept or reject → update components. Kruskal's algorithm is simple once you can see that loop.

Kruskal State Snapshot

Conclusion

Conclusion

Implementing Kruskal's algorithm demonstrates core CS abilities like working with graphs, greedy algorithms, union-find structures, and analyzing time complexity. These fundamentals are at the heart of many technical interviews.

At a high level, Kruskal's works by processing edges from lowest to highest weight, adding them to the MST one-by-one, skipping any that form cycles. This greedy approach results in the overall minimum spanning tree.

Thank you for following along with this tutorial on Kruskal's algorithm.

One Pager Cheat Sheet

One Pager Cheat Sheet

  • Kruskal's Algorithm is a coding interview topic known for demonstrating mastery of computer science basics, as it calculates the minimum spanning tree (MST) of a connected, weighted graph, testing skills like understanding of graphs, greedy algorithms, union-find data structures, and time complexity; MSTs, which connect all vertices without cycles and minimize total cost, have applications in networks, circuit design, and facility location.
  • In graph theory, a Minimum Spanning Tree (MST) is a subset of a graph's edges that connects all vertices without cycles and with the minimum total edge weight, accomplished by using a minimization principle and a greedy approach in algorithms like Kruskal's.
  • Kruskal's Algorithm constructs a Minimum Spanning Tree (MST) without cycles by starting with an empty canvas, sorting edges by weight, adding edges that do not create a cycle, and terminating when the MST is complete (has V - 1 edges), with the process illustrated through ASCII diagrams and code snippets in multiple languages.
  • The guide details the implementation of Kruskal's Algorithm using union-find and priority queue data structures to prevent cycles, ensure connectivity, and always select edges with the lowest weight, demonstrating the procedure in multiple programming languages. The algorithm's time complexity is O(E log E) due to sorting edges and checking for cycles, while its space complexity is O(V) for storing parent pointers, with an additional performance tweak option provided: union by rank which balances the tree and reduces time complexity. The pros include simplicity and optimality, while the cons suggest potential slowness, alongside an alternative: Prim's Algorithm.
  • - Kruskal is a repeated choice: take the next-lightest edge only when Union-Find says it will not create a cycle.
  • - Kruskal = sorted edges plus union-find cycle checks until the MST has V - 1 edges.
  • Implementing Kruskal's algorithm showcases essential computer science skills including working with graphs, using greedy algorithms and union-find structures, and analyzing time complexity, with the algorithm functioning by processing edges from lowest to highest weight to create the minimum spanning tree.
Open interactive view →