Greedy Algorithms Explained: How They Work, When They Fail and How to Use Them
#algorithms
#greedy
#data-structures
#coding-pattern
#algorithm-pattern
#programming-pattern
#dsa-pattern
#dsa-algorithm-patterns
#greedy-algorithms
Picture a vending machine giving you change. It doesn't sit there calculating every possible combination of coins for your 78 cents. It just grabs the biggest coin it can use, then the next biggest and keeps going until it hits zero. No planning ahead, no rethinking earlier picks. Just: what's the best move right now?
That's a greedy algorithm. And honestly, once you see it that clearly, half the battle of learning this topic is over the rest is knowing which problems this "grab the best thing now" strategy actually works on and which ones it quietly breaks.
This guide walks through greedy algorithms the way I wish someone had explained them to me the first time: starting from the plain-English idea, moving into the math that decides whether greedy will actually work, then through the algorithms you'll meet again and again activity selection, knapsack, Huffman coding, Dijkstra, Kruskal, Prim and job scheduling with C# code you can actually run. We'll also cover where greedy quietly gives you the wrong answer, because that part matters more than most tutorials admit.
What is a Greedy Algorithm, Really?
A greedy algorithm builds up a solution one piece at a time and at every single step, it picks whatever option looks best at that moment without checking back later to see if that pick was actually a good idea. That's it. That's the whole definition. Everything else in this article is just detail on top of that one sentence.
The name fits: a greedy algorithm behaves like someone splitting a bill who always claims the biggest piece for themselves, every single time, without wondering if that leaves the group in an awkward spot later. Sometimes that works out fine for everyone. Sometimes it doesn't. Same with the algorithm.
And that's really the tension you need to sit with throughout this whole topic: greedy algorithms always produce an answer. They just don't always produce the right one. No error message, no crash just a confidently wrong result if you apply greedy to a problem it wasn't built for.
Why Does This Approach Even Exist?
Because the "correct" way of solving a lot of optimization problems try every possible combination and keep the best one gets unusable fast.
Say you're scheduling 20 jobs and want to check every possible order. That's 20 factorial combinations. Write that number out and it has 19 digits. Your laptop is not finishing that computation before you retire. Greedy algorithms sidestep this entirely. Instead of exploring the whole space of possibilities, they commit to one choice per step and move on. When that's mathematically valid, you get:
- Speed. Most greedy algorithms run in
O(n log n)time, since sorting is usually the most expensive part. - Simplicity. No recursion trees, no backtracking, no giant memoization tables. Usually just a sort followed by a single pass.
The catch, again: this only pays off when the problem has a very specific structure that guarantees "best right now" adds up to "best overall." So let's talk about what that structure actually looks like.
The Two Conditions That Decide Whether Greedy Will Work
Before you write a single line of greedy code, check whether your problem has both of these. If either one is missing, greedy is the wrong tool full stop.
Greedy Choice Property
This means: making the locally best choice at each step never comes back to bite you. Once you commit, you never need to undo it, no matter what choices come after.
Optimal Substructure
This means: the best overall solution is built out of the best solutions to its smaller pieces. If you solved a sub-problem optimally, that optimal piece is genuinely part of the final answer not just close to it.
Dynamic programming also relies on optimal substructure. The difference is what happens next: DP checks multiple candidate sub-solutions and keeps the winner. Greedy just picks one and never looks back. That single difference is why greedy is faster when it applies and wrong when it doesn't.
The Coin Problem: Where Greedy Shines and Where It Falls Apart
I like starting here because it shows both sides of greedy in under a minute.
The setup: you're given coin denominations and a target amount and you want to use the fewest coins to hit that amount exactly.
When it works
Say your coin drawer has denominations {1, 5, 10, 25} regular US coins and someone needs 63 cents back.
Target: 63
Step 1: Largest coin ≤ 63 → 25 → remaining 38
Step 2: Largest coin ≤ 38 → 25 → remaining 13
Step 3: Largest coin ≤ 13 → 10 → remaining 3
Step 4: Largest coin ≤ 3 → 1 → remaining 2
Step 5: Largest coin ≤ 2 → 1 → remaining 1
Step 6: Largest coin ≤ 1 → 1 → remaining 0
Coins used: 25, 25, 10, 1, 1, 1 → 6 coins total
For US-style denominations, grabbing the biggest coin every time happens to always produce the minimum coin count. This isn't a lucky coincidence it's because {1, 5, 10, 25} forms what's called a "canonical" coin system, one where greedy is provably optimal.
When it doesn't
Now change the denominations to {1, 4, 5} and ask for 8 cents.
Greedy's approach:
Step 1: Largest coin ≤ 8 → 5 → remaining 3
Step 2: Largest coin ≤ 3 → 1 → remaining 2
Step 3: Largest coin ≤ 2 → 1 → remaining 1
Step 4: Largest coin ≤ 1 → 1 → remaining 0
Coins used: 5, 1, 1, 1 → 4 coins
The actual best answer:
4 + 4 = 8 → 2 coins
Greedy grabbed the 5 first because it looked biggest and best and that single choice locked it into needing three more small coins. The optimal answer just used two 4s something greedy never even considered, because it never reconsiders a choice once it's made.
This is the whole lesson in one example: greedy will run to completion and hand you an answer every time, whether or not that answer is actually correct. You have to prove correctness for your specific problem. You can't eyeball it.
The General Shape of a Greedy Solution
Most greedy algorithms follow roughly this pattern, whatever the specific problem is:
1. Decide what "best right now" means for this problem
(smallest weight? earliest deadline? highest ratio?)
2. Sort or organize the input around that rule
3. Walk through the sorted input, one item at a time
4. Take the item if it fits the constraints
5. Skip it if it doesn't and move on
6. Stop once the input runs out or the goal is met
Step 1 is where all the actual thinking happens. Steps 2 through 6 are usually a loop and a handful of lines. If you find yourself struggling with the "code," you're probably still stuck on step 1 figuring out the right criterion and proving it holds up.
Activity Selection: Booking the Most Sessions in One Room
Imagine you run a single studio room and people keep requesting time slots for one-off sessions yoga, a client meeting, whatever. Only one session can use the room at a time. Your job: accept as many non-overlapping sessions as possible.
The greedy rule: always accept whichever remaining session ends earliest.
Why earliest finish and not, say, earliest start, or shortest duration? Because ending early is exactly what frees up the most room for everything that comes after. Any other first pick can only free up an equal or smaller window. You can actually prove this with a swap: take any optimal schedule, swap its first session out for the earliest-finishing one instead and you'll find the schedule is never worse off. That swap-based reasoning is called an exchange argument and it's the standard way greedy correctness gets proven.
Here's a set of requests (start, end):
A(0,2) B(1,4) C(3,5) D(5,7) E(3,9) F(6,8) G(8,11) H(9,12)
Sort by end time: A(0,2), B(1,4), C(3,5), D(5,7), F(6,8), E(3,9), G(8,11), H(9,12)
Pick A(0,2) → room free from time 2
B starts at 1 → too early, skip
Pick C(3,5) → room free from time 5
Pick D(5,7) → room free from time 7
F starts at 6 → too early, skip
E starts at 3 → too early, skip
Pick G(8,11) → room free from time 11
H starts at 9 → too early, skip
Final schedule: A, C, D, G → 4 sessions booked
In C#, this looks like:
public static List<(int Start, int End)> SelectActivities(List<(int Start, int End)> requests)
{
var sorted = requests.OrderBy(r => r.End).ToList();
var selected = new List<(int Start, int End)>();
int lastEnd = int.MinValue;
foreach (var request in sorted)
{
if (request.Start >= lastEnd)
{
selected.Add(request);
lastEnd = request.End;
}
}
return selected;
}
O(n log n) for the sort, O(n) for the single pass after that. This is the same basic logic behind any system that tries to pack the maximum number of non-conflicting bookings into a single resource a meeting room, a single server slot, a single checkout counter.
Fractional Knapsack: When You Can Split the Loot
Here's a scenario: you're packing a delivery van with limited capacity and each package has a value and a weight. Unlike a heist movie where you grab whole items, imagine you're allowed to load partial units of each package (think bulk goods, not sealed boxes). Your goal is to maximize total value without exceeding the van's weight limit.
The greedy rule: always load as much as you can of whichever item gives you the most value per kilogram, then move to the next-best ratio and so on.
This works cleanly because fractions are allowed there's no risk of "wasting" leftover space, since you can always top off the remaining capacity exactly. Take the item with the best return per kilo first and you're guaranteed to squeeze out the maximum possible value.
Say your van holds 25 kg and you're choosing between four items:
Item 1: value 50, weight 10 kg → ratio 5.0
Item 2: value 140, weight 20 kg → ratio 7.0
Item 3: value 60, weight 5 kg → ratio 12.0
Item 4: value 80, weight 8 kg → ratio 10.0
Sorted by ratio, best first: Item 3, Item 4, Item 2, Item 1.
Load Item 3 fully → 5 kg used, 20 kg left, value so far: 60
Load Item 4 fully → 8 kg used, 12 kg left, value so far: 140
Load Item 2 partly → 12 of 20 kg (60%), value added: 140 × 0.6 = 84
Van is full. Skip Item 1 entirely.
Total value: 60 + 80 + 84 = 224
public static double FractionalKnapsack(List<(double Value, double Weight)> items, double capacity)
{
var sorted = items.OrderByDescending(i => i.Value / i.Weight).ToList();
double totalValue = 0;
foreach (var (value, weight) in sorted)
{
if (capacity <= 0) break;
double taken = Math.Min(weight, capacity);
totalValue += taken * (value / weight);
capacity -= taken;
}
return totalValue;
}
One quiet trap: comparing value / weight as floating-point numbers can introduce rounding errors on large datasets. If you need bulletproof precision, compare ratios via cross-multiplication instead a * d > c * b instead of a/b > c/d and skip floating-point division in the comparison step entirely.
And a warning worth repeating: this greedy trick does not carry over to the 0/1 knapsack problem, where you must take each item whole or leave it behind. There, a high-ratio item can leave you with awkward leftover capacity that nothing fits into cleanly and greedy has no way to correct for that. That version needs dynamic programming trying to force greedy onto it is one of the most common mistakes people make with this topic.
Huffman Coding: Squeezing Data Down by Being Unfair to Rare Symbols
Here's a genuinely useful question: if some characters show up constantly in your data and others barely show up at all, why give them the same number of bits?
That's the entire motivation behind Huffman coding. Give frequent characters short binary codes, give rare characters longer ones and the total encoded size drops sometimes dramatically compared to giving every character a fixed-length code.
The greedy rule: repeatedly take the two least-frequent items you have, merge them into one combined node and put that merged node back into the pool. Keep going until only one node the root is left.
Let's say you're encoding five symbols with these frequencies:
m: 2 n: 3 o: 4 p: 5 q: 6
Step 1: Smallest two are m(2) and n(3) → merge into node X(5)
Pool now: o(4), X(5), p(5), q(6)
Step 2: Smallest two are o(4) and X(5) → merge into node Y(9)
Pool now: p(5), q(6), Y(9)
Step 3: Smallest two are p(5) and q(6) → merge into node Z(11)
Pool now: Y(9), Z(11)
Step 4: Merge Y(9) and Z(11) → root(20)
Notice the frequencies add up correctly at every step (2+3=5, 4+5=9, 5+6=11, 9+11=20) that's a handy way to sanity-check your own Huffman tree by hand.
Because m and n got merged first, they end up deepest in the tree, meaning they'll get the longest codes. q, the most frequent symbol, stays shallow and gets a short code. That's exactly the outcome you want and it's not a heuristic guess this greedy merging strategy is mathematically proven to produce the shortest possible total encoding for a prefix-free code (meaning no character's code is a prefix of another's, so decoding never gets ambiguous).
In C#, a min-heap makes this clean. .NET's built-in PriorityQueue<TElement, TPriority> does the job:
public class HuffmanNode
{
public char? Symbol { get; set; }
public int Frequency { get; set; }
public HuffmanNode? Left { get; set; }
public HuffmanNode? Right { get; set; }
}
public static HuffmanNode BuildHuffmanTree(Dictionary<char, int> frequencies)
{
var queue = new PriorityQueue<HuffmanNode, int>();
foreach (var kvp in frequencies)
queue.Enqueue(new HuffmanNode { Symbol = kvp.Key, Frequency = kvp.Value }, kvp.Value);
while (queue.Count > 1)
{
var first = queue.Dequeue();
var second = queue.Dequeue();
var merged = new HuffmanNode
{
Frequency = first.Frequency + second.Frequency,
Left = first,
Right = second
};
queue.Enqueue(merged, merged.Frequency);
}
return queue.Dequeue();
}
Time complexity: O(n log n), since every dequeue/enqueue on the heap costs O(log n) and you do this roughly n times.
Worth knowing: Huffman coding is a documented building block inside common compression formats it's part of the DEFLATE algorithm behind ZIP and gzip and it shows up in JPEG's entropy coding step too. That's public, well-documented behavior of those formats, not a claim about any specific vendor's private implementation.
Dijkstra's Algorithm: Finding the Shortest Route Without Checking Every Route
Anyone who's used a maps app has relied on some version of this idea, even if the production system behind it is far more elaborate than what we're covering here.
The problem: given a graph with non-negative edge weights and a starting point, find the shortest distance from that start to every other node.
The greedy rule: always finalize whichever unvisited node currently has the smallest known distance from the start.
Say you're routing between a warehouse and four stores and the numbers are travel time in minutes:
Warehouse --4--> StoreA
Warehouse --1--> StoreB
StoreB --2--> StoreA
StoreA --1--> StoreC
StoreB --5--> StoreD
StoreC --3--> StoreD
Starting from the Warehouse, Dijkstra doesn't check every possible path to every store. It grabs the closest unvisited node, locks in its distance and uses it to update its neighbors repeating until everything is visited:
Start: Warehouse = 0, everything else = infinity
Visit Warehouse (0) → update StoreA to 4, StoreB to 1
Visit StoreB (1) → update StoreA to min(4, 1+2)=3, StoreD to 1+5=6
Visit StoreA (3) → update StoreC to 3+1=4
Visit StoreC (4) → update StoreD to min(6, 4+3)=6
Visit StoreD (6) → done
Final shortest distances from Warehouse:
StoreA: 3, StoreB: 1, StoreC: 4, StoreD: 6
public static Dictionary<string, int> Dijkstra(
Dictionary<string, List<(string Neighbor, int Weight)>> graph,
string source)
{
var distances = graph.Keys.ToDictionary(node => node, _ => int.MaxValue);
distances[source] = 0;
var visited = new HashSet<string>();
var queue = new PriorityQueue<string, int>();
queue.Enqueue(source, 0);
while (queue.Count > 0)
{
var current = queue.Dequeue();
if (!visited.Add(current)) continue;
foreach (var (neighbor, weight) in graph[current])
{
int newDistance = distances[current] + weight;
if (newDistance < distances[neighbor])
{
distances[neighbor] = newDistance;
queue.Enqueue(neighbor, newDistance);
}
}
}
return distances;
}
Why does locking in a node's distance early never come back to bite you? Because every edge weight is non-negative there's no way a path through a currently-farther, unvisited node could somehow end up shorter later. Take away that non-negative guarantee and this whole argument collapses.
This is the part people forget under pressure: Dijkstra's algorithm gives wrong answers on graphs with negative edge weights and it won't tell you it's wrong it just returns a shortest-path answer that isn't actually shortest. If your graph can have negative weights, you need the Bellman-Ford algorithm instead, which isn't a greedy algorithm at all it relaxes every edge repeatedly rather than committing early.
Complexity: O((V + E) log V) using a binary heap-backed priority queue.
Kruskal's and Prim's Algorithms: Wiring Up a Building for the Least Cable
Say you're wiring five rooms in a small office with network cable and you want every room connected to the network using the least total cable no redundant loops, just the minimum needed to reach everyone. That's a Minimum Spanning Tree (MST) problem and there are two classic greedy ways to solve it.
Kruskal's Algorithm: Sort Everything, Then Filter
Kruskal's approach: list every possible cable run with its length, sort that list cheapest to most expensive and walk down the list adding each cable unless it would connect two rooms that are already connected some other way (which would just create a wasteful loop).
Possible cable runs (Room1-Room2: meters):
R1-R2: 3 R1-R3: 6 R2-R3: 4 R2-R4: 5 R3-R4: 2 R4-R5: 7 R3-R5: 8
Sorted by length: R3-R4(2), R1-R2(3), R2-R3(4), R2-R4(5), R1-R3(6), R4-R5(7), R3-R5(8)
Add R3-R4 (2m) → connects {R3,R4}
Add R1-R2 (3m) → connects {R1,R2}
Add R2-R3 (4m) → connects {R1,R2,R3,R4}
Skip R2-R4 → R2 and R4 already connected, would create a loop
Skip R1-R3 → same reason
Add R4-R5 (7m) → connects {R1,R2,R3,R4,R5}, done
Total cable used: 2 + 3 + 4 + 7 = 16 meters
To check "are these two rooms already connected?" efficiently, Kruskal's implementation leans on a Union-Find (Disjoint Set) structure a data structure built specifically to answer that question in almost constant time, O(α(n)), where α is the inverse Ackermann function. In plain terms: for any input size you'll ever actually deal with, this is effectively instant.
public class UnionFind
{
private readonly int[] _parent;
public UnionFind(int size)
{
_parent = new int[size];
for (int i = 0; i < size; i++) _parent[i] = i;
}
public int Find(int x) => _parent[x] == x ? x : (_parent[x] = Find(_parent[x]));
public bool Union(int a, int b)
{
int rootA = Find(a), rootB = Find(b);
if (rootA == rootB) return false;
_parent[rootA] = rootB;
return true;
}
}
public static int KruskalMst(int nodeCount, List<(int A, int B, int Weight)> edges)
{
var uf = new UnionFind(nodeCount);
int totalWeight = 0;
foreach (var edge in edges.OrderBy(e => e.Weight))
{
if (uf.Union(edge.A, edge.B))
totalWeight += edge.Weight;
}
return totalWeight;
}
Prim's Algorithm: Grow One Tree Outward
Prim's does the same job differently. Start at any single room and keep adding the cheapest cable that connects the growing network to a room not yet reached one edge at a time, from the boundary inward.
Both algorithms are provably correct for the same underlying reason, called the "cut property": for any way you split the rooms into two groups, the cheapest cable crossing that split must belong to some minimum spanning tree. Kruskal's checks this globally by sorting all edges up front; Prim's checks it locally, one boundary at a time, using a min-heap instead of a full sort.
Which one should you actually reach for?
| Kruskal's | Prim's | |
|---|---|---|
| Plays nicer with | Sparse graphs (few edges) | Dense graphs (lots of edges) |
| Main data structure | Union-Find | Min-heap / priority queue |
| How it thinks | Globally sorts every edge up front | Locally grows one tree at a time |
| Complexity | O(E log E) |
O((V+E) log V), or O(V²) with a plain matrix on dense graphs |
Neither one is "better" in some absolute sense. If your graph has relatively few edges compared to nodes, Kruskal's tends to be simpler and just as fast. If it's densely connected, Prim's with an adjacency matrix can actually outperform it.
Job Sequencing With Deadlines: Picking the Highest-Value Work You Can Actually Finish
Say you're a freelancer with a backlog of one-hour jobs, each with a deadline (in hours from now) and a payout. You can only do one job per hour slot and a job only pays out if you finish it by its deadline. You want to walk away with the most money possible.
The greedy rule: sort jobs by payout, highest first. For each job, slot it into the latest available hour before its deadline not the earliest, because leaving earlier slots open gives lower-paying, tighter-deadline jobs more room to squeeze in later.
Jobs (deadline in hours, payout in $):
J1: deadline 2, pays 120
J2: deadline 1, pays 45
J3: deadline 2, pays 75
J4: deadline 3, pays 30
J5: deadline 1, pays 60
Sorted by payout: J1(120), J3(75), J5(60), J2(45), J4(30)
J1 (deadline 2) → slot 2 is free → take it
J3 (deadline 2) → slot 2 taken, try slot 1 → free → take it
J5 (deadline 1) → slot 1 taken → no earlier slot exists → skip
J2 (deadline 1) → slot 1 taken → skip
J4 (deadline 3) → slot 3 is free → take it
Jobs completed: J1, J3, J4 → total payout: 120 + 75 + 30 = $225
public static (List<string> Jobs, int TotalProfit) ScheduleJobs(
List<(string Name, int Deadline, int Profit)> jobs)
{
var sorted = jobs.OrderByDescending(j => j.Profit).ToList();
int maxDeadline = jobs.Max(j => j.Deadline);
var slots = new string?[maxDeadline + 1]; // index 0 unused
int totalProfit = 0;
foreach (var job in sorted)
{
for (int slot = job.Deadline; slot >= 1; slot--)
{
if (slots[slot] == null)
{
slots[slot] = job.Name;
totalProfit += job.Profit;
break;
}
}
}
var scheduled = slots.Where(s => s != null).Select(s => s!).ToList();
return (scheduled, totalProfit);
}
The naive slot search above is O(n × d), where d is the largest deadline. If d is large, swap the linear slot search for a Union-Find structure that jumps straight to the latest free slot that gets you down to roughly O(n log n) overall.
Greedy vs. Dynamic Programming vs. Divide and Conquer
People mix these up because all three break a big problem into smaller ones. The difference is what happens to those smaller pieces.
+-------------------+----------------------------+-------------------------------+
| Technique | Core idea | Revisits earlier choices? |
+-------------------+----------------------------+-------------------------------+
| Greedy | Commit to one best-looking | No decision is final |
| | choice per step | |
+-------------------+----------------------------+-------------------------------+
| Dynamic | Try relevant options, | Yes via memoization or a |
| Programming | reuse sub-problem results | table of solved sub-problems |
+-------------------+----------------------------+-------------------------------+
| Divide & Conquer | Split into independent | Not applicable pieces don't |
| | pieces, solve, combine | overlap |
+-------------------+----------------------------+-------------------------------+
| Greedy | Dynamic Programming | |
|---|---|---|
| Typical speed | Fast usually O(n log n) |
Slower often O(n²) or worse, depending on state space |
| Memory use | Low | Higher stores sub-problem results |
| Always optimal? | Only if the greedy-choice property genuinely holds | Yes, whenever optimal substructure holds |
| Classic examples | Activity selection, Dijkstra's, MST algorithms | 0/1 Knapsack, Longest Common Subsequence, Edit Distance |
A rule of thumb that's saved me more than once: if a problem smells like knapsack but you can't split items into fractions, your gut reaction should be dynamic programming, not greedy. The fractional version is greedy territory; the 0/1 version almost never is.
Proving Greedy Is Actually Correct
Don't take a greedy algorithm's correctness on faith just because the logic "feels obvious." There are two standard tools for actually proving it.
Exchange argument. Assume, for the sake of argument, that some optimal solution doesn't start with the greedy choice. Show that you can swap the greedy choice in without making things worse. If that swap always holds, the greedy choice was safe all along. This is how activity selection and MST correctness get proven.
Matroid theory. This is the more advanced, more general tool. A matroid is a mathematical structure that captures exactly which kinds of problems are guaranteed to be solvable by a greedy approach. If your problem's constraints form a matroid, the Rado–Edmonds theorem guarantees greedy gives you the optimal answer no case-by-case proof needed. MST problems, for instance, can be shown to form a matroid, which is part of why both Kruskal's and Prim's algorithms are provably correct rather than "usually correct."
You don't need matroid theory to write greedy code day to day. But it's the reason some greedy algorithms are bulletproof while others like our {1,4,5} coin example quietly fall apart.
Mistakes People Actually Make With Greedy Algorithms
- Trusting the "feels right" instinct. The coin problem alone should be proof that greedy logic can look completely reasonable and still be wrong.
- Skipping the proof. If you can't explain why your greedy rule is safe, you don't actually know it's safe you're just hoping.
- Sorting by the wrong thing. Job sequencing sorted by deadline instead of profit will run fine and quietly give you a worse answer.
- Forcing greedy onto 0/1 knapsack. This is probably the single most common greedy mistake in interviews. If items can't be split, be suspicious.
- Not checking for negative weights before using Dijkstra. It won't throw an exception. It'll just hand you a wrong shortest path and let you find out later.
- Ignoring tie-breaking rules. When two options score equally on the greedy criterion (same finish time, same ratio), be explicit about how you handle the tie it can matter more than it looks.
Where Greedy Runs Into Trouble
- It's narrow by design. Greedy solves exactly the problems where both properties hold and offers nothing beyond that.
- There's no universal checklist. Unlike brute force (always correct, just slow), greedy needs a fresh correctness proof for every new problem.
- No do-overs. Since greedy never revisits a decision, a wrong assumption about the problem's structure produces consistently wrong answers, not occasional glitches.
- Fast but useless if wrong. A greedy algorithm that returns quickly and returns the wrong answer isn't actually an improvement over something slower that gets it right.
Performance and Production Notes
- Sorting is usually your bottleneck. Most greedy algorithms open with a sort, which puts a practical floor of
O(n log n)on their performance unless a specialized structure replaces it. - Heap vs. one-time sort. If the "current best option" keeps changing as you go (Dijkstra, Huffman), use a priority queue instead of resorting from scratch. If you only need one fixed ordering to walk through once (activity selection), a single sort is enough don't overengineer it.
- Union-Find earns its keep. Path compression plus union by rank gets you near-constant-time connectivity checks, which is why it's the backbone of any reasonably fast Kruskal's implementation.
- Memory footprint stays light. Greedy algorithms typically skip the large lookup tables dynamic programming needs, which matters when you're working with genuinely large graphs or datasets.
- Watch floating-point comparisons. Anything involving ratios (fractional knapsack, cost-benefit sorting) is a candidate for subtle precision bugs at scale cross-multiply instead of dividing when you can.
Interview Questions on Greedy Algorithms
Beginner
1. What is a greedy algorithm, in plain terms?
An algorithm that picks whatever looks best at each step and never revisits that choice, hoping the sum of those choices ends up being the best overall solution.
2. Does greedy always give the right answer?
No. It's only guaranteed to be optimal when the problem has both the greedy-choice property and optimal substructure. Otherwise, it produces a valid answer just not necessarily the best one.
3. Can you give a case where greedy actually fails?
Coin denominations {1, 4, 5} targeting 8: greedy grabs a 5 first and ends up needing four coins total, while the actual best answer only needs two (4 + 4).
Intermediate
4. Why does Dijkstra's algorithm break with negative edge weights?
Its whole logic rests on the idea that once a node's shortest distance is locked in, nothing later can shrink it further. Negative weights can violate that assumption completely, since a path that currently looks longer could turn out shorter once a negative edge is factored in.
5. Why won't greedy work for the 0/1 knapsack problem?
Because whole items can leave you with leftover capacity that nothing fits into cleanly there's no way to "top off" the remaining space the way you can with fractions. Local, ratio-based decisions can't account for that, so you need dynamic programming instead.
6. What's actually different between Kruskal's and Prim's algorithms?
Both are greedy MST algorithms, but Kruskal's sorts every edge globally and adds each one if it avoids a cycle (via Union-Find), while Prim's grows a single tree outward, always grabbing the cheapest edge at its current boundary (via a min-heap). Kruskal's usually suits sparse graphs better; Prim's usually suits dense ones.
Advanced
7. How would you formally prove a greedy algorithm is correct?
Two common routes: an exchange argument, where you show any optimal solution can be reshaped to include the greedy choice without getting worse, or by showing the problem's structure forms a matroid, in which case the Rado–Edmonds theorem guarantees greedy optimality outright.
8. Why does merging the two smallest frequencies first give Huffman coding an optimal result?
Because it guarantees the least frequent symbols end up deepest in the tree meaning the longest codes while the most frequent symbols stay shallow with the shortest codes. That directly minimizes the total weighted code length and it's a property that's been formally proven for this exact construction, not just observed empirically.
Scenario-Based
9. You're building a booking system that should accept the maximum number of non-overlapping sessions. How do you approach it and what breaks at scale?
This maps straight onto activity selection: sort by end time, greedily accept anything that doesn't overlap with what's already booked. At scale, the real risks are usually outside the algorithm itself time zone normalization before sorting, deciding how to break ties when two sessions end at the same instant and whether "maximum count" is even the right business goal. A real booking system might care more about maximizing revenue or prioritizing VIP sessions, in which case plain activity selection stops being the right model.
10. You need to shrink log files before shipping them off a storage-constrained device. Would Huffman coding be your answer?
It's a reasonable piece of the puzzle it's simple, cheap to decode and provably optimal for a fixed, known frequency table. In practice, most real compression tools layer it on top of other techniques like dictionary-based matching rather than using it alone and it's worth being upfront in an interview that you're describing Huffman as a building block, not claiming insight into any specific vendor's actual compression pipeline.
Frequently Asked Questions
1. What is a greedy algorithm?
It's an algorithm that solves a problem by making the best available choice at every step, without ever going back to reconsider an earlier decision.
2. Is a greedy algorithm the same thing as dynamic programming?
No. Greedy commits to one choice per step and moves on for good. Dynamic programming considers multiple candidate choices for each sub-problem and keeps the best one, which is slower but correct for a much wider range of problems.
3. When should I actually use a greedy algorithm?
When you can prove the problem has both the greedy-choice property and optimal substructure think activity selection, fractional knapsack, Huffman coding and minimum spanning tree problems.
4. When should I avoid greedy entirely?
When an early "best" choice can lock you into a bad overall outcome the 0/1 knapsack problem and coin change with non-canonical denominations are the two classic warning signs.
5. Is greedy always faster than dynamic programming?
Usually, since it skips storing and comparing sub-problem results. But speed doesn't matter at all if the algorithm gives you a wrong answer for your specific problem.
6. Why doesn't Dijkstra's algorithm work with negative weights?
Because its correctness depends entirely on the assumption that a finalized shortest distance can never be beaten by a path discovered later. Negative weights can break that assumption outright and the algorithm won't warn you it'll just return an incorrect shortest path.
7. What's an exchange argument in the context of greedy algorithms?
It's a proof technique: assume a different, optimal solution exists, then show that swapping in the greedy choice never makes it worse. If that always holds, you've proven the greedy choice is safe.
8. Do greedy algorithms actually get used in real systems?
Yes. Huffman coding is a documented component of common compression formats like DEFLATE, which powers ZIP and gzip. Dijkstra's algorithm is the well-documented theoretical basis behind many shortest-path and routing systems. Minimum spanning tree algorithms show up in network design problems, like minimizing the cable or wiring needed to connect a fixed set of points.
Wrapping Up
Greedy algorithms aren't one algorithm they're a mindset: commit to whatever looks best right now and never look back. When a problem actually has the greedy-choice property and optimal substructure, this mindset gives you fast, clean solutions to real problems shortest paths, minimum spanning trees, optimal compression, interval scheduling. When those properties don't hold, greedy fails silently, which is exactly why proving correctness matters as much as writing the code.
The real skill isn't memorizing "these are the greedy problems." It's developing the habit of asking, for any new problem: if I lock in the best-looking choice right now, will I ever regret it later? If you can answer yes with confidence ideally backed by an exchange argument or a matroid structure greedy is probably your fastest path to a correct answer. If you can't answer that, or worse, if you can find a counterexample, that's your signal to reach for dynamic programming or another approach instead.
