Data Structures & Algorithms Cheatsheet

Greedy and Intervals

Use this Data Structures & Algorithms reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

When To Reach For This

Greedy takes the locally best choice at every step and never revisits it. It is correct only when the problem has the greedy choice property: some optimal solution contains the locally best choice. When it holds, greedy beats DP on both time and code length. When it does not, greedy is confidently wrong.

Signals in a problem statement:

  • Intervals, meetings, or scheduling. Nearly every interval problem is greedy after a sort.
  • "Minimum number of X" to cover, remove, or partition something.
  • The input can be sorted so that the right choice becomes obvious.
  • An exchange argument works: given any optimal solution, you can swap in the greedy choice without making it worse.
  • A local invariant is enough, such as "the furthest index reachable so far" or "the smallest end time seen".

Two ways to justify a greedy in an interview:

  1. Exchange argument. Take an optimal solution that differs from greedy at the first choice, swap greedy's choice in, and show the result is still optimal.
  2. Stays-ahead argument. Show that after each step, greedy's partial solution is at least as good as any other by the relevant measure.

If neither works, it is probably DP. Coin Change with arbitrary denominations is the standard counterexample: greedy on [1, 3, 4] for 6 picks 4+1+1 (three coins) when 3+3 (two coins) is optimal.

Classic problems it solves:

  1. Merge Intervals and Insert Interval
  2. Non-overlapping Intervals (Erase Overlap Intervals)
  3. Meeting Rooms and Meeting Rooms II
  4. Minimum Number of Arrows to Burst Balloons
  5. Jump Game and Jump Game II
  6. Gas Station
  7. Task Scheduler
  8. Partition Labels

Complexity

ProblemSort byTimeSpace
merge intervalsstartO(n log n)O(n)
insert into sorted intervalsalready sortedO(n)O(n)
max non-overlapping intervalsendO(n log n)O(1)
minimum removals to de-overlapendO(n log n)O(1)
minimum meeting roomsstart, heap of endsO(n log n)O(n)
minimum arrows to burst balloonsendO(n log n)O(1)
interval intersection of two listsalready sortedO(n + m)O(n + m)
employee free timeheap or sweepO(n log n)O(n)
Jump Game (reachability)noneO(n)O(1)
Jump Game II (fewest jumps)noneO(n)O(1)
task schedulercountsO(n)O(1)
fractional knapsackvalue densityO(n log n)O(1)

Sort By Start Or Sort By End

This single decision determines whether an interval greedy is correct.

GoalSort byWhy
merge or union overlapping intervalsstartyou need them in timeline order to extend a running block
count the maximum you can keependfinishing earliest leaves the most room for the rest
minimum removals to make them disjointendequivalent to the maximum you can keep
minimum points or arrows to hit every intervalendplace the point at the earliest end, it hits the most
minimum resources (rooms, machines)start, plus a min-heap of endsyou must know what has freed up
find intersections of two sorted listsalready sortedtwo pointers, advance the one that ends first
def merge_intervals(intervals):
    """Sort by START. O(n log n)."""
    if not intervals:
        return []
    intervals.sort(key=lambda x: x[0])
    out = [list(intervals[0])]
    for start, end in intervals[1:]:
        if start <= out[-1][1]:             # overlap or touch
            out[-1][1] = max(out[-1][1], end)
        else:
            out.append([start, end])
    return out

def insert_interval(intervals, new):
    """intervals is already sorted and disjoint. O(n), no sort needed."""
    out, i, n = [], 0, len(intervals)
    while i < n and intervals[i][1] < new[0]:
        out.append(intervals[i])            # entirely before
        i += 1
    start, end = new
    while i < n and intervals[i][0] <= end:
        start = min(start, intervals[i][0])  # absorb every overlap
        end = max(end, intervals[i][1])
        i += 1
    out.append([start, end])
    out.extend(intervals[i:])
    return out

def max_non_overlapping(intervals):
    """Sort by END. The classic activity-selection greedy."""
    if not intervals:
        return 0
    intervals.sort(key=lambda x: x[1])
    count, last_end = 0, float("-inf")
    for start, end in intervals:
        if start >= last_end:               # no conflict, take it
            count += 1
            last_end = end
    return count

def erase_overlap_intervals(intervals):
    """Minimum removals = total - maximum kept."""
    return len(intervals) - max_non_overlapping(intervals)

def find_min_arrows(points):
    """Minimum arrows to burst every balloon. Sort by END, shoot at it."""
    if not points:
        return 0
    points.sort(key=lambda x: x[1])
    arrows, last = 1, points[0][1]
    for start, end in points[1:]:
        if start > last:                    # touching counts as a hit
            arrows += 1
            last = end
    return arrows

Why sorting by end works for activity selection. Among all intervals compatible with what you have chosen, the one that ends earliest leaves the largest remaining window. Exchanging any optimal solution's first interval for that one cannot reduce the count, which is the exchange argument.

Resource Counting And Sweep Lines

When the answer is "how many at once", either keep a heap of end times or sweep the boundaries.

import heapq

def min_meeting_rooms(intervals):
    """Heap of end times. Sort by START. O(n log n)."""
    if not intervals:
        return 0
    intervals.sort(key=lambda x: x[0])
    ends = []
    for start, end in intervals:
        if ends and ends[0] <= start:
            heapq.heapreplace(ends, end)    # reuse the earliest-freed room
        else:
            heapq.heappush(ends, end)
    return len(ends)

def min_meeting_rooms_sweep(intervals):
    """Sweep line. Sort starts and ends separately, walk both. O(n log n)."""
    starts = sorted(s for s, _ in intervals)
    ends = sorted(e for _, e in intervals)
    rooms = best = 0
    i = j = 0
    while i < len(starts):
        if starts[i] < ends[j]:
            rooms += 1
            best = max(best, rooms)
            i += 1
        else:
            rooms -= 1
            j += 1
    return best

def car_pooling(trips, capacity):
    """Difference array on a bounded timeline. O(n + T)."""
    diff = [0] * 1001
    for num, start, end in trips:
        diff[start] += num
        diff[end] -= num
    onboard = 0
    for d in diff:
        onboard += d
        if onboard > capacity:
            return False
    return True

def interval_intersection(a, b):
    """Both sorted and disjoint. Two pointers, O(n + m)."""
    out, i, j = [], 0, 0
    while i < len(a) and j < len(b):
        lo = max(a[i][0], b[j][0])
        hi = min(a[i][1], b[j][1])
        if lo <= hi:
            out.append([lo, hi])
        if a[i][1] < b[j][1]:               # advance the one ending first
            i += 1
        else:
            j += 1
    return out

def employee_free_time(schedules):
    """Gaps common to everyone. Merge all intervals, report the holes."""
    flat = sorted((s, e) for person in schedules for s, e in person)
    out, end = [], flat[0][1]
    for s, e in flat[1:]:
        if s > end:
            out.append([end, s])
        end = max(end, e)
    return out
// Java: sort intervals with a comparator, never with subtraction
Arrays.sort(intervals, (x, y) -> Integer.compare(x[1], y[1]));   // by end
PriorityQueue<Integer> ends = new PriorityQueue<>();
for (int[] iv : intervals) {
    if (!ends.isEmpty() && ends.peek() <= iv[0]) ends.poll();
    ends.offer(iv[1]);
}
int rooms = ends.size();

Reachability And Furthest-Reach Greedy

Some greedy problems have no sort at all. The invariant is a running bound.

def can_jump(nums):
    """Jump Game. Track the furthest reachable index. O(n), O(1)."""
    reach = 0
    for i, x in enumerate(nums):
        if i > reach:
            return False                    # a gap we cannot cross
        reach = max(reach, i + x)
    return True

def min_jumps(nums):
    """Jump Game II. Implicit BFS over levels, O(n)."""
    jumps = cur_end = furthest = 0
    for i in range(len(nums) - 1):
        furthest = max(furthest, i + nums[i])
        if i == cur_end:                    # exhausted this level
            jumps += 1
            cur_end = furthest
    return jumps

def can_complete_circuit(gas, cost):
    """Gas Station. If the total is non-negative a start exists, and the
    index just after the lowest running deficit is it. O(n)."""
    if sum(gas) < sum(cost):
        return -1
    start = tank = 0
    for i in range(len(gas)):
        tank += gas[i] - cost[i]
        if tank < 0:
            start = i + 1                   # everything before i fails too
            tank = 0
    return start

def partition_labels(s):
    """Partition so each letter appears in one part only. O(n)."""
    last = {ch: i for i, ch in enumerate(s)}
    out, start, end = [], 0, 0
    for i, ch in enumerate(s):
        end = max(end, last[ch])
        if i == end:                        # nothing later reaches back
            out.append(end - start + 1)
            start = i + 1
    return out

Counting And Exchange Greedies

def task_scheduler(tasks, n):
    """Minimum time with a cooldown of n between identical tasks.
    The most frequent task sets the skeleton. O(len(tasks))."""
    from collections import Counter
    counts = Counter(tasks)
    top = max(counts.values())
    ties = sum(1 for v in counts.values() if v == top)
    return max(len(tasks), (top - 1) * (n + 1) + ties)

def fractional_knapsack(weights, values, capacity):
    """Items divisible, so greedy by value density is optimal.
    The 0/1 version is NOT, that one needs DP."""
    items = sorted(zip(weights, values), key=lambda wv: -wv[1] / wv[0])
    total = 0.0
    for w, v in items:
        if capacity >= w:
            total += v
            capacity -= w
        else:
            total += v * capacity / w       # take a fraction, then stop
            break
    return total

def largest_number(nums):
    """Concatenate for the largest value. Custom pairwise comparator."""
    from functools import cmp_to_key
    def cmp(x, y):
        if x + y > y + x:
            return -1
        if x + y < y + x:
            return 1
        return 0
    out = "".join(sorted(map(str, nums), key=cmp_to_key(cmp)))
    return "0" if out[0] == "0" else out

def two_city_scheduling(costs):
    """Send n to each city. Sort by the cost DIFFERENCE, not by cost."""
    costs.sort(key=lambda c: c[0] - c[1])
    n = len(costs) // 2
    return (sum(c[0] for c in costs[:n])
            + sum(c[1] for c in costs[n:]))

two_city_scheduling is the pure exchange argument in code: sorting by the difference means the first n items are exactly the ones that gain the most from choice A.

Where Greedy Fails

ProblemGreedy resultCorrect approach
Coin Change, arbitrary denominationswrong on [1, 3, 4] for 6DP, unbounded knapsack
0/1 knapsackdensity greedy is wrong when items are indivisibleDP, O(n·W)
longest path in a general graphno safe local choiceNP-hard, exponential search
Partition Equal Subset Sumlargest-first failsDP, subset sum
edit distanceno greedy edit sequenceDP, O(nm)
word breaklongest-match-first failsDP or memoized recursion
Burst Balloonspopping the smallest first failsinterval DP
travelling salesmannearest-neighbor is not optimalbitmask DP or approximation

Greedy is provably optimal for: activity selection, fractional knapsack, Huffman coding, Kruskal's and Prim's MST, Dijkstra on non-negative weights, and interval point covering. All of these have published exchange-argument proofs.

Gotchas

  • Sort by end, not start, for "maximum kept" and "minimum points". Sorting by start gives a plausible wrong answer that passes small tests.
  • Closed versus open endpoints. Decide whether [1, 2] and [2, 3] overlap. Meeting rooms usually say no, balloon bursting usually says yes. It changes < to <=.
  • Comparator overflow. (a, b) -> a[0] - b[0] in Java overflows on large coordinates. Use Integer.compare.
  • Mutating the input. Sorting the caller's list in place is often fine in an interview but worth calling out.
  • Empty input. Every interval routine needs a guard before touching intervals[0].
  • Prove it or test it. State the exchange argument out loud. If you cannot, write the DP instead, or test greedy against brute force on small random inputs.
  • heapreplace needs a non-empty heap. Check ends and ends[0] <= start in that order.
  • Fractional versus 0/1 knapsack. Density greedy is optimal only when items can be split. For indivisible items it is a classic wrong answer.
  • Sweep lines need consistent tie handling. When a start and an end coincide, process the end first if touching intervals do not conflict.