Understanding the problem
A question asked once per element
A trader wants to know, for each day's closing price, how many days pass before the price is higher. The natural first answer walks forward from every day:
for i in range(n):
for j in range(i + 1, n):
if price[j] > price[i]:
wait[i] = j - i
break
On a rising market this is fast, because every inner loop stops after one step. On a falling market it is a disaster: no day ever finds a higher price, and every inner loop runs to the end of the array.
How bad is the bad case?
On a steadily falling series the inner loop runs n − 1, then
n − 2, and so on: about n²/2 comparisons in total.
| Days of prices | Comparisons on a falling series | At 10⁸ a second |
|---|---|---|
| 1,000 | ≈ 500,000 | instant |
| 100,000 | ≈ 5 billion | about a minute |
| 1,000,000 | ≈ 500 billion | well over an hour |
Worse, the work is wasted in a specific way. When day 5 scans forward past days 6, 7 and 8, it learns that each of them is lower than day 5. Then day 6 scans the same stretch again and learns almost the same thing. The brute force keeps no memory of what it has already seen.
Turn the question round
Instead of each day going to look for its answer, let the answers come to the days. Walk through the prices once and keep a list of the days that have not yet seen a higher price. When a new price arrives, it is the answer for every waiting day it beats.
Which waiting days does it beat? Always the most recent ones. A day can only still be waiting if nothing after it was higher, so the waiting prices fall from oldest to newest. The new price removes waiting days from the newest end until it meets one it does not beat, then joins the list itself. Adding and removing at the same end is a stack.
The same prices, handled with a list of waiting days. Watch how a single new price can settle several days at once.
-
Step 1 / 6price730i741752713694725766waiting730settled0
Day 0, price 73. Nothing is waiting. Day 0 starts waiting.
-
Step 2 / 6price730741i752713694725766waiting740settled1
Day 1, price 74, beats 73. Day 0 gets its answer, 1 day. Day 1 waits.
-
Step 3 / 6price730741752i713694725766waiting750settled2
Day 2, price 75, beats 74. Day 1 settled. Day 2 waits.
-
Step 4 / 6price730741752713694i725766waiting750711692settled2
Days 3 and 4 (71, then 69) beat nothing. They pile up on top of 75, in falling order.
-
Step 5 / 6price730741752713694725i766waiting750721settled4
Day 5, price 72, beats 69 and then 71, but not 75. Two days settled in one visit; 75 stays.
-
Step 6 / 6price730741752713694725766iwaiting760settled6
Day 6, price 76, beats 72 and 75. Everything that could be settled is settled, after one pass.
Why it is linear
There is a while loop inside the for loop, so it looks
quadratic. Count differently: every day is added to the stack once and removed at most once.
However the removals are spread across the iterations, their total over the whole run is at
most n. That style of argument, charging the work to the elements rather than to
the loop iterations, is called amortised analysis, and interviewers expect you to give it
whenever you write a loop like this.
The stack holds questions that are still open. Each new element closes some of them and opens one of its own. When a problem asks about the nearest larger or smaller element, or the matching partner of something seen earlier, that is the picture to reach for.