Always pick the locally best option and hope it leads to a globally best solution. Sounds reckless, but when the problem has the right structure, greedy is both correct and fast.
IntermediateA greedy algorithm makes a sequence of choices, each time picking the option that looks best right now โ without looking ahead or reconsidering past decisions. Once a choice is made, it's final. No backtracking, no "what if," no regret.
Imagine you're at a buffet and you want to fill your plate with the most delicious food. A greedy strategy: always take the tastiest-looking item you can see right now. This works surprisingly well at most buffets โ the lobster is always a better pick than the dinner roll. But it fails if the dessert table is hidden around a corner and you've already piled your plate high with appetizers. Greedy works when you can see enough of the problem to make good local decisions.
Greedy algorithms have been around almost as long as algorithms themselves. Kruskal's algorithm for minimum spanning trees was published in 1956. Dijkstra's shortest path algorithm followed in 1959 โ Edsger Dijkstra reportedly invented it in about 20 minutes while sitting at a cafรฉ in Amsterdam. Huffman coding (1952) was developed by David Huffman as a term paper for an MIT class when his professor offered an A to anyone who could find the optimal prefix-free code. Huffman succeeded where his professor and the information theorist Claude Shannon had not.
The term "greedy" was formalized in the 1970s as part of the broader study of matroid theory โ a mathematical framework that precisely characterizes when greedy algorithms give optimal results.
Greedy only works when the problem has two properties:
DP considers all options and picks the best after evaluating everything. Greedy picks one option immediately and never looks back. When greedy works, it's typically faster and simpler than DP โ O(n log n) vs O(nยฒ) in many cases. But greedy is less general. It gives the right answer for fewer problems. The key question is always: can I prove that the locally best choice never needs revision?
The textbook example, and the one that makes greedy click. You have a set of activities, each with a start and finish time. You want to attend as many as possible, but they can't overlap. Here's the greedy approach:
Why sorting by finish time works: Because finishing early leaves the maximum remaining time for future activities. Intuitively, an activity that finishes at 2pm leaves more room than one that finishes at 5pm, even if the 5pm one started earlier. Sorting by start time or duration would give wrong answers โ you can construct counterexamples for both.
The proof sketch (exchange argument): Take any optimal solution. If its first activity doesn't finish earliest, swap it with the earliest-finishing activity. The swap doesn't create conflicts (the replacement finishes sooner, so it can't overlap with the second activity). Repeat this argument for each subsequent activity. You end up with the greedy solution, which has the same number of activities as the optimal one. Therefore greedy is optimal.
You can't just hope greedy works. You need a proof. Two standard techniques:
In interviews, you usually don't need to write a formal proof, but you should be able to articulate why the greedy choice works. "It seems right" is not good enough. "Finishing earlier leaves more room" is.
The fastest way to check: try to find a small counterexample. If you can construct an input where greedy gives a suboptimal answer, it fails. Coin change with {1, 3, 4} and amount 6 is the classic counterexample. If you can't find a counterexample after trying a few cases, the greedy approach might work โ but "might" isn't "does." The proof matters.
Watch greedy activity selection in action. Activities are sorted by finish time. The algorithm picks the earliest-finishing activity, then skips anything that conflicts. Green = selected, grey = skipped due to overlap.
Greedy algorithms are usually fast. The bottleneck is almost always the sorting step โ the greedy selection itself is typically a linear scan.
| Algorithm | Time | Space | Notes |
|---|---|---|---|
| Activity Selection | O(n log n) | O(n) | Sort by finish time, then linear scan |
| Fractional Knapsack | O(n log n) | O(1) | Sort by value/weight ratio |
| Huffman Coding | O(n log n) | O(n) | Priority queue (min-heap) for n symbols |
| Jump Game | O(n) | O(1) | Track farthest reachable index |
| Minimum Platforms | O(n log n) | O(n) | Sort arrivals and departures separately |
| Dijkstra (min-heap) | O((V+E) log V) | O(V) | Greedy + priority queue |
| Kruskal's MST | O(E log E) | O(V) | Sort edges + Union-Find |
def activity_selection(activities):
"""Select maximum non-overlapping activities.
activities = list of (start, finish) tuples.
Greedy strategy: always pick the one that finishes earliest."""
if not activities:
return []
# Sort by finish time โ finishing early leaves more room
sorted_acts = sorted(activities, key=lambda x: x[1])
selected = [sorted_acts[0]]
for start, finish in sorted_acts[1:]:
# Only pick activities that start at or after the last one ends
if start >= selected[-1][1]:
selected.append((start, finish))
return selected
# Example
activities = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 9), (5, 9),
(6, 10), (8, 11), (8, 12), (2, 14), (12, 16)]
result = activity_selection(activities)
print(f"Selected {len(result)} activities: {result}")
# Selected 4 activities: [(1, 4), (5, 7), (8, 11), (12, 16)]
def erase_overlap_intervals(intervals):
"""Find the minimum number of intervals to REMOVE so the
rest don't overlap. This is the complement of activity selection:
max non-overlapping = n - min removals.
Same greedy idea: sort by finish time, keep non-overlapping ones."""
if not intervals:
return 0
intervals.sort(key=lambda x: x[1]) # Sort by end time
kept = 1 # Keep the first interval
last_end = intervals[0][1]
for start, end in intervals[1:]:
if start >= last_end:
# No overlap โ keep this interval
kept += 1
last_end = end
# else: overlaps โ skip it (implicitly "remove" it)
return len(intervals) - kept # Total - kept = removed
def fractional_knapsack(items, capacity):
"""Maximize value with a weight limit. Can take fractions.
items = list of (weight, value) tuples.
Greedy: sort by value/weight ratio, take as much of the
best items as possible."""
# Sort by value-per-weight ratio, highest first
items_sorted = sorted(items, key=lambda x: x[1]/x[0], reverse=True)
total_value = 0.0
remaining = capacity
for weight, value in items_sorted:
if remaining <= 0:
break
if weight <= remaining:
# Take the whole item
total_value += value
remaining -= weight
else:
# Take a fraction โ this is why greedy works here
# (you can't do this in 0/1 knapsack)
fraction = remaining / weight
total_value += value * fraction
remaining = 0
return total_value
function fractionalKnapsack(items, capacity) {
// Sort by value-per-weight ratio, highest first
items.sort((a, b) => (b.value / b.weight) - (a.value / a.weight));
let totalValue = 0;
let remaining = capacity;
for (const item of items) {
if (remaining <= 0) break;
if (item.weight <= remaining) {
totalValue += item.value;
remaining -= item.weight;
} else {
totalValue += item.value * (remaining / item.weight);
remaining = 0;
}
}
return totalValue;
}
def can_jump(nums):
"""Can you reach the last index? nums[i] = max jump length from i.
Greedy: track the farthest index reachable so far.
If at any point i > farthest, we're stuck."""
farthest = 0
for i in range(len(nums)):
if i > farthest:
return False # Can't even reach this position
# Greedily extend our reach as far as possible
farthest = max(farthest, i + nums[i])
if farthest >= len(nums) - 1:
return True # Early exit: can already reach the end
return True
def jump(nums):
"""Minimum number of jumps to reach the last index.
Greedy BFS: each 'level' is a range of indices reachable
with the same number of jumps. Expand the range greedily."""
if len(nums) <= 1:
return 0
jumps = 0
current_end = 0 # Farthest we can reach with 'jumps' jumps
farthest = 0 # Farthest we can reach with 'jumps + 1' jumps
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
if i == current_end:
# We've explored all positions reachable with current jumps
jumps += 1
current_end = farthest
if current_end >= len(nums) - 1:
break
return jumps
function activitySelection(activities) {
/**
* Select maximum non-overlapping activities.
* activities = array of {start, finish} objects.
* Greedy: sort by finish time, always pick the earliest finisher.
*/
if (activities.length === 0) return [];
// Sort by finish time โ earliest finisher first
const sorted = [...activities].sort((a, b) => a.finish - b.finish);
const selected = [sorted[0]];
for (let i = 1; i < sorted.length; i++) {
// Only pick if this activity starts at or after the last one ended
if (sorted[i].start >= selected[selected.length - 1].finish) {
selected.push(sorted[i]);
}
}
return selected;
}
// Example
const acts = [
{start: 1, finish: 4}, {start: 3, finish: 5}, {start: 0, finish: 6},
{start: 5, finish: 7}, {start: 3, finish: 9}, {start: 5, finish: 9},
{start: 6, finish: 10}, {start: 8, finish: 11}, {start: 8, finish: 12},
{start: 2, finish: 14}, {start: 12, finish: 16}
];
console.log(activitySelection(acts));
// [{start:1,finish:4}, {start:5,finish:7}, {start:8,finish:11}, {start:12,finish:16}]
function canJump(nums) {
/**
* Can you reach the last index?
* nums[i] = max jump length from position i.
* Greedy: track the farthest index reachable so far.
*/
let farthest = 0;
for (let i = 0; i < nums.length; i++) {
if (i > farthest) return false; // Can't reach this position
farthest = Math.max(farthest, i + nums[i]);
if (farthest >= nums.length - 1) return true; // Early exit
}
return true;
}
function minJumps(nums) {
/**
* Minimum jumps to reach the end.
* Greedy BFS: each "level" is a range of reachable indices.
*/
if (nums.length <= 1) return 0;
let jumps = 0, currentEnd = 0, farthest = 0;
for (let i = 0; i < nums.length - 1; i++) {
farthest = Math.max(farthest, i + nums[i]);
if (i === currentEnd) {
jumps++;
currentEnd = farthest;
if (currentEnd >= nums.length - 1) break;
}
}
return jumps;
}
import heapq
def huffman_coding(freq):
"""Build a Huffman tree for optimal prefix-free encoding.
freq = dict of {symbol: frequency}.
Greedy: always merge the two lowest-frequency nodes.
Returns a dict of {symbol: binary_code_string}."""
# Priority queue of (frequency, unique_id, node)
# unique_id prevents comparison of nodes when frequencies tie
heap = [(f, i, symbol) for i, (symbol, f) in enumerate(freq.items())]
heapq.heapify(heap)
uid = len(heap)
# Merge nodes until one tree remains
while len(heap) > 1:
f1, _, left = heapq.heappop(heap)
f2, _, right = heapq.heappop(heap)
merged = (left, right) # Internal node
heapq.heappush(heap, (f1 + f2, uid, merged))
uid += 1
# Extract codes by traversing the tree
codes = {}
def traverse(node, code=""):
if isinstance(node, str):
codes[node] = code or "0" # Single-symbol edge case
return
left, right = node
traverse(left, code + "0")
traverse(right, code + "1")
if heap:
traverse(heap[0][2])
return codes
# Example
frequencies = {"a": 45, "b": 13, "c": 12, "d": 16, "e": 9, "f": 5}
codes = huffman_coding(frequencies)
for symbol, code in sorted(codes.items()):
print(f" '{symbol}': {code}")
# 'a' gets the shortest code (highest frequency)
# 'f' gets the longest code (lowest frequency)
Every time you create a ZIP file, JPEG image, or MP3 audio file, Huffman coding (or a descendant like arithmetic coding) is at work. The core idea: assign shorter binary codes to frequent symbols and longer codes to rare ones. A file where 'e' appears 1000 times and 'z' appears twice? Give 'e' a 2-bit code and 'z' a 12-bit code. Huffman's greedy strategy โ always merge the two least-frequent symbols โ provably produces the optimal prefix-free code. DEFLATE (used in gzip, PNG, and ZIP) combines Huffman coding with LZ77 dictionary compression.
When your packet travels across the internet, routers use variants of Dijkstra's algorithm (a greedy shortest-path algorithm) to decide where to forward it. OSPF (Open Shortest Path First), one of the most widely deployed routing protocols, runs Dijkstra on a weighted graph of network links. Each router builds a complete map of the network topology and greedily extends shortest paths by always processing the nearest unvisited router next. BGP (Border Gateway Protocol), which routes between large networks, uses a different greedy strategy based on path attributes and policy preferences.
The Shortest Job First (SJF) CPU scheduling algorithm is greedy: always run the process with the shortest expected burst time next. This minimizes average waiting time. Real OS schedulers (Linux's CFS, Windows' dispatcher) use more sophisticated versions, but the core insight is greedy โ prioritize jobs that free up the CPU fastest. The Shortest Remaining Time First (SRTF) variant is preemptive: if a new job arrives with a shorter remaining time than the current one, switch immediately.
You're building a conference room booking system. Given a list of meeting requests, how many rooms do you need so that no two overlapping meetings share a room? This is the "minimum platforms" / "meeting rooms" problem โ one of the most practical greedy algorithms out there.
import heapq
def min_meeting_rooms(meetings):
"""Find the minimum number of conference rooms needed.
meetings = list of (start, end) tuples.
Greedy with a min-heap: track when each room becomes free.
When a new meeting starts, if the earliest-freeing room is
available, reuse it. Otherwise, allocate a new room.
The heap size at any point = number of rooms in use."""
if not meetings:
return 0
# Sort by start time โ process meetings in chronological order
meetings.sort(key=lambda x: x[0])
# Min-heap of end times (when each room becomes free)
rooms = []
heapq.heappush(rooms, meetings[0][1]) # First meeting gets a room
for start, end in meetings[1:]:
if rooms[0] <= start:
# Earliest-freeing room is available โ reuse it
heapq.heapreplace(rooms, end) # Pop old end, push new end
else:
# All rooms are busy โ need a new one
heapq.heappush(rooms, end)
return len(rooms)
# Example: a busy Tuesday
meetings = [
(9, 10), # Standup
(9, 11), # Planning session (overlaps with standup)
(10, 12), # Design review
(11, 13), # Lunch meeting
(13, 14), # 1-on-1
(14, 15), # Sprint retro
]
rooms_needed = min_meeting_rooms(meetings)
print(f"Need {rooms_needed} rooms") # Need 2 rooms
# 9-10 and 9-11 overlap โ 2 rooms
# From 11 onward, only 1 room is needed at a time
This exact algorithm powers conference room displays, Calendly-style scheduling tools, and operating system resource allocation. The heap tracks which resources become available soonest โ a pattern that shows up anywhere you're managing a pool of reusable resources (database connections, worker threads, parking spots).
function minMeetingRooms(meetings) {
/**
* Minimum conference rooms needed. O(n log n) time.
*
* Alternative approach: event-based sweep line.
* Mark each start as +1 room needed, each end as -1.
* Sort all events. The peak concurrent meetings = answer.
*/
if (meetings.length === 0) return 0;
const events = [];
for (const [start, end] of meetings) {
events.push([start, 1]); // +1 room at start
events.push([end, -1]); // -1 room at end
}
// Sort by time. If tied, process ends (-1) before starts (+1)
// so that a meeting ending at 10:00 frees the room before
// a meeting starting at 10:00 claims it.
events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
let rooms = 0, maxRooms = 0;
for (const [, delta] of events) {
rooms += delta;
maxRooms = Math.max(maxRooms, rooms);
}
return maxRooms;
}
console.log(minMeetingRooms([[9,10],[9,11],[10,12],[11,13],[13,14]]));
// 2 (9-10 and 9-11 overlap)
function huffmanCoding(freq) {
/**
* Build Huffman codes from a frequency map.
* freq = { 'a': 45, 'b': 13, 'c': 12, ... }
* Returns { 'a': '0', 'b': '101', ... }
*
* Uses a simple sorted-array priority queue (fine for small alphabets).
* For large alphabets, use a proper min-heap.
*/
// Build initial nodes sorted by frequency
let nodes = Object.entries(freq)
.map(([sym, f]) => ({ freq: f, symbol: sym }))
.sort((a, b) => a.freq - b.freq);
if (nodes.length === 1) {
return { [nodes[0].symbol]: '0' };
}
// Merge two lowest-frequency nodes until one tree remains
while (nodes.length > 1) {
const left = nodes.shift();
const right = nodes.shift();
const merged = { freq: left.freq + right.freq, left, right };
// Insert merged node in sorted position
let i = 0;
while (i < nodes.length && nodes[i].freq <= merged.freq) i++;
nodes.splice(i, 0, merged);
}
// Traverse tree to extract codes
const codes = {};
function traverse(node, code) {
if (node.symbol) {
codes[node.symbol] = code || '0';
return;
}
if (node.left) traverse(node.left, code + '0');
if (node.right) traverse(node.right, code + '1');
}
traverse(nodes[0], '');
return codes;
}
const codes = huffmanCoding({ a: 45, b: 13, c: 12, d: 16, e: 9, f: 5 });
console.log(codes);
// 'a' gets shortest code (highest frequency), 'f' gets longest
Let's trace through activity selection with this input:
Activities: (1,4) (3,5) (0,6) (5,7) (3,9) (6,10) (8,11) (12,16)
Step 1: Sort by finish time. Already sorted above. Earliest finisher is (1,4).
Step 2: Pick (1,4). Selected = [(1,4)]. Last finish = 4.
Step 3: Check (3,5). Start 3 < last finish 4 โ overlaps. Skip.
Step 4: Check (0,6). Start 0 < 4 โ overlaps. Skip.
Step 5: Check (5,7). Start 5 โฅ 4 โ no overlap! Pick it. Selected = [(1,4), (5,7)]. Last finish = 7.
Step 6: Check (3,9). Start 3 < 7 โ overlaps. Skip.
Step 7: Check (6,10). Start 6 < 7 โ overlaps. Skip.
Step 8: Check (8,11). Start 8 โฅ 7 โ pick it! Selected = [(1,4), (5,7), (8,11)]. Last finish = 11.
Step 9: Check (12,16). Start 12 โฅ 11 โ pick it! Selected = [(1,4), (5,7), (8,11), (12,16)].
Result: 4 activities selected. You can verify this is optimal โ no selection of 5 non-overlapping activities exists for this input. The greedy strategy (earliest finish time) leaves maximum room for future activities at every step.
| Problem | Difficulty | Pattern | Link |
|---|---|---|---|
| Jump Game | Medium | Farthest reachable | LC #55 |
| Jump Game II | Medium | BFS / greedy jumps | LC #45 |
| Non-overlapping Intervals | Medium | Activity selection | LC #435 |
| Meeting Rooms II | Medium | Interval + heap | LC #253 |
| Gas Station | Medium | Circular greedy | LC #134 |
| Task Scheduler | Medium | Frequency counting | LC #621 |
| Min Arrows to Burst Balloons | Medium | Interval overlap | LC #452 |
| Partition Labels | Medium | Interval + last occurrence | LC #763 |