Greedy Algorithms

intermediate greedy algorithm interval-scheduling

A greedy algorithm makes the best-looking choice at each step, hoping that locally optimal decisions lead to a globally optimal solution. No going back, no reconsidering. Just pick what looks best right now and move on.

In simple language, it’s like eating at a buffet where we always grab the most delicious item in front of us. Sometimes that strategy works perfectly. Sometimes we end up with five desserts and no actual meal.

When Does Greedy Work?

Greedy works when two conditions hold:

  1. Greedy choice property — a locally optimal choice leads to a globally optimal solution
  2. Optimal substructure — an optimal solution contains optimal solutions to subproblems

The tricky part? Not every problem has these properties. That’s why greedy doesn’t always work, and why interviewers love asking us to prove it does.

Greedy vs DP -- When to Use What?
Greedy
Make one choice per step, never look back. O(n) or O(n log n) usually.
DP
Try all choices per step, cache results. O(n^2) or O(n*m) usually.
Rule of thumb: if greedy gives the wrong answer on a small example, we need DP. Always test with a counterexample first.

Activity Selection / Interval Scheduling

This is THE classic greedy problem. Given a list of activities with start and end times, pick the maximum number of non-overlapping activities.

The greedy insight: always pick the activity that ends earliest. This leaves the most room for future activities.

function activitySelection(activities) {
  // sort by end time
  activities.sort((a, b) => a[1] - b[1]);
  const selected = [activities[0]];
  let lastEnd = activities[0][1];

  for (let i = 1; i < activities.length; i++) {
    if (activities[i][0] >= lastEnd) { // no overlap
      selected.push(activities[i]);
      lastEnd = activities[i][1];
    }
  }
  return selected;
}
// activitySelection([[1,3],[2,5],[3,6],[5,7],[8,9]])
// => [[1,3],[3,6],[8,9]] -- 3 activities, no overlaps
def activity_selection(activities):
    # sort by end time
    activities.sort(key=lambda x: x[1])
    selected = [activities[0]]
    last_end = activities[0][1]

    for start, end in activities[1:]:
        if start >= last_end:  # no overlap
            selected.append([start, end])
            last_end = end
    return selected

# activity_selection([[1,3],[2,5],[3,6],[5,7],[8,9]])
# => [[1,3],[3,6],[8,9]] -- 3 activities, no overlaps
static List<int[]> activitySelection(int[][] activities) {
    // sort by end time
    Arrays.sort(activities, (a, b) -> a[1] - b[1]);
    List<int[]> selected = new ArrayList<>();
    selected.add(activities[0]);
    int lastEnd = activities[0][1];

    for (int i = 1; i < activities.length; i++) {
        if (activities[i][0] >= lastEnd) { // no overlap
            selected.add(activities[i]);
            lastEnd = activities[i][1];
        }
    }
    return selected;
}

Time: O(n log n) for sorting. The greedy pass itself is O(n). Space: O(n) for the result.

Jump Game

Given an array where each element tells us the maximum we can jump from that position, determine if we can reach the last index (LeetCode #55).

The greedy insight: track the farthest position we can reach. If we ever land on a position beyond our farthest reach, we’re stuck.

function canJump(nums) {
  let farthest = 0;
  for (let i = 0; i < nums.length; i++) {
    if (i > farthest) return false; // can't reach here
    farthest = Math.max(farthest, i + nums[i]);
  }
  return true;
}
// canJump([2,3,1,1,4]) => true  (0->1->4 or 0->2->3->4)
// canJump([3,2,1,0,4]) => false (stuck at index 3)
def can_jump(nums):
    farthest = 0
    for i in range(len(nums)):
        if i > farthest:
            return False  # can't reach here
        farthest = max(farthest, i + nums[i])
    return True

# can_jump([2,3,1,1,4]) => True
# can_jump([3,2,1,0,4]) => False
static boolean canJump(int[] nums) {
    int farthest = 0;
    for (int i = 0; i < nums.length; i++) {
        if (i > farthest) return false; // can't reach here
        farthest = Math.max(farthest, i + nums[i]);
    }
    return true;
}
// canJump([2,3,1,1,4]) => true
// canJump([3,2,1,0,4]) => false

Time: O(n), Space: O(1). One pass through the array. Beautiful.

Gas Station

There are n gas stations in a circle. Station i has gas[i] fuel and costs cost[i] to reach the next station. Find the starting station that lets us complete the full circuit, or return -1 (LeetCode #134).

The greedy insight: if the total gas >= total cost, a solution must exist. And if we can’t reach station j from station i, then no station between i and j can be the start either. So we reset our start to j+1.

function canCompleteCircuit(gas, cost) {
  let totalTank = 0, currTank = 0, start = 0;
  for (let i = 0; i < gas.length; i++) {
    const diff = gas[i] - cost[i];
    totalTank += diff;
    currTank += diff;
    if (currTank < 0) { // can't reach next station
      start = i + 1;    // reset start to next station
      currTank = 0;
    }
  }
  return totalTank >= 0 ? start : -1;
}
def can_complete_circuit(gas, cost):
    total_tank = curr_tank = start = 0
    for i in range(len(gas)):
        diff = gas[i] - cost[i]
        total_tank += diff
        curr_tank += diff
        if curr_tank < 0:  # can't reach next station
            start = i + 1  # reset start to next station
            curr_tank = 0
    return start if total_tank >= 0 else -1
static int canCompleteCircuit(int[] gas, int[] cost) {
    int totalTank = 0, currTank = 0, start = 0;
    for (int i = 0; i < gas.length; i++) {
        int diff = gas[i] - cost[i];
        totalTank += diff;
        currTank += diff;
        if (currTank < 0) { // can't reach next station
            start = i + 1;  // reset start to next station
            currTank = 0;
        }
    }
    return totalTank >= 0 ? start : -1;
}

Time: O(n), Space: O(1). Single pass with two running sums.

Proving Greedy Correctness in Interviews

Interviewers often ask: “How do we know greedy works here?” There are two standard approaches:

1. Exchange Argument

Show that swapping any non-greedy choice with a greedy one never makes the solution worse.

“If our greedy picks activity A (earliest end), and some optimal solution picks activity B instead, we can swap B for A. Since A ends earlier, everything after still fits. So the greedy solution is at least as good.”

2. Greedy Stays Ahead

Show that at every step, the greedy solution is at least as good as any other.

“After picking k activities, greedy’s last end time is <= any other strategy’s last end time. Proof by induction on k.”

In simple language, we don’t need a formal proof in an interview. But we do need to say something like: “The greedy choice never blocks a better option later, because…” and give an intuitive argument. If we can’t articulate why greedy works, we should probably use DP instead.

Common Greedy Problems to Practice

ProblemGreedy StrategyComplexity
Activity SelectionPick earliest end timeO(n log n)
Jump GameTrack farthest reachableO(n)
Gas StationReset start when tank < 0O(n)
Assign CookiesSort both, match smallestO(n log n)
Best Time to Buy/Sell StockTrack min price so farO(n)
Minimum PlatformsSort arrivals/departuresO(n log n)

The pattern with greedy: sort the input (usually), then make one pass making the locally best choice. If that gives the right answer, great. If not, we need DP or backtracking.