Greedy Algorithms
A greedy algorithm makes the locally optimal choice at every single step, and never looks back to reconsider it — hoping that a sequence of locally-best choices adds up to a globally optimal answer. It's tempting because it's simple and fast, usually O(n log n) or better. The catch: it only produces the correct answer for problems with the greedy choice property — where a locally optimal choice is provably never wrong. This lesson shows one problem where that property holds (so greedy is safe and simple), and one where it doesn't (so greedy quietly gives a wrong answer) — the skill worth building is telling the two apart.
Where Greedy Works: Activity Selection / Interval Scheduling
Problem: given a list of activities, each with a start and end time, select the maximum number of activities that don't overlap (you can only do one at a time).
The greedy rule: sort all activities by end time, ascending. Then walk through them in that order, greedily picking every activity whose start time is at or after the end time of the last activity you picked.
Why this greedy choice is safe: among all activities, the one that finishes earliest can never hurt you to pick first — picking it leaves the maximum possible remaining time for everything else, and any valid solution can be rearranged to include it without reducing the count. That exchange argument is what makes this a real "greedy choice property," not just a fast heuristic that usually works.
class Activity {
int start, end;
Activity(int start, int end) { this.start = start; this.end = end; }
}
public int maxActivities(Activity[] activities) {
Arrays.sort(activities, (a, b) -> a.end - b.end); // sort by end time, ascending
int count = 1; // always take the first activity after sorting
int lastEnd = activities[0].end;
for (int i = 1; i < activities.length; i++) {
if (activities[i].start >= lastEnd) { // doesn't overlap the last one picked
count++;
lastEnd = activities[i].end;
}
}
return count;
}
Time complexity: O(n log n) — dominated by the sort; the single pass afterward is O(n). Space complexity: O(1) extra (ignoring the sort's own internal space) — just two running variables, no table or memo needed, unlike the DP problems from the last two lessons.
Where Naive Greedy Fails: 0/1 Knapsack
Recall 0/1 Knapsack from the previous lesson: choose a subset of items (whole or not at all) to maximize value within a weight capacity. A tempting greedy rule: sort items by value density (value ÷ weight) descending, and keep taking items in that order as long as they still fit.
// Greedy by value density — looks reasonable, but is NOT guaranteed optimal for 0/1 items.
public int knapsackGreedyByDensity(int[] weights, int[] values, int capacity) {
int n = weights.length;
Integer[] indices = new Integer[n];
for (int i = 0; i < n; i++) indices[i] = i;
// sort item indices by value/weight ratio, descending
Arrays.sort(indices, (a, b) ->
Double.compare((double) values[b] / weights[b], (double) values[a] / weights[a]));
int totalValue = 0;
int remaining = capacity;
for (int idx : indices) {
if (weights[idx] <= remaining) {
totalValue += values[idx];
remaining -= weights[idx];
}
}
return totalValue;
}
Counter-example that breaks it: capacity = 10. Item A: weight 6, value 12 (density 2.0). Item B: weight 5, value 9 (density 1.8). Item C: weight 5, value 9 (density 1.8). Greedy picks A first (highest density) using 6 of the 10 capacity, leaving only 4 — not enough for B or C — for a total value of 12. But picking B and C together costs weight 10 (exactly the capacity) for a total value of 18 — the true optimum, which greedy never even considers because it committed to A and can't undo that choice.
Time complexity: O(n log n) — the sort dominates; the pass afterward is O(n). Space complexity: O(n) for the index array. But the complexity is irrelevant here since the answer can be wrong — this is precisely why runtime alone never justifies an algorithm; correctness comes first.
Recognizing Which Applies
| Signal | Likely approach |
|---|---|
| Choosing an item excludes/limits future choices in a way that can be proven never regretted (exchange argument holds) | Greedy is safe — usually faster and simpler than DP |
| A locally-best choice can lock you out of a better global combination (as with 0/1 knapsack) | DP — needs to compare all relevant combinations, not commit early |
| Interval/scheduling problems (activity selection, minimum meeting rooms) | Greedy, typically after sorting by end time |
| "Fractional" version of a resource-allocation problem (can take part of an item) | Often greedy works even when the 0/1 version needs DP — Fractional Knapsack is the classic example |