Kruskal's algorithm (minimum spanning tree)
Connect every node with the lowest total cost by taking edges from cheapest to most expensive and skipping any edge that would close a loop.
O(e log e)Space O(n + e)4 practice problemsStep-through pictureThe idea
Kruskal's algorithm finds a minimum spanning tree (the cheapest set of edges that connects every node without loops) in a different way from Prim's. Instead of growing one tree, it sorts all edges by cost and looks at them from cheapest to most expensive.
For each edge, it asks: are its two ends already connected by the edges taken so far? If yes, the edge would only close a loop, so skip it. If no, take it, because it joins two separate groups as cheaply as possible.
The "already connected?" question is exactly what union-find answers in almost constant time. The algorithm stops once it has taken n - 1 edges, the size of any spanning tree.
An everyday example
Building bridges between islands on a budget
A group of islands needs bridges so you can drive between any two. You get a price list for every possible bridge and read it from cheapest to most expensive. You build a bridge only if its two islands can't already reach each other through bridges you've built. Each built bridge merges two groups of islands into one. When all islands form one group, you stop.
| In the example | In the code |
|---|---|
| The price list, cheapest first | sorted, the edges sorted by weight |
| Groups of islands joined by bridges | Union-find groups |
| Can these two islands already reach each other? | find(a) === find(b) |
| Building a bridge | parent[rootA] = rootB and total += weight |
| Stopping when everything is one group | used === n - 1 |
From the slow way to kruskal's algorithm (minimum spanning tree)
- Start with the obvious way: try every set of
n - 1edges, keep the ones that connect everything without a loop, and pick the cheapest. Even 20 edges on 10 nodes give 167,960 sets to check, and it grows explosively from there. - Look at the cheapest edge in the whole graph. Splitting the nodes so that its two ends are on different sides, it's the cheapest crossing edge, so by the cut property some cheapest tree contains it. The same thinking works for the next cheapest edge, and the next, as long as the edge joins two pieces that aren't connected yet.
- What about an edge whose ends are already connected? Every edge already taken is cheaper or equal, so this edge would close a loop where it's the most expensive member. A cheapest tree never needs that edge. So: go through the edges from cheapest to most expensive, take an edge if it joins two separate pieces, and skip it otherwise.
- The only hard question is "are these two ends already connected?". A fresh search each time costs
O(n)per edge. Union-find answers it in nearly constant time and merges the two pieces when an edge is taken. Sorting, then one pass withfind, stopping atn - 1edges: that is Kruskal's algorithm.
How it works
- Make a union-find
parentarray where each node is its own group. - Sort a copy of the edges by
weight, cheapest first. - Go through the sorted edges. For each
[a, b, weight], find the rootsrootAandrootB. - If the roots are equal,
aandbare already connected: skip the edge. - Otherwise merge the two groups, add
weighttototal, and count the edge inused. - Stop at
n - 1edges. If fewer were used, the graph isn't connected: return -1.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function kruskalMst(n, edges) {const parent = Array.from({ length: n }, (_, i) => i);const find = (x) => {while (parent[x] !== x) {parent[x] = parent[parent[x]];x = parent[x];}return x;};const sorted = [...edges].sort((a, b) => a[2] - b[2]);let total = 0;let used = 0;for (const [a, b, weight] of sorted) {const rootA = find(a);const rootB = find(b);if (rootA === rootB) continue;parent[rootA] = rootB;total += weight;used++;if (used === n - 1) break;}return used === n - 1 ? total : -1;}
How fast it is
| Time | O(e log e) |
|---|---|
| Extra memory | O(n + e) |
Sorting the e edges is the main cost; each union-find step afterwards is nearly constant time. The sorted copy holds e edges and parent holds n numbers.
- Best case
O(e)The edges are already in weight order, so the sort makes a single pass (V8's sort is TimSort, which notices sorted runs), and the first
n - 1edges form the tree. - Average case
O(e log e)Edges in random order: sorting dominates, and each
findafterwards is nearly constant. - Worst case
O(e log e)Random order, and the most expensive edge is the only one reaching some node, so the loop reads every edge before it can stop.
Common mistakes
- Sorting with
edges.sort()and no compare function, which sorts as text, so10comes before9. - Checking
parent[a] === parent[b]instead of comparing roots fromfind. - Forgetting the
n - 1check, and returning a total for a graph in several pieces. - Sorting the input array in place when the caller still needs its original order, such as for returning edge indexes.
When to use it
Look for these clues in a problem:
- The question asks for the cheapest way to connect every node.
- The input is already a list of edges with costs.
- You also need to know which edges are in the tree, in cost order.
- The graph is sparse, so sorting the edges is cheap.
Check yourself
Answer each one in your head first, then open it.
Why is sort((a, b) => a[2] - b[2]) needed instead of sort()?
Plain sort() compares the edges as text, so the order is wrong. The compare function sorts by weight as numbers.
Why can the loop stop at n - 1 edges?
A spanning tree of n nodes has exactly n - 1 edges. Any later edge would close a loop and be skipped anyway.
In the trace, why was 3-4 skipped although it costs only 3?
Nodes 3 and 4 were already connected through 2-3 and 2-4, which cost 1 and 2. Adding 3-4 would close a loop where it's the most expensive edge.
What part of the running time comes from union-find?
Almost none: about two nearly-constant find calls per edge. Sorting the e edges, O(e log e), is the main cost.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 4 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingmedium
Go further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
Two rules hold after each edge is processed. The edges taken so far are part of some minimum spanning tree T. And the union-find groups are exactly the connected pieces formed by the taken edges.
When an edge a-b is taken, its ends are in different pieces, A and B. Every cheaper edge has already been processed. If some cheaper edge crossed between A and the rest of the graph, its ends were in different groups when it was processed, since groups only grow, so it would have been taken and merged them. So a-b is a cheapest edge leaving A, and the same exchange argument as Prim's applies: if T lacks it, adding it makes a loop that leaves A through another edge at least as expensive, and swapping them keeps T minimal.
When an edge is skipped, its ends already have a path of taken edges, each cheaper or equal. Adding it would only close a loop, and a loop's most expensive edge is never needed. The loop ends after n - 1 taken edges, which connect all n nodes without a loop: a spanning tree inside a minimum spanning tree. If the edges run out first, the graph is in pieces, and it returns -1.
Going deeper
3 more ideas
Why taking the cheapest edge between groups is safe
When Kruskal takes an edge between two groups, every cheaper edge has already been looked at, and none of them connected those groups. So this edge is the cheapest edge crossing between "this group" and "everything else". By the cut property (the same rule behind Prim's algorithm), some minimum spanning tree contains it.
Cost
Sorting takes O(e log e). Each edge then needs two find calls, which are almost constant time with path compression. So sorting is the main cost, and the total is O(e log e).
Ties and the edges you pick
When edges have equal weights, a stable sort keeps their original order, so the tree you build depends on that order. The total cost is always the same. If a problem asks which edges are chosen, it must define a tie rule such as "earlier edges in the input first", which a stable sort follows.
Work it by hand
Every step in a table, with why
Six nodes, nine edges. One edge closes a loop and is skipped, two edges tie at weight 4, and the loop stops before the last three edges.
n = 6, edges = [[0,1,4], [1,2,4], [0,2,6], [2,3,1], [3,4,3], [2,4,2], [4,5,7], [1,5,8], [0,5,5]]
| Step | Edge (weight) | rootA | rootB | total | used | What happens | Why |
|---|---|---|---|---|---|---|---|
| 1 | 2-3 (1) | 2 | 3 | 1 | 1 | Take: parent[2] = 3 | After sorting, the cheapest edge comes first. Its ends are separate, so it's taken. |
| 2 | 2-4 (2) | 3 | 4 | 3 | 2 | Take: parent[3] = 4 | find(2) walks to root 3. Node 4 is alone, so the two pieces merge. |
| 3 | 3-4 (3) | 4 | 4 | 3 | 2 | Skip: same root | Nodes 3 and 4 are already connected through 2, so this edge would close the loop 2-3-4. |
| 4 | 0-1 (4) | 0 | 1 | 7 | 3 | Take: parent[0] = 1 | A new piece starts on the other side of the graph. |
| 5 | 1-2 (4) | 1 | 4 | 11 | 4 | Take: parent[1] = 4 | This ties with 0-1 at weight 4; the stable sort kept input order. find(2) also shortens a path: parent[2] becomes 4. |
| 6 | 0-5 (5) | 4 | 5 | 16 | 5 | Take, then stop | find(0) walks 0 → 1 → 4 and points 0 straight at 4. Now used === n - 1 = 5, so the loop breaks. |
It returns 16, using edges 2-3, 2-4, 0-1, 1-2, and 0-5. The edges 0-2 (6), 4-5 (7), and 1-5 (8) are never looked at, because the tree was finished first.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Kruskal's algorithm (minimum spanning tree) | O(e log e) | O(n + e) | The input is a list of edges, the graph is sparse, or you want clusters by stopping early. |
| Prim's algorithm | O(e log e) | O(n + e) | The graph is given as adjacency lists, or you want the tree grown from one node. |
| Prim's with an array instead of a heap | O(n²) | O(n) | Every pair of points can be linked, so listing and sorting about n² edges would cost more. |
Extra memory: O(n + e): the sorted copy holds e edges, parent holds n numbers, and the sort may use O(e) extra.
How the steps grow
- Kruskal's algorithm (minimum spanning tree), Prim's algorithm
O(e log e) - Prim's with an array instead of a heap
O(n²)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Kruskal's algorithm (minimum spanning tree) | 33 | 9,966 | 20 million |
| Prim's algorithm | 33 | 9,966 | 20 million |
| Prim's with an array instead of a heap | 100 | 1 million | 1 trillion |
Inputs that need care
6 tricky inputs
- A graph in pieces:
n = 5, edges0-1,1-2,3-4 - Only 3 edges can be taken, fewer than
n - 1 = 4, so it returns-1. - A self-loop like
[2, 2, 1] find(2)equalsfind(2), so it's skipped like any loop-closing edge.- Two edges between the same pair
- The cheaper one comes first and is taken; the other is skipped because its ends now share a root.
- Edges with equal weights, like
0-1and1-2at 4 - The stable sort keeps their input order. The chosen edges can depend on that order, but
totalnever does. - Negative weights
- They sort first and work like any other weight. The cut property doesn't care about signs.
n = 1with no edgesusedstays 0, which equalsn - 1, so it returns 0.
Variations and follow-ups
Twists interviewers add
Clustering
To split points into k groups that are as far apart as possible, run Kruskal's and stop after taking n - k edges. The groups in union-find are the clusters, and the next edge that would be taken is the smallest gap between two clusters.
Maximum spanning tree
Sort from most expensive to cheapest instead. The same argument shows the result is the spanning tree with the largest total.
Bottleneck questions
A minimum spanning tree also makes the largest edge on the path between any two nodes as small as possible. So "what's the smallest weight limit that still connects a to b?" is the biggest edge on their path in the tree, or, offline, the weight at which Kruskal's first puts them in one group.
Must-have and never-needed edges
To test whether an edge is in every cheapest tree, run Kruskal's without it: if the total goes up or the graph splits, it's required. To test whether it's in at least one, run Kruskal's with it taken first: if the total stays the same, it can be used.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "minimum cost to connect" when the input is a list of possible connections with prices. Mention that union-find makes the "already connected?" check nearly free.
- Explain both decisions out loud: "take the edge if it joins two separate pieces, because it's the cheapest way out of its piece; skip it otherwise, because it would be the most expensive edge on a loop".
- Test by hand a graph with an edge that closes a loop, a tie in weights, and a disconnected graph that must return
-1. - Expect "Prim's or Kruskal's?". Answer: both are
O(e log e). Kruskal's is natural for an edge list and for clustering; Prim's suits adjacency lists and dense graphs, especially the array version.
Where frontend code uses it
The same idea in real UI code
- Clustering points in a chart or map: run Kruskal and stop early, and the groups left are clusters of nearby points.
- Maze generators for browser games that join random grid cells with union-find, which is randomized Kruskal's.
- Planning tools that pick the cheapest set of network links or roads, where the input is naturally a list of possible links with prices.