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:
- 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.
- 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:
- Merge Intervals and Insert Interval
- Non-overlapping Intervals (Erase Overlap Intervals)
- Meeting Rooms and Meeting Rooms II
- Minimum Number of Arrows to Burst Balloons
- Jump Game and Jump Game II
- Gas Station
- Task Scheduler
- Partition Labels
Complexity
| Problem | Sort by | Time | Space |
|---|---|---|---|
| merge intervals | start | O(n log n) | O(n) |
| insert into sorted intervals | already sorted | O(n) | O(n) |
| max non-overlapping intervals | end | O(n log n) | O(1) |
| minimum removals to de-overlap | end | O(n log n) | O(1) |
| minimum meeting rooms | start, heap of ends | O(n log n) | O(n) |
| minimum arrows to burst balloons | end | O(n log n) | O(1) |
| interval intersection of two lists | already sorted | O(n + m) | O(n + m) |
| employee free time | heap or sweep | O(n log n) | O(n) |
| Jump Game (reachability) | none | O(n) | O(1) |
| Jump Game II (fewest jumps) | none | O(n) | O(1) |
| task scheduler | counts | O(n) | O(1) |
| fractional knapsack | value density | O(n log n) | O(1) |
Sort By Start Or Sort By End
This single decision determines whether an interval greedy is correct.
| Goal | Sort by | Why |
|---|---|---|
| merge or union overlapping intervals | start | you need them in timeline order to extend a running block |
| count the maximum you can keep | end | finishing earliest leaves the most room for the rest |
| minimum removals to make them disjoint | end | equivalent to the maximum you can keep |
| minimum points or arrows to hit every interval | end | place the point at the earliest end, it hits the most |
| minimum resources (rooms, machines) | start, plus a min-heap of ends | you must know what has freed up |
| find intersections of two sorted lists | already sorted | two 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
| Problem | Greedy result | Correct approach |
|---|---|---|
| Coin Change, arbitrary denominations | wrong on [1, 3, 4] for 6 | DP, unbounded knapsack |
| 0/1 knapsack | density greedy is wrong when items are indivisible | DP, O(n·W) |
| longest path in a general graph | no safe local choice | NP-hard, exponential search |
| Partition Equal Subset Sum | largest-first fails | DP, subset sum |
| edit distance | no greedy edit sequence | DP, O(nm) |
| word break | longest-match-first fails | DP or memoized recursion |
| Burst Balloons | popping the smallest first fails | interval DP |
| travelling salesman | nearest-neighbor is not optimal | bitmask 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. UseInteger.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.
heapreplaceneeds a non-empty heap. Checkends and ends[0] <= startin 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.