Greedy Algorithm Explained With Simple Examples

A greedy algorithm makes the locally optimal choice at each step, hoping to find the global optimum. It's fast, simple, and works surprisingly well for many problems — often reducing O(n²) dynamic programming solutions to a single O(n log n) sort followed by a linear pass. This guide explains the core greedy-choice property, when greedy works vs fails, covers the most common interview patterns with complete code examples, and shows how to prove correctness using exchange arguments.

Local best

choice at each step — never revisits decisions

O(n log n)

typical — sort + single pass

No backtrack

committed to each decision permanently

Proof required

must verify greedy-choice property before using

1

The Core Idea — What Makes Greedy Work

The greedy intuition

Greedy is like always taking the biggest coin when making change. At each step, choose the option that looks best right now. No planning ahead, no reconsidering past decisions. Simple and fast — but only correct when the problem has the "greedy-choice property": making the locally best choice at each step leads to a globally optimal solution.

Two conditions must hold for greedy to be correct: the greedy-choice property(a global optimum can be reached by making locally optimal choices) and optimal substructure (the optimal solution contains optimal solutions to its subproblems). Without these, greedy gives a suboptimal answer — and the algorithm doesn't know.

2

When Greedy Works and When It Fails

ItemGreedy Works ✅Greedy Fails ❌
Coin change (standard denominations)US coins: always take largest coin ≤ remainingCoins [1,3,4], target 6: greedy picks 4+1+1=3 coins, optimal is 3+3=2 coins
Activity selection / interval schedulingSort by end time, take earliest-ending firstWeighted interval scheduling: need DP when intervals have values
Fractional knapsackTake items by value/weight ratio (can split items)0/1 knapsack: items can't be split, greedy fails — use DP
Dijkstra's shortest pathAlways process closest unvisited nodeNegative edges: greedy choice becomes wrong — use Bellman-Ford
Huffman codingAlways merge two smallest frequencies
Jump Game (can you reach end?)Track max reachable indexJump Game II (minimum jumps) requires careful greedy, not obvious

Greedy ≠ always optimal

Greedy algorithms require proof that local optimality leads to global optimality. For the 0/1 knapsack (items can't be split), greedy fails completely. The danger: greedy runs without error and returns a wrong answer silently. Always verify with small counterexamples before relying on greedy for a new problem type.
3

Classic 1: Activity Selection Problem

Given intervals representing activities with start and end times, find the maximum number of non-overlapping activities you can attend. The greedy insight: always pick the activity that ends earliest — this leaves the most room for future activities.

javascriptActivity Selection — Greedy by End Time
// Given intervals, find max number of non-overlapping activities
function activitySelection(intervals) {
  // Sort by end time (greedy choice: finish earliest first)
  intervals.sort((a, b) => a[1] - b[1]);

  let count = 1;
  let lastEnd = intervals[0][1];

  for (let i = 1; i < intervals.length; i++) {
    if (intervals[i][0] >= lastEnd) { // starts after last ends
      count++;
      lastEnd = intervals[i][1];
    }
  }

  return count;
}

// Walkthrough:
// Intervals sorted by end: [[1,3],[2,4],[3,5],[0,6],[5,7],[3,9],[5,9],[6,10],[8,11],[8,12],[2,14],[12,16]]
// Take [1,3]: lastEnd=3, count=1
// Skip [2,4]: starts at 2 < lastEnd=3
// Take [3,5]: starts at 3 >= lastEnd=3, count=2, lastEnd=5
// Take [5,7]: starts at 5 >= lastEnd=5, count=3, lastEnd=7
// Take [8,11]: starts at 8 >= lastEnd=7, count=4, lastEnd=11
// Take [12,16]: starts at 12 >= lastEnd=11, count=5

activitySelection([[1,3],[2,4],[3,5],[0,6],[5,7],[3,9],[5,9],[6,10],[8,11],[8,12],[2,14],[12,16]]);
// → 5
4

Classic 2: Jump Game — Can You Reach the End?

Each element in an array tells you the maximum number of steps you can jump forward from that position. The greedy key: track the furthest index reachable at each step. If you're ever at a position beyond the max reach, you're stuck.

javascriptJump Game — Greedy Maximum Reach
// Greedy: track the furthest reachable index at each position
function canJump(nums) {
  let maxReach = 0;

  for (let i = 0; i < nums.length; i++) {
    if (i > maxReach) return false; // stuck — can't reach position i
    maxReach = Math.max(maxReach, i + nums[i]); // extend reach
    if (maxReach >= nums.length - 1) return true; // reached end
  }

  return true;
}

canJump([2,3,1,1,4]); // → true  (0→1→4 or 0→2→3→4)
canJump([3,2,1,0,4]); // → false (always gets stuck at index 3, jump=0)

// Jump Game II — Minimum Jumps (Greedy Level BFS)
function jump(nums) {
  let jumps = 0;
  let currentEnd = 0;  // furthest position reachable with 'jumps' jumps
  let farthest = 0;    // furthest position reachable with 'jumps+1' jumps

  for (let i = 0; i < nums.length - 1; i++) {
    farthest = Math.max(farthest, i + nums[i]);
    if (i === currentEnd) { // used up all positions at current jump count
      jumps++;
      currentEnd = farthest;
    }
  }

  return jumps;
}

jump([2,3,1,1,4]); // → 2 (0→1→4)
jump([2,3,0,1,4]); // → 2 (0→1→4)
5

Classic 3: Minimum Arrows to Burst Balloons

javascriptMinimum Arrows to Burst Balloons — LeetCode #452
// Each balloon is an interval [start, end]. One arrow shot at x bursts all balloons where start≤x≤end.
// Find minimum arrows needed to burst all balloons.
function findMinArrowShots(points) {
  points.sort((a, b) => a[1] - b[1]); // sort by end position

  let arrows = 1;
  let arrowPos = points[0][1]; // shoot first arrow at end of first balloon

  for (let i = 1; i < points.length; i++) {
    if (points[i][0] > arrowPos) { // this balloon starts AFTER current arrow position
      arrows++;                     // need a new arrow
      arrowPos = points[i][1];      // shoot it at this balloon's end
    }
    // else: current arrow already bursts this balloon too (it overlaps)
  }

  return arrows;
}

// Walkthrough: [[10,16],[2,8],[1,6],[7,12]]
// Sorted by end: [[1,6],[2,8],[7,12],[10,16]]
// Arrow 1 at x=6: bursts [1,6] and [2,8] (both contain 6)
// [7,12]: starts at 7 > 6 → Arrow 2 at x=12: bursts [7,12] and [10,16]
// Result: 2 arrows

findMinArrowShots([[10,16],[2,8],[1,6],[7,12]]); // → 2
findMinArrowShots([[1,2],[3,4],[5,6],[7,8]]);    // → 4 (no overlaps)
6

Classic 4: Gas Station

javascriptGas Station — LeetCode #134
// n gas stations in a circle. gas[i] = gas at station i, cost[i] = cost to reach station i+1.
// Find starting station for a full circuit, or return -1.
function canCompleteCircuit(gas, cost) {
  let totalGas = 0;     // total surplus across all stations
  let currentGas = 0;   // current surplus from the candidate start
  let start = 0;        // candidate starting station

  for (let i = 0; i < gas.length; i++) {
    const surplus = gas[i] - cost[i];
    totalGas += surplus;
    currentGas += surplus;

    // If we can't reach station i+1 from current 'start', reset
    if (currentGas < 0) {
      start = i + 1;    // try starting from the next station
      currentGas = 0;   // reset running surplus
    }
  }

  // If total gas < total cost, no solution exists
  return totalGas >= 0 ? start : -1;
}

// Key insight: if total gas >= total cost, a solution always exists.
// The greedy: if we run out of gas at station k, no station between
// start and k can be the starting point (they'd run out even sooner).

canCompleteCircuit([1,2,3,4,5], [3,4,5,1,2]); // → 3
canCompleteCircuit([2,3,4], [3,4,3]);          // → -1
7

Classic 5: Fractional Knapsack

0/1 Knapsack vs Fractional Knapsack — When Greedy Works

0/1 Knapsack — use Dynamic Programming

❌ Bad
// ❌ 0/1 Knapsack: items can't be split — greedy fails
// Items: [{weight:3, value:4}, {weight:4, value:5}, {weight:5, value:6}], capacity=7
// Greedy by value/weight ratio: 4/3=1.33, 5/4=1.25, 6/5=1.2
// Takes item0 (3kg,4val) + item1 (4kg,5val) = 7kg, value=9
// But optimal: item1 (4kg,5val) + item2 (3kg of item2 at 6/5=3.6) — wrong for 0/1
// Actually 0/1 optimal: item0+item2=3+5=8kg>7 too heavy; item1+item2=9kg>7 too heavy
// Only item0+item1=7kg, value=9 OR item2 alone=5kg, value=6
// In THIS case greedy gets the right answer but it's not always correct for 0/1

Fractional Knapsack — greedy by value/weight ratio is provably optimal

✅ Good
// ✅ Fractional Knapsack: can take fractions — greedy by value/weight ratio is optimal
function fractionalKnapsack(items, capacity) {
  // Sort by value-to-weight ratio (greedy choice)
  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;

    const take = Math.min(item.weight, remaining); // take all or partial
    totalValue += take * (item.value / item.weight);
    remaining -= take;
  }

  return totalValue;
}

const items = [
  { weight: 10, value: 60 },  // ratio: 6
  { weight: 20, value: 100 }, // ratio: 5
  { weight: 30, value: 120 }, // ratio: 4
];

fractionalKnapsack(items, 50); // → 240 (all of item0, all of item1, 20/30 of item2)
8

Greedy vs Dynamic Programming

Use Greedy When

Problem has greedy-choice property (proven or well-known). Keywords: scheduling, intervals, minimum/maximum single dimension. Exchange argument shows greedy is safe. O(n log n) preferred over O(n²) DP.

Use Dynamic Programming When

Choices depend on future or previous choices. 0/1 selection (can't partially take items). Multiple constraints simultaneously. Counting ways rather than optimizing value.

The Decision Process

Try greedy first — sketch an exchange argument. If you can always swap a non-greedy choice for the greedy one without making it worse, greedy is correct. If you find a counterexample with small inputs, switch to DP.

Common Greedy Interview Patterns

Sort by end time (interval scheduling). Sort by ratio (fractional knapsack, task scheduling). Track running maximum (jump game). Merge smallest (Huffman). Always-reset start (gas station).

9

How to Prove a Greedy Algorithm Correct

1

State the greedy choice

Define precisely what "locally optimal" means at each step. Example: "always pick the activity with the earliest end time."

2

Exchange argument

Assume an optimal solution OPT differs from greedy solution G. Find the first position where they differ. Show you can swap OPT's choice for G's choice without making the solution worse.

3

Repeat the swap

Apply the exchange repeatedly until OPT looks exactly like G. This shows G is at least as good as any other solution — therefore G is optimal.

4

Verify optimal substructure

After making the greedy choice, confirm the remaining subproblem is the same type as the original. The same greedy choice applies recursively.

5

Test with counterexamples

Try small inputs where greedy might fail. If you find a counterexample, greedy is wrong. Try arbitrary coin denominations, items with specific weight/value ratios, or graphs with unusual structure.

Frequently Asked Questions