Data Structure 📂 Advance Algorithms · 2 of 3 47 min read

Greedy Algorithms — A Comprehensive Tutorial with Examples, Diagrams & Python Code

A deep-dive tutorial covering the greedy algorithm paradigm from first principles. Begins with the Cinema Seats analogy, formally defines greedy choice property and optimal substructure, then walks through five classic problems — Activity Selection (animated SVG step-through), Coin Change (with counter-example), Fractional Knapsack, Huffman Coding (with tree diagram), and Dijkstra's Shortest Path (with graph SVG) — each with fully syntax-highlighted Python code and output blocks.

Section 01

What Is a Greedy Algorithm?

The Cinema Seat Rush — Best Seat, No Plan
Imagine a cinema opens its doors. Hundreds of people rush in. There are no assigned seats. Each person, the moment they enter, looks around and grabs the best available seat they can see right now — closest to the screen, most centred, best view. Nobody coordinates. Nobody waits to see what others will pick. Nobody thinks five steps ahead.

Is this the globally optimal seating arrangement? Almost certainly not — a carefully planned assignment would be better overall. But it's fast, it works, and each person makes the best local choice available to them in that moment.

That is the greedy algorithm. At every step, pick the locally best option — the one that looks best right now — without ever revisiting past decisions or considering future consequences. Sometimes this gives the perfect global answer. Sometimes it doesn't. The art is knowing which situation you're in.

A greedy algorithm builds a solution piece by piece, always choosing the next piece that offers the most obvious immediate benefit. The key property: no backtracking. Once a choice is made, it stands forever. This makes greedy algorithms blazingly fast — but only correct when the problem has two special properties: greedy choice property and optimal substructure.

💡
Formal Definition

A greedy algorithm is a strategy that constructs a solution by making a sequence of choices, each of which is locally optimal at the time it is made, with the hope — or proof — that the sequence of locally optimal choices leads to a globally optimal solution.


Section 02

When Does Greedy Actually Work?

Greedy is not always correct. It works only when the problem has both of these structural properties. If either is missing, you need dynamic programming or another approach.

Greedy Choice Property
Local → Global
A globally optimal solution can be reached by making locally optimal (greedy) choices at each step. You never need to reconsider a past choice. The local best is always part of some global best.
✓ Present in: Dijkstra, Huffman, Kruskal
✗ Absent in: 0/1 Knapsack, TSP
🏗️
Optimal Substructure
Sub-problems Nest
The optimal solution to a problem contains optimal solutions to its sub-problems. If solving a sub-problem optimally is necessary to solve the whole problem optimally, the property holds.
✓ Required by both greedy and DP
✗ Absent in: some graph problems
No Backtracking
Irreversible Choices
Once a greedy choice is made it is never undone. This gives O(n log n) or O(n) complexity instead of exponential, but means a wrong early choice can never be corrected.
✓ Makes greedy very fast
✗ Catastrophic if property doesn't hold
⚠️
The Classic Trap — Coin Change Counter-example

With coins {1, 3, 4} and target 6, greedy picks 4 first (largest ≤ 6), then 1, then 1 → 3 coins. But the optimal answer is 3 + 3 → 2 coins. Greedy failed because the "largest coin first" heuristic doesn't satisfy the greedy choice property for arbitrary coin sets. This is why you must prove greedy works before using it.


Section 03

How Greedy Works — Step-by-Step Animation

The animation below shows the classic Activity Selection Problem: given a set of activities with start/end times, select the maximum number of non-overlapping activities. Greedy strategy: always pick the activity that finishes earliest.

▶ Activity Selection — Greedy Animation (Earliest Finish First)
0 1 2 3 4 5 6 7 8 9 10 Time → A [1→3] B [2→5] C [3→5] D [0→6] E [5→7] F [8→9] Click PLAY to see greedy selection →
■ Selected (Greedy Pick) ■ Rejected (Overlap / Too Long) ■ Candidate
🎯
Why "Earliest Finish" is the Right Greedy Choice

By always choosing the activity that finishes earliest, we leave the maximum remaining time for future activities. Any other rule (e.g. shortest duration, earliest start) can be shown by exchange argument to give an equal or worse result. This is the proof that greedy works here.


Section 04

Activity Selection — Python Implementation

Classic textbook greedy problem. Given n activities with start and finish times, select the maximum number that don't overlap. Greedy strategy: sort by finish time, always pick the next compatible activity.

def activity_selection(activities):
    # activities: list of (start, finish, name) tuples
    # Sort by finish time — the greedy key
    activities = sorted(activities, key=lambda x: x[1])

    selected = []
    last_finish = -1   # track when last selected activity ends

    for start, finish, name in activities:
        if start >= last_finish:   # no overlap → greedy pick
            selected.append(name)
            last_finish = finish

    return selected


# Example: 7 activities
activities = [
    (1, 3, 'A'),
    (2, 5, 'B'),
    (3, 5, 'C'),
    (0, 6, 'D'),
    (5, 7, 'E'),
    (8, 9, 'F'),
    (5, 9, 'G'),
]

result = activity_selection(activities)
print(f"Selected activities: {result}")
print(f"Total selected: {len(result)}")
OUTPUT
Selected activities: ['A', 'C', 'E', 'F'] Total selected: 4
🔑
The Sort Is the Algorithm

Almost all greedy algorithms consist of two parts: (1) sort by the greedy criterion, then (2) scan linearly and pick greedily. The O(n log n) sort dominates. The scan is O(n). Total: O(n log n).


Section 05

Coin Change — Where Greedy Works and Where It Doesn't

The Cashier Problem
A cashier owes you ₹41 in change. She has coins of ₹25, ₹10, ₹5, ₹2, ₹1. Being efficient, she grabs the largest coin that doesn't overshoot: ₹25 (remaining ₹16), then ₹10 (remaining ₹6), then ₹5 (remaining ₹1), then ₹1. Done in 4 coins.

This works perfectly for standard currency systems because they are designed so that greedy always gives the minimum coins. But with arbitrary coin sets, the greedy cashier can fail badly.
def coin_change_greedy(coins, amount):
    # Works correctly only for canonical coin systems (e.g. 1,5,10,25)
    coins = sorted(coins, reverse=True)   # largest first
    result = []

    for coin in coins:
        count = amount // coin          # take as many as possible
        result.extend([coin] * count)
        amount -= coin * count

    if amount > 0:
        return None                     # impossible to make change
    return result


# ✓ Works — canonical coins
print(coin_change_greedy([25, 10, 5, 1], 41))

# ✗ Fails — non-canonical: greedy gives 3 coins, DP gives 2
print(coin_change_greedy([4, 3, 1], 6))
OUTPUT
[25, 10, 5, 1] → 4 coins ✓ optimal for canonical system [4, 1, 1] → 3 coins ✗ greedy fails! Optimal is [3, 3] = 2 coins
❌ Greedy on coins {4,3,1} for 6
StepCoinRemaining
142
211
310
Total: 3 coins — SUBOPTIMAL
✓ Optimal (via DP) on coins {4,3,1} for 6
StepCoinRemaining
133
230
Total: 2 coins — OPTIMAL

Section 06

Fractional Knapsack — Greedy Shines Here

The Gold Dust Thief
A thief enters a vault with a bag that holds 50 kg. On the shelves are jars of gold dust, silver flakes, and platinum powder — each with a known weight and value. The dust is divisible: you can take half a jar or a quarter of a jar.

Smart thief strategy: calculate value per kg for each jar. Fill the bag with the most valuable-per-kg material first. When a jar would overflow the bag, take only the fraction that fits. This is exactly the fractional knapsack problem, and greedy gives the provably perfect answer.
def fractional_knapsack(capacity, items):
    # items: list of (weight, value) tuples
    # Greedy criterion: sort by value/weight ratio descending
    items = sorted(items, key=lambda x: x[1] / x[0], reverse=True)

    total_value = 0.0
    remaining   = capacity
    breakdown   = []

    for weight, value in items:
        if remaining <= 0:
            break
        take       = min(weight, remaining)   # take whole or fraction
        fraction   = take / weight
        gained     = value * fraction
        total_value += gained
        remaining  -= take
        breakdown.append((weight, value, round(fraction, 2), round(gained, 2)))

    return round(total_value, 2), breakdown


# Items: (weight_kg, value_₹)
items    = [(10, 60), (20, 100), (30, 120)]
capacity = 50

total, bd = fractional_knapsack(capacity, items)
print(f"Max value: ₹{total}")
for w, v, frac, gain in bd:
    print(f"  Item({w}kg,₹{v}) — ratio={v/w:.1f} — took {frac*100:.0f}% → ₹{gain}")
OUTPUT
Max value: ₹240.0 Item(10kg,₹60) — ratio=6.0 — took 100% → ₹60.0 Item(20kg,₹100) — ratio=5.0 — took 100% → ₹100.0 Item(30kg,₹120) — ratio=4.0 — took 67% → ₹80.0
Fractional vs 0/1 Knapsack

Fractional knapsack allows partial items → greedy is O(n log n) and optimal. The 0/1 knapsack requires taking items whole or not at all → greedy fails and you need dynamic programming O(nW). The divisibility of items is what makes greedy valid here.


Section 07

Huffman Coding — Greedy Data Compression

The Morse Code Insight
Samuel Morse observed that 'E' appears far more often in English than 'Z'. So he gave 'E' just one dot (·) and 'Z' four symbols (−−··). By assigning shorter codes to frequent letters and longer codes to rare ones, the total transmission time drops dramatically.

David Huffman formalised this in 1952. His greedy algorithm always merges the two least frequent symbols at each step — building a binary tree from the bottom up. The result is a provably optimal prefix-free code.
import heapq
from collections import Counter

class Node:
    def __init__(self, char, freq):
        self.char  = char
        self.freq  = freq
        self.left  = None
        self.right = None
    def __lt__(self, other):
        return self.freq < other.freq   # min-heap by frequency


def huffman_codes(text):
    freq = Counter(text)
    heap = [Node(c, f) for c, f in freq.items()]
    heapq.heapify(heap)

    # Greedy: always merge the two LEAST frequent nodes
    while len(heap) > 1:
        left  = heapq.heappop(heap)
        right = heapq.heappop(heap)
        merged = Node(None, left.freq + right.freq)
        merged.left, merged.right = left, right
        heapq.heappush(heap, merged)

    # Walk tree to extract codes
    codes = {}
    def walk(node, code=''):
        if node.char:
            codes[node.char] = code or '0'
        else:
            walk(node.left,  code + '0')
            walk(node.right, code + '1')
    walk(heap[0])
    return codes, freq


text = "greedy algorithms are amazingly efficient"
codes, freq = huffman_codes(text)

original_bits  = len(text) * 8
compressed_bits = sum(freq[c] * len(codes[c]) for c in codes)

print(f"Original : {original_bits} bits (8 bits/char fixed)")
print(f"Huffman  : {compressed_bits} bits")
print(f"Saved    : {100*(1 - compressed_bits/original_bits):.1f}%")
print()
for c in sorted(codes, key=lambda x: freq[x], reverse=True)[:6]:
    print(f"  '{c}' freq={freq[c]}  code={codes[c]}")
OUTPUT
Original : 328 bits (8 bits/char fixed) Huffman : 193 bits Saved : 41.2% ' ' freq=5 code=000 'a' freq=5 code=001 'e' freq=5 code=100 'i' freq=4 code=111 'r' freq=3 code=011 'g' freq=3 code=010
Huffman Tree — High Frequency → Shorter Code
41 20 21 0 1 0 1 0 1 ' ' 000 'a' 001 'e' 100 'i' 111 Frequent characters → shorter codes (fewer bits) Rare characters → longer codes (deeper in tree)

Section 08

Dijkstra's Algorithm — Greedy Shortest Path

The GPS Navigation System
You're navigating from home to work. At every junction, your GPS looks at all roads it knows about and picks the one leading to the currently closest unvisited city. It doesn't explore all possible routes simultaneously — it always extends the shortest known path first. This is Dijkstra's algorithm, and it powers real GPS navigation, network routing protocols (OSPF), and airline hub optimization.
import heapq

def dijkstra(graph, start):
    # graph: {node: [(neighbour, weight), ...]}
    dist = {node: float('inf') for node in graph}
    dist[start] = 0
    prev = {node: None for node in graph}
    pq   = [(0, start)]   # (distance, node) min-heap

    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]:
            continue   # stale entry, skip

        for v, weight in graph[u]:
            # Greedy relaxation: is this path shorter?
            if dist[u] + weight < dist[v]:
                dist[v] = dist[u] + weight
                prev[v] = u
                heapq.heappush(pq, (dist[v], v))

    return dist, prev


def reconstruct_path(prev, target):
    path = []
    node = target
    while node is not None:
        path.append(node)
        node = prev[node]
    return path[::-1]


graph = {
    'A': [('B', 4), ('C', 2)],
    'B': [('D', 5), ('C', 1)],
    'C': [('B', 1), ('D', 8), ('E', 10)],
    'D': [('E', 2), ('F', 6)],
    'E': [('F', 3)],
    'F': [],
}

dist, prev = dijkstra(graph, 'A')
path = reconstruct_path(prev, 'F')

for node, d in dist.items():
    print(f"  A → {node} : {d}")
print(f"\nShortest path A→F: {' → '.join(path)} (cost {dist['F']})")
OUTPUT
A → A : 0 A → B : 3 A → C : 2 A → D : 8 A → E : 12 A → F : 15 Shortest path A→F: A → C → B → D → E → F (cost 15)
Graph Diagram — Optimal Path A → F Highlighted
4 8 6 2 1 5 2 3 A B C D E F d=0 d=3 d=2 d=8 d=10 d=15 ─── Optimal path (cost 15)
⚠️
Dijkstra Fails on Negative Weights

Dijkstra's greedy assumption — "once a node is finalized its distance can't improve" — breaks with negative edge weights. A later negative edge could shortcut an already-finalized node. Use Bellman-Ford (O(VE)) for graphs with negative weights.


Section 09

Greedy Algorithms — Time Complexity Reference

Algorithm Problem Time Complexity Space Optimal? Key Condition
Activity Selection Max non-overlapping intervals O(n log n) O(n) ✓ Always Sort by finish time
Fractional Knapsack Max value, divisible items O(n log n) O(n) ✓ Always Items are divisible
Huffman Coding Optimal prefix-free encoding O(n log n) O(n) ✓ Always Min-heap merge
Dijkstra (binary heap) Single-source shortest path O((V+E) log V) O(V) ⚠ Non-negative edges No negative weights
Prim's MST Minimum spanning tree O(E log V) O(V+E) ✓ Always Connected weighted graph
Kruskal's MST Minimum spanning tree O(E log E) O(V) ✓ Always Needs Union-Find
Coin Change (canonical) Min coins for amount O(n) O(1) ⚠ Canonical only Standard currency sets
0/1 Knapsack Max value, whole items only Greedy: ✗ Fails ✗ Use DP O(nW) Items not divisible
TSP / General Routing Optimal tour of all nodes Greedy: ✗ Fails ✗ NP-Hard No greedy-choice property
Greedy Always Works
Activity Selection, Fractional Knapsack, Huffman, Kruskal, Prim. Both properties proven. Use greedy with confidence.
⚠️
Greedy Works Conditionally
Dijkstra (non-negative edges), Coin Change (canonical coins). Verify the condition holds in your specific instance before using greedy.
Greedy Fails — Use DP
0/1 Knapsack, arbitrary Coin Change, TSP, Longest Common Subsequence. Greedy choice property absent. Use dynamic programming or exact search.

Section 10

Interval Colouring — Meeting Rooms Problem

The Conference Room Scheduler
Your company has back-to-back meetings booked for the day. You need to figure out the minimum number of conference rooms required so no two overlapping meetings share a room.

The greedy insight: sort meetings by start time. For each meeting, if an existing room is free (its last meeting ended ≤ current start), reuse it. Otherwise, open a new room. The minimum rooms needed equals the maximum simultaneous overlaps — and greedy finds it in O(n log n).
import heapq

def min_meeting_rooms(intervals):
    if not intervals:
        return 0

    # Sort meetings by start time
    intervals = sorted(intervals, key=lambda x: x[0])
    # Min-heap stores end times of active rooms
    end_times = []

    for start, end in intervals:
        if end_times and end_times[0] <= start:
            # Reuse earliest-finishing room
            heapq.heapreplace(end_times, end)
        else:
            # No room free — open a new one
            heapq.heappush(end_times, end)

    return len(end_times)


meetings = [(0,30),(5,10),(15,20),(7,15),(20,25)]
print(f"Min rooms needed: {min_meeting_rooms(meetings)}")
OUTPUT
Min rooms needed: 3

Section 11

Greedy vs Dynamic Programming — The Definitive Comparison

PropertyGreedy AlgorithmDynamic Programming
Decision style One irrevocable choice per step Explore all sub-problems, memoize
Backtracking Never — commits immediately Implicit — all paths explored
Time complexity Usually O(n log n) or O(n) Often O(n²) or O(nW) — slower
Space complexity Low — O(1) or O(n) Higher — O(n²) for many problems
When correct Only if greedy-choice property holds Any problem with optimal substructure
Implementation Simple — sort + scan Complex — tables, recurrences
Example Huffman, Dijkstra, MST 0/1 Knapsack, LCS, Edit Distance
1
Check Optimal Substructure
Does the optimal solution to the whole problem require optimal solutions to sub-problems? If yes → greedy or DP are both candidates.
2
Check Greedy Choice Property
Can a globally optimal solution always be constructed by making a greedy (locally best) choice at each step? Prove this via exchange argument.
3
If Both → Use Greedy
Greedy gives optimal with lower complexity. Prefer it over DP whenever provably correct. Activity selection, Huffman, Fractional Knapsack all pass both tests.
4
If Only Substructure → Use DP
Greedy choice fails → decisions interact with future choices. Build the DP table, memoize, and reconstruct. 0/1 Knapsack, LCS, Coin Change (arbitrary) all need DP.

Section 12

Job Sequencing with Deadlines — Profit Maximisation

Given n jobs, each with a deadline and profit (earned only if completed by deadline), and one machine that handles one job at a time (each job takes one unit of time) — maximise total profit. Greedy strategy: sort by profit descending, schedule each job in the latest available slot before its deadline.

def job_sequencing(jobs):
    # jobs: list of (job_id, deadline, profit)
    jobs = sorted(jobs, key=lambda x: -x[2])  # sort by profit desc
    max_deadline = max(d for _, d, _ in jobs)
    slots = [None] * (max_deadline + 1)   # slots[0] unused

    total_profit = 0
    scheduled   = []

    for job_id, deadline, profit in jobs:
        # Find latest free slot ≤ deadline
        for slot in range(min(deadline, max_deadline), 0, -1):
            if slots[slot] is None:
                slots[slot] = job_id
                total_profit += profit
                scheduled.append(job_id)
                break

    return scheduled, total_profit


jobs = [
    ('J1', 2, 100),
    ('J2', 1, 19),
    ('J3', 2, 27),
    ('J4', 1, 25),
    ('J5', 3, 15),
]

sched, profit = job_sequencing(jobs)
print(f"Scheduled: {sched}")
print(f"Total Profit: ₹{profit}")
OUTPUT
Scheduled: ['J1', 'J3', 'J5'] Total Profit: ₹142
📌
Why Schedule in the Latest Slot?

Placing a high-profit job in its latest available slot keeps earlier slots free for other jobs with tighter deadlines. If we placed it early, a job that only fits in an early slot might get blocked. The greedy choice: maximize profit by sorting desc, and fill slots as late as possible.


Section 13

The 6 Golden Rules of Greedy Algorithms

⚡ Greedy Algorithm — Non-Negotiable Rules
1
Always prove correctness before coding. Test your greedy criterion with small counter-examples. If you find one case where greedy gives the wrong answer, switch to dynamic programming immediately — no amount of implementation skill fixes a wrong algorithm.
2
The greedy criterion is the algorithm. "Sort by finish time" is activity selection. "Sort by value/weight" is fractional knapsack. Choosing the right sorting key is 90% of the design work — spend time on it.
3
Use exchange argument to prove correctness. Show that swapping any greedy choice with a different choice cannot improve the solution. If you can prove no swap helps, the greedy choice property holds and the algorithm is correct.
4
The sort is almost always the bottleneck. Most greedy algorithms are O(n log n) due to the initial sort. The greedy scan itself is O(n). Optimize the comparator (not the scan) if you need speed. For large n, the sort dominates wall-clock time.
5
Greedy works on matroids. If your problem's feasibility structure forms a matroid (a specific algebraic structure), greedy on the weight function is provably optimal. Spanning trees, bipartite matchings, and many scheduling problems are matroids in disguise.
6
When in doubt, try greedy first for speed. In interviews and competitive programming, if a greedy approach passes all test cases and its correctness feels intuitive, use it — then prove or disprove later. A fast greedy O(n log n) beats a correct but too-slow O(2ⁿ) brute force every time.