Stacks, binary search, heaps and greedy algorithms
Before this lesson: lesson 01 (the routine, the cost of Python operations, two pointers). Time: about 90 minutes, plus the notebook. This lesson is long: take a break after section 3.3 (binary search).
Why this matters #
These patterns appear in most coding interviews, and in each one the code is short but the
reasoning is not. Interviewers ask "why is this loop ?", "why mid + 1 and not mid?",
"why a min-heap for the largest items?" and "why is the greedy correct?". A clear invariant
answers each question. The same tools run inside ML systems: reverse-mode autodiff runs the
backward pass from a stack of recorded operations (last in, first out), np.searchsorted places
values into bins by binary search, beam search keeps the best hypotheses, vector search keeps
the nearest neighbours in a heap, and job schedulers sort and sweep intervals of time.
1. Predict first #
2. The picture #
The stack: the last open item closes first. In ( [ ] ), the ] must match the most recent
bracket that is still open. A stack is a list where you add (push) and remove (pop) only at
one end, the top: last in, first out. With one bracket type, a counter of open brackets is enough.
With several types, ( [ ) ] has balanced counts but is wrong. You must remember the order of
the open brackets, and that is what a stack stores.
The monotonic stack: wait until you are answered. Take the temperatures
[73, 74, 75, 71, 69, 72, 76, 73]. The brute force looks ahead from every day: . Instead,
keep the days that still wait for a warmer day. Their temperatures never increase from the bottom
to the top of the stack, because a warmer day would have answered a colder day below it and
removed it. Day 5 (72) answers 69 and 71, which are on top, and stops at 75. Every day is pushed
once and popped at most once.
Press Next step to run Daily Temperatures. The bars are the temperatures; the blue day is the current one, and the stack (right) holds the days that still wait. When the current day is warmer than the top, the top is popped and gets its answer. Switch on Predict to guess how many days each new day pops before you see it. Watch the counters: pushes plus pops never exceed .
Run the monotonic stack (pop while the top is colder than today) on [73, 74, 75, 71, 69, 72]. Which temperatures are on the stack at the end, from bottom to top?
74 pops 73, 75 pops 74, then 71 and 69 are pushed (both colder than the top). 72 pops 69 and 71 (both colder) and stops at 75. The stack is 75, 72: non-increasing from bottom to top, and both still wait for a warmer day.
Binary search: halve the range where the answer can be. Guess a number between 1 and 100 with questions "is it ?". Each answer halves the candidates, so 7 questions are enough (). This needs no sorted list, only a yes/no question whose answers are "no, …, no, yes, …, yes" along an ordered axis: the indices of a sorted list, or the possible answers themselves ("is speed fast enough?").
Press Next step. Each cell shows the value of the predicate (F or T). The search keeps and : everything left of is known to be F and everything from on is known to be T. The cell is the one being tested. Move and check that the result is always the first T. Then choose custom : its switches several times. The search still stops after at most calls, but it returns a switch point that is not the first T. Click cells to make monotone and run it again.
A search for the first True uses while lo < hi: mid = (lo + hi) // 2; if P(mid): hi = mid; else: lo = mid. Sometimes it never ends. Which change fixes it?
When , ; if is False, lo = mid changes nothing, forever. Since is False, cannot be the first True, so lo = mid + 1 is safe and always shrinks the range. Rounding up instead makes possible, which is outside the range and stops hi = mid from shrinking it; hi = mid - 1 can skip the answer.
Heap: know only the minimum. A binary heap is a complete binary tree stored in a plain list. Every level is full except the last one, which fills from the left. In a min-heap a parent is never larger than its children. So the root holds the minimum, and the rest is only partly ordered. That is the point: you pay only for knowing the minimum, not for a full sort.
Push and pop values. The same heap is drawn as a tree (top) and as a list (bottom); the colours link each node to its list cell, and index has children and . A push appends the value and sifts it up while it is smaller than its parent; a pop moves the last value to the root and sifts it down towards its smaller child. Count the swaps: never more than the height .
Intervals: sort once, then sweep. Draw the intervals on a time line. After you sort them by start, an interval can overlap only the block just before it, because every earlier interval started earlier. So you look at each interval once and compare it with one number: the end of the current block.
Greedy: choose, and never go back. A greedy algorithm builds the answer one choice at a time. It is correct when some optimal solution starts with the greedy choice: then the rest is a smaller copy of the same problem. If you cannot show this, the greedy is only a guess.
3. Definitions and main results #
3.1 The stack #
A stack supports push, pop (remove and return the top), top and is_empty. A Python list is a
stack: append, pop() and [-1] cost (append: amortized). pop(0) is , so never use
it in a loop; a queue needs collections.deque.
Use a stack when an item is closed by the most recent unclosed item: brackets, nested strings
like 3[a2[c]], "undo", or an operator that takes the last two operands (Reverse Polish
notation, RPN: ["2", "1", "+", "3", "*"] means ). Any recursion can be
written as a loop with an explicit stack. In Python this matters: the default recursion limit is
1,000 frames, so a recursive depth-first search on a path of nodes crashes.
def valid_brackets(s):
match = {")": "(", "]": "[", "}": "{"}
stack = []
for ch in s:
if ch in "([{":
stack.append(ch)
elif not stack or stack.pop() != match[ch]: # nothing open, or the wrong type
return False
return not stack # every bracket was closed
3.2 The monotonic stack #
Problem (next greater element). Given , find for every index the smallest index with , or "none".
Algorithm. Scan . While the stack is not empty and : pop the top index and set . Then push . The indices left on the stack at the end get "none". Daily Temperatures is this problem with (and 0 for "none").
Theorem 1 (monotonic stack). The algorithm is correct. It does pushes, at most pops and
at most comparisons: time and extra space, although a while loop sits inside
the for loop.
Invariant. Before step : (a) the stack holds exactly the indices that have no greater element in ; (b) their values do not increase from the bottom to the top.
Proof.
- Before step 0 the stack is empty and there is no index , so the invariant holds.
- Step pops indices with . By (a), no element between and is greater than , so is the first greater element: is correct.
- By (b), the indices with form a block at the top. So "pop while the top is smaller" removes exactly them, and it stops at a top with .
- The indices that stay still have no greater element up to , and has none yet. Pushing on top of a value keeps (b). So the invariant holds before step . After the last step it says that the indices left have no greater element.
- Cost (an amortized bound): each index is pushed once and popped at most once. One step can
pop many indices, but the whole run pops at most . Each comparison either causes a pop or ends
a
whileloop (at most one per step). So there are at most comparisons.
Variants. Change the comparison:
| You want, for each | Pop while | The answer comes from |
|---|---|---|
| next greater | , for each popped index | |
| next smaller | , for each popped index | |
| previous greater | the top after the pops, for | |
| previous smaller | the top after the pops, for |
One pass gives two answers: is the next element of every index it pops, and the top left after the pops is the previous element of . A monotonic deque does the same on a sliding window: pop smaller values from the back, and pop the front when its index leaves the window. It gives the maximum of every window of size in (Sliding Window Maximum).
The hard case: Largest Rectangle in Histogram
Bars of heights , each of width 1. Find the largest rectangle under the bars. The best rectangle has the height of its lowest bar , and it extends from the previous smaller bar of (exclusive) to the next smaller bar of (exclusive). One monotonic stack gives both: when pops (pop while ), is the right limit, and the new top after popping is the left limit.
def largest_rectangle(h):
stack, best = [], 0 # indices with increasing heights
for i, x in enumerate(h + [0]): # a final 0 pops everything
while stack and h[stack[-1]] >= x:
j = stack.pop()
left = stack[-1] if stack else -1
best = max(best, h[j] * (i - left - 1))
stack.append(i)
return best
print(largest_rectangle([2, 1, 5, 6, 2, 3])) # 10 (height 5, width 2)
With ties (>=), a bar may get a right limit that is too early, but the last bar of each group of
equal heights gets the full width, so the maximum is still correct. Time .
3.3 Binary search, correct by construction #
A predicate maps integers to {False, True}. It is monotone on if True implies True, so its values are False, …, False, True, …, True. We want the first True , or if all values are False.
The template (half-open range ).
def first_true(P, L, R):
lo, hi = L, R
while lo < hi:
mid = (lo + hi) // 2 # lo <= mid < hi
if P(mid):
hi = mid # mid may be the first True: keep it in the range
else:
lo = mid + 1 # mid is False, so the first True is after it
return lo
Theorem 2 (binary search). For a monotone and , the template returns after at most calls of .
Invariant. is False for every in and True for every in . (We treat itself as True.)
Proof.
- At the start both ranges are empty, so the invariant holds.
- In the loop , so :
midis always inside the open range. - If is True, monotonicity makes True on , so
hi = midkeeps the invariant. If it is False, monotonicity makes False on , solo = mid + 1keeps it. - Let . The new size is (after
hi = mid) or (afterlo = mid + 1); both are at most . So the loop ends after at most iterations. - At the end : everything before is False and everything from on is True, so .
The invariant decides every detail: < versus <= in the loop, mid versus mid + 1, and the
return value. Choose one range style and keep it.
Instances.
- Sorted list : gives the lower bound (
bisect.bisect_left), the first position where could be inserted. givesbisect_right. Their difference counts the copies of . - Binary search on the answer. = " is enough" (a speed, a capacity, a time). It is
monotone when more is never worse. Example (Koko Eating Bananas): piles
[3, 6, 7, 11]and hours; at speed a pile of bananas takes hours. " is enough" means . Search in : the answer is 4. Cost: . - Rotated sorted list (distinct values), for example
[4, 5, 6, 7, 0, 1, 2]: is False on the left block and True on the right block, so its first True is the minimum. To search for a value, note that one of the halves and is sorted; one range check tells whether the target can be in it. - Real numbers (bisection): keep False and True, and stop when . After steps the interval has length .
When is not monotone. Monotonicity is used only in step 3 of the proof. Without it, the loop still ends, and it keeps a weaker invariant: is False (or ) and is True (or ). So it returns some where switches from False to True, not necessarily the first. This can be useful: with , "the list goes down after ", the switch point is a local peak (Find Peak Element).
3.4 The binary heap #
A list heap of length stores a complete binary tree: node has the children and
and the parent (0-based, as in Python's heapq). The tree has
height .
- Invariant (min-heap):
heap[parent(i)] <= heap[i]for every . Soheap[0]is the minimum. - Push: append the new item, then sift up: swap it with its parent while it is smaller. At most one swap per level: .
- Pop: take
heap[0], move the last item to the root, then sift down: swap it with its smaller child while that child is smaller. . - Peek:
heap[0]in . Any other item needs a search. For example, the maximum of a min-heap is in a leaf, one of the indices .
Theorem 3 (heapify is linear). Building a heap from items by sifting down every internal
node, from the last one back to the root, costs .
Proof.
- A sift-down that starts at a node of height (its distance to the lowest leaf below it) makes at most swaps.
- A complete tree with nodes has at most nodes of height .
- Heights run from 0 to . Add the bounds of steps 1 and 2 over all heights, and use and .
Hence heapify costs .
In contrast, pushes cost in the worst case. In a sift-up, the many nodes near the bottom are the ones that can travel levels. In a sift-down they travel only a few levels, and the leaves (half of all nodes) do not move at all.
Python details. heapq is a min-heap on a plain list. For a max-heap, push -x and negate
again when you pop. Tuples compare field by field, so (priority, payload) compares the payloads
when two priorities are equal, and this fails for dicts; push (priority, counter, payload) with a
running counter instead. heapq.heapreplace(h, x) pops, then pushes (the result may be larger than
x); heapq.heappushpop(h, x) pushes, then pops (it returns x at once if x is the smallest).
3.5 Top- #
To keep the largest items of a stream, keep a min-heap of size . Its root is the
weakest member of the current top : the gatekeeper. A new item enters only if it beats the
root, and then it replaces the root (heapreplace). Cost: time and memory.
| Method for the largest of items | Time | Extra memory |
|---|---|---|
| sort everything | ||
heapify everything, then pops |
||
| min-heap of size | , works on a stream | |
| quickselect | average, worst | , in place |
| counts in buckets (top- frequent) |
Merge sorted lists: keep a heap with the current head of each list; pop the smallest, push
the next item of its list. for items in total (heapq.merge does this).
Running median with two heaps
Keep the smaller half of the numbers in a max-heap low (store negated values) and the larger
half in a min-heap high, with len(low) == len(high) or one more. Invariant: every value in low
is every value in high. To add : push it into low, move the maximum of low to
high, and if high is now larger, move its minimum back to low. The median is the top of low
(odd count) or the mean of the two tops (even count). Each update is , each query .
3.6 Intervals #
An interval is a pair with . Say which convention you use, because it decides the edge cases:
- closed : and overlap when and , so and overlap;
- half-open , the usual one for meetings: they overlap when and , so back-to-back meetings do not overlap.
Lemma (merging after a sort by start). Sort by start and keep the current block . If the next interval has , then no later interval overlaps the block either, because every later start is . So the block is final, and one pass merges everything: for the sort plus for the pass. The update is , not , because a short interval can sit inside a long one.
Meeting rooms. The fewest rooms that hold all meetings equals the depth: the largest number of meetings that are in progress at one moment. Sweep over the events sorted by time: at each start, at each end. With half-open meetings, an end at time comes before a start at the same time . The largest running count is the depth. (Equivalent: sort by start and keep a min-heap of the end times of the rooms in use.)
Choose merge or rooms, then press Next step. In merge mode the intervals are sorted by start; the current block grows while the next start is at most its end, and a gap closes it into the output. In rooms mode a vertical line sweeps over the start and end events; the running count is the number of rooms in use, and its maximum is the answer.
Meetings are half-open: , , , . What is the fewest number of rooms?
overlaps each of the others, so at least 2 rooms are needed. The three short meetings are back to back: with half-open intervals, ends exactly when starts, so they can share one room. The sweep processes the end at 10 before the start at 10, and the count never exceeds 2. With closed intervals, the meetings would touch at time 10 and you would get 3.
3.7 Greedy algorithms and the exchange argument #
A greedy algorithm builds a solution by a sequence of locally best choices and never revisits them. The classic example is interval scheduling: choose the largest set of pairwise non-overlapping intervals.
Theorem 4 (interval scheduling). For intervals with , sort them by end and take each interval that starts at or after the end of the last one taken. The result is a largest set of non-overlapping intervals. Time .
Proof (exchange argument, then induction).
- Let be the interval with the smallest end. Let be any optimal set, and the interval of with the smallest end.
- ends no later than . Every other interval of starts at or after the end of , so it also starts at or after the end of .
- So has no overlap and the same size: some optimal set contains .
- After taking , the remaining candidates are the intervals that start at or after the end of . This is the same problem on fewer intervals. By induction, the greedy solves it optimally.
Pick a greedy rule and compare it with the optimum. The chosen intervals are aqua; the rejected ones are grey. The readout compares the size of the greedy set with the optimum found by trying all subsets. The presets are the counterexamples for "earliest start" and "shortest first"; press Random a few times and notice that only "earliest end" always matches the optimum.
Two proof templates cover most interview greedy problems:
- Exchange argument: take any optimal solution and swap the greedy choice into it without making it worse (the proof above).
- Greedy stays ahead: show by induction that after each step the greedy partial solution is at least as good as any other, measured by one number. For interval scheduling: the -th interval taken by the greedy ends no later than the -th interval of any valid schedule.
Two one-pass greedy problems and their key facts.
- Jump Game.
nums[i]is the longest jump from index ; can you reach the last index? Keepreach= the furthest index reachable so far. Key fact: the reachable indices always form a prefix , because from any reachable index you can also stop earlier. So the answer is False exactly when some . For[3, 2, 1, 0, 4], reach stays 3 and index 4 is never reached. - Gas Station. Stations on a circle with gas and cost to drive to the next one. Key lemma: if you start at with an empty tank and the gas first runs out on the way from station to station , then no station from to can be a start either (you arrived at each of them with gas , and starting there empty only gives less). So restart at . A solution exists exactly when .
Many problems look greedy and are not. With coins and amount 6, "largest coin first" gives (3 coins), but uses 2. That problem needs dynamic programming (lesson 04). Before you trust a greedy, compare it with a brute force on many tiny random inputs.
In the exchange argument for interval scheduling, why may we replace (the first-ending interval of an optimal set ) by (the interval with the smallest end overall)?
The only fact the proof needs is . All other intervals of start at or after , hence at or after , so the swap keeps the set valid and its size. may be longer than , may start later, and may overlap more intervals; none of that matters.
Answers to section 1. (1) No. Each index is pushed once and popped at most once, so the whole run does at most pops and comparisons: (Theorem 1, step 5). (2) A min-heap of size 10. Its root is the 10th largest number so far; each new number is compared with the root and replaces it if it is larger. The cost is with , and the memory is . A max-heap of everything would need memory for all numbers. (3) Earliest end first (Theorem 4). Earliest start fails on : it takes and nothing else, but two meetings fit. Shortest first fails on : it takes , which blocks both others, but and fit together.
4. Worked example #
(a) A monotonic stack: stock spans. For each day, the span is the number of consecutive days,
ending today, with a price today's price. This is "previous strictly greater" from the variants
table: pop while ; then the span is , or if
the stack is empty. Prices [100, 80, 60, 70, 60, 75, 85]:
| day | price | popped | top after the pops | span | stack (prices, bottom → top) |
|---|---|---|---|---|---|
| 0 | 100 | none | 1 | 100 | |
| 1 | 80 | day 0 (100) | 1 | 100, 80 | |
| 2 | 60 | day 1 (80) | 1 | 100, 80, 60 | |
| 3 | 70 | 60 | day 1 (80) | 2 | 100, 80, 70 |
| 4 | 60 | day 3 (70) | 1 | 100, 80, 70, 60 | |
| 5 | 75 | 60, 70 | day 1 (80) | 4 | 100, 80, 75 |
| 6 | 85 | 75, 80 | day 0 (100) | 6 | 100, 85 |
Spans: [1, 1, 1, 2, 1, 4, 6]. Day 5 pops two days at once, but the whole run has 7 pushes and
only 5 pops.
(b) Binary search on the answer: the integer square root. Find the largest with . Use , which is monotone, on ; then .
| update | |||||
|---|---|---|---|---|---|
| 0 | 31 | 15 | 225 | True | |
| 0 | 15 | 7 | 49 | True | |
| 0 | 7 | 3 | 9 | False | |
| 4 | 7 | 5 | 25 | False | |
| 6 | 7 | 6 | 36 | True |
The loop stops with , so and , after calls of .
(c) A heap by hand. Push 5, 3, 8, 1 into an empty min-heap.
- Push 5:
[5]. - Push 3:
[5, 3]. 3 is smaller than its parent 5, so swap:[3, 5]. - Push 8:
[3, 5, 8]. 8 is larger than its parent 3, so stop. - Push 1:
[3, 5, 8, 1](index 3, parent index 1). 1 < 5, swap:[3, 1, 8, 5]. 1 < 3, swap:[1, 3, 8, 5].
Pop: return 1. Move the last item, 5, to the root: [5, 3, 8]. The children of index 0 are 3 and 8;
the smaller one, 3, is less than 5, so swap: [3, 5, 8]. Index 1 has no child
(), so stop. Result: [3, 5, 8].
(d) Interval scheduling by hand. The intervals (touching allowed) are already sorted by end.
| Interval | Start last end? | Action | Last end |
|---|---|---|---|
| take | 3 | ||
| skip | 3 | ||
| take | 5 | ||
| skip | 5 | ||
| take | 8 | ||
| skip | 8 | ||
| take | 10 |
Four intervals are kept, so the fewest removals that leave no overlap is . A brute force over all subsets agrees.
You push 5, 3, 8, 1 (in this order) into an empty heapq min-heap. What is the list now?
The heap is not sorted. 1 is appended at index 3, then sifts up past 5 (index 1) and 3 (index 0): [3, 5, 8, 1] → [3, 1, 8, 5] → [1, 3, 8, 5]. 8 never moves, because it was never smaller than its parent. A heap only promises that each parent is its children.
5. In code #
Templates for problems that are not the notebook exercises, each checked against a brute force on random inputs.
import heapq
import math
import random
from collections import Counter
from itertools import combinations
def stock_spans(prices):
"""span[i] = number of consecutive days ending at day i with a price <= prices[i]. O(n)."""
spans, stack = [], [] # stack: indices with strictly decreasing prices
for i, p in enumerate(prices):
while stack and prices[stack[-1]] <= p: # these days can never end a later span
stack.pop()
spans.append(i - stack[-1] if stack else i + 1)
stack.append(i)
return spans
def isqrt(x):
"""The largest integer r with r*r <= x, for an int x >= 0. O(log x) steps."""
lo, hi = 0, x + 1 # search [0, x+1) for the first k with k*k > x
while lo < hi:
mid = (lo + hi) // 2
if mid * mid > x:
hi = mid
else:
lo = mid + 1
return lo - 1
def find_peak(nums):
"""An index i with nums[i] > both neighbours (outside: -inf). Neighbours differ. O(log n)."""
lo, hi = 0, len(nums) - 1
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] > nums[mid + 1]: # going down after mid: a peak at mid or on its left
hi = mid
else: # going up after mid: a peak on its right
lo = mid + 1
return lo
def top_k_frequent(nums, k):
"""The k most frequent values: a min-heap of size k over (count, value). O(n log k)."""
heap = []
for value, count in Counter(nums).items():
if len(heap) < k:
heapq.heappush(heap, (count, value))
elif count > heap[0][0]: # beats the gatekeeper
heapq.heapreplace(heap, (count, value))
return [v for _, v in heap]
def min_removals(intervals):
"""Fewest removals so that the rest do not overlap (touching allowed): sort by END."""
kept, last_end = 0, -math.inf
for s, e in sorted(intervals, key=lambda iv: iv[1]):
if s >= last_end:
kept, last_end = kept + 1, e
return len(intervals) - kept
# Brute-force references
def brute_spans(prices):
out = []
for i, p in enumerate(prices):
j = i
while j >= 0 and prices[j] <= p:
j -= 1
out.append(i - j)
return out
def brute_removals(intervals):
n = len(intervals)
for size in range(n, -1, -1):
for sub in combinations(sorted(intervals), size):
if all(a[1] <= b[0] for a, b in zip(sub, sub[1:])):
return n - size
rng = random.Random(0)
for _ in range(2000):
prices = [rng.randint(1, 6) for _ in range(rng.randint(0, 12))]
assert stock_spans(prices) == brute_spans(prices)
x = rng.randint(0, 10**30)
assert isqrt(x) == math.isqrt(x)
nums = rng.sample(range(50), rng.randint(1, 12)) # distinct, so neighbours differ
i = find_peak(nums)
padded = [-math.inf] + nums + [-math.inf]
assert padded[i] < padded[i + 1] > padded[i + 2]
ivs = [[s, s + rng.randint(1, 6)] for s in (rng.randint(0, 12) for _ in range(rng.randint(0, 7)))]
assert min_removals(ivs) == brute_removals(ivs)
print(stock_spans([100, 80, 60, 70, 60, 75, 85])) # [1, 1, 1, 2, 1, 4, 6]
print(isqrt(30), isqrt(10**30 + 1)) # 5 1000000000000000
print(find_peak([1, 3, 2, 4, 1])) # 3 (index 1 is also a peak)
print(sorted(top_k_frequent([1, 1, 1, 2, 2, 3, 4, 4, 4, 4], 2))) # [1, 4]
print(min_removals([[1, 3], [2, 4], [3, 5], [0, 7], [5, 8], [6, 9], [8, 10]])) # 3
print("2000 random tests passed")
Two details. isqrt uses exact integers: int(math.sqrt(x)) goes through a float and is wrong for
large x (for it returns ). And find_peak returns index 3, not
the first peak at index 1: its predicate is not monotone, so binary search finds a switch point.
6. Where it shows up #
- Autodiff and parsers. Reverse-mode autodiff records the forward operations on a tape and runs the backward pass in reverse order: last in, first out. Expression parsers and tokenizers that handle nested brackets use a stack.
- Signals. A running maximum over a window (a 1-D max pool, the envelope of a seismogram) costs with a monotonic deque, for any window length. The largest all-ones rectangle in a binary mask (for example, a cloud-free block of a satellite image) runs the histogram algorithm on every row.
- Binary search in ML code.
np.searchsortedandtorch.searchsortedput values into sorted bins. Sampling from a categorical distribution is a cumulative sum plus a search for the first cumulative probability , with uniform in . On the answer: the largest batch size that fits in GPU memory, andgit bisectto find the commit that broke a training run. - Top- and merging. Beam search keeps the best partial sequences at each step. CPU vector
search libraries keep a size- heap of the best distances per query. Top- sampling in LLM
decoding is the same selection problem, done on the GPU with
torch.topk. Merging sorted shards uses a heap with one entry per shard. - Intervals in science and systems. Merging overlapping time windows of sensor records, finding gaps in a data archive, merging genome annotations, and counting the GPUs in use over time from job start and end events are all "sort, then sweep".
- Greedy in ML. Huffman coding is greedy with an exchange-argument proof (Information theory, lesson 05). BPE tokenization greedily merges the most frequent pair; it is a heuristic, not an optimal compressor. Greedy decoding is an unsafe greedy: the most likely next token does not always lead to the most likely sequence.
7. Common mistakes #
- Popping an empty stack. At a closing bracket, check that the stack is not empty; at the end, check that it is empty.
- Values instead of indices on a monotonic stack. You usually need a distance or a width, so store indices.
- Strict or not strict. Is 73 "warmer" than 73? The pop condition (
<or<=) decides it; say it aloud. - Two range styles in one loop.
while lo <= hiwithhi = n - 1(closed) andwhile lo < hiwithhi = n(half-open) need different updates. Keep one style. - An update that does not shrink the range:
lo = midwithmid = (lo + hi) // 2loops forever when . - A predicate that is not monotone, or a search range that does not contain the answer.
- Floats in integer problems:
math.ceil(p / k)andint(math.sqrt(x))fail for large integers; use(p + k - 1) // kand integer comparisons. - Wrong heap direction. A max-heap of all items for "the largest" needs memory. Use a min-heap of size .
- Reading the heap as sorted. Only
heap[0]has a fixed meaning;heap[-1]is not the maximum. - Comparing payloads on ties (
TypeError), or forgetting the sign when you negate keys. - Intervals:
last_end = einstead ofmax(last_end, e)when merging; mixing the closed and half-open conventions; sorting by the wrong key. - Greedy without proof. Passing the examples is not a proof. Test against a brute force, then give the exchange argument.
8. Check yourself #
- Next greater element with the stack, on a strictly increasing list of values: how many pushes
and pops, and how large does the stack get? And on a strictly decreasing list?
Answer
Increasing: pushes, pops (each value pops the previous one), at most 1 index on the stack. Decreasing: pushes, 0 pops, the stack grows to . Both are . a = [1, 2, 2, 2, 5]. Givebisect_left(a, 2),bisect_right(a, 2), the number of 2s, and the same for the value 3.Answer
1 and 4, so three 2s. For 3: both are 4 (the insertion position), so zero 3s.- Packages of weights must be shipped in order within 5 days; each day takes the
next packages in the line, with total weight at most the capacity . Which range do you search
for the smallest , why is the predicate monotone, and what is the answer?
Answer
: below 10 the heaviest package never fits, and (the total) always works in 1 day. If works, works with the same loading, so " is enough" is monotone. Binary search, with greedy loading as the test, gives 15 (days: 1–5, 6–7, 8, 9, 10). - Where is the maximum of a min-heap, and what does it cost to find it?
Answer
In a leaf, one of the indices , because an internal node is never larger than its children. Finding it costs : a scan over about leaves. - Numbers arrive one by one. After each number you must report the -th largest so far. What do
you store, and what does one update cost?
Answer
A min-heap of the largest numbers so far; the answer isheap[0]. One update is a comparison with the root and at most oneheapreplace: . - Why is "sort by start" right for merging, but "sort by end" right for scheduling?
Answer
Merging needs "once the next start is past the block's end, the block is final", which holds when later intervals start later. Scheduling needs "the first choice leaves the most room for the rest", which is the earliest end; the exchange argument proves it.
What does a monotonic stack hold in "next greater element", and in what order?
The indices that still wait for a greater element. Their values do not increase from the bottom to the top.
Why is the monotonic stack despite a
whileinside afor?Each index is pushed once and popped at most once, so the whole run has at most pops and comparisons.
State the invariant of the half-open binary search for the first True.
is False on and True on . The loop runs while and returns .
What does binary search on the answer need?
A monotone test " is enough" (if works, works) and a range that surely contains the answer.
In a 0-based array heap, where are the children and the parent of node ?
Children and ; parent .
Why does
heapifycost and not ?Sift-down from a node of height costs , and only about nodes have height ; .
How do you keep the largest items of a stream, and at what cost?
A min-heap of size ; replace the root when a new item is larger. time, memory.
To merge overlapping intervals, sort by what, and why is one pass enough?
By start. Once the next start is past the current end, no later interval (later start) can overlap the current block.
Give the exchange argument for interval scheduling in one sentence.
The interval with the earliest end ends no later than the first interval of any optimal set, so swapping for keeps the set valid and the same size.
Give a counterexample to "shortest meeting first" for maximum non-overlapping meetings.
: shortest first takes only ; the optimum takes and .
9. Interview angle #
Recognize the pattern from the wording.
| The problem says | Try |
|---|---|
| matching pairs, nesting, "undo", evaluate an expression | stack |
| next or previous greater/smaller; "how many days until"; span; histogram widths | monotonic stack |
| maximum or minimum of every window of size | monotonic deque |
| sorted input and ; first or last position; insert position | binary search on indices |
| "smallest capacity / speed / time such that …" | binary search on the answer |
| rotated sorted list; peak; "first bad version" | binary search with a custom predicate |
| largest / smallest / closest / most frequent; merge sorted sources | heap |
| overlapping ranges, bookings, time windows | sort, then sweep |
| most non-conflicting; fewest X to cover everything; "can you reach the end" | greedy, then prove it or break it |
What to say before you code. Monotonic stack: what it holds, why it is ordered, and the push-once, pop-once bound. Binary search: the predicate, the range , the invariant, and why the range shrinks. Heap: the direction and the size. Greedy: the rule and the exchange argument in three sentences.
Typical questions and short model answers.
- "Why is your Daily Temperatures solution ?" — "Each day is pushed onto the stack exactly once and popped at most once. The inner loop pops, so over the whole run it runs at most times in total. Every comparison either pops a day or ends the inner loop, so there are at most comparisons. The extra space is the stack, in the worst case, for falling temperatures."
- "Find the minimum eating speed so that all piles are eaten in hours." — "If speed is enough, any faster speed is also enough, so the predicate is monotone. I search in with the half-open template; the test sums with integer arithmetic, . That is ."
- "The 10 largest of a huge stream?" — "A min-heap of size 10. The root is the smallest of the current top 10, so each new item is compared with the root and replaces it if larger. time and memory. Sorting would need all the data in memory and . If all data fits in memory and I need only the -th value, quickselect is on average."
- "Why is
heapifylinear?" — "It sifts down from the bottom. Half of the nodes are leaves and do not move, a quarter move at most one level, and so on. The sum is , so the total is . Pushing items one by one is in the worst case." - "Maximum number of non-overlapping meetings — why is your greedy correct?" — "I sort by end and take every meeting that starts after the last one I took. Exchange argument: take any optimal schedule and its first-ending meeting. My first choice ends no later, so I can swap it in, and the schedule stays valid with the same size. Then the same argument applies to the rest. Earliest start fails on one long meeting that covers two short ones."
Typical follow-ups. "Values can repeat?" (monotonic stack: strict or not strict; rotated search: the worst case becomes ). "A stream?" (the monotonic stack and the size- heap work online). " is expensive?" (binary search calls it only times). "Intervals arrive sorted?" (one pass, as in Insert Interval). "Median of a stream?" (two heaps).
10. Practice #
The notebook for this lesson runs in Colab. Try each exercise from memory first. Each one is tested against a brute force or the standard library on random inputs:
daily_temperatures: a monotonic stack.lower_bound: the half-open binary search, compared withbisect.bisect_left.min_eating_speed: binary search on the answer (Koko Eating Bananas).kth_largestwith a size- heap, andmerge_intervals.max_non_overlapping: the earliest-end greedy, checked against all subsets of small inputs.
Timed practice (25 minutes each, talking aloud): Valid Parentheses, Evaluate Reverse Polish Notation, Daily Temperatures, Search in Rotated Sorted Array, Koko Eating Bananas, Kth Largest Element in an Array, Merge Intervals, Non-overlapping Intervals, Meeting Rooms II, Gas Station; harder: Largest Rectangle in Histogram, Find Median from Data Stream.
Say it aloud drill. Without code, explain in 90 seconds why lo = mid + 1 is safe and
lo = mid is not, then explain the exchange argument for interval scheduling. Record yourself and
check that you said the invariant both times.
11. Watch and read #
- Jeff Erickson, Algorithms (free PDF), chapter 4, Greedy Algorithms: §4.2 (earliest finish first, with the exchange argument) and §4.3 (the general pattern). The whole book is on the author's page.
- Kleinberg and Tardos, Algorithm Design, chapter 4, in the lecture slides by Kevin Wayne: interval scheduling, interval partitioning (meeting rooms) and "greedy stays ahead".
- Sedgewick and Wayne, Algorithms, 4th edition, §2.4 Priority Queues: swim (sift up), sink (sift down) and heapsort.
- cp-algorithms, Binary search: lower and upper bound, search on an arbitrary predicate, and continuous search.
- Python documentation:
bisect(the exact meaning ofbisect_leftandbisect_right) andheapq(heapreplaceversusheappushpop, and the priority-queue notes). - Tech Interview Handbook cheatsheets for stacks, heaps and intervals; public problem lists Grind 75 and NeetCode 150 (sections Stack, Binary Search, Heap / Priority Queue, Intervals, Greedy).
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.