901. Online Stock Span
Problem Description
You need to design a class that tracks daily stock prices and calculates the "span" for each day's price.
The span of a stock price on a given day is defined as the maximum number of consecutive days (including the current day and going backward) where the stock price was less than or equal to the current day's price.
Let's understand with examples:
-
If the last 4 days had prices
[7, 2, 1, 2]
and today's price is2
, the span is4
. This is because looking backward from today:2 <= 2
(today),1 <= 2
(1 day ago),2 <= 2
(2 days ago),2 <= 2
(3 days ago). All 4 consecutive days have prices ≤ 2. -
If the last 4 days had prices
[7, 34, 1, 2]
and today's price is8
, the span is3
. Looking backward:8 <= 8
(today),2 <= 8
(1 day ago),1 <= 8
(2 days ago), but34 > 8
(3 days ago). So only 3 consecutive days have prices ≤ 8.
You need to implement the StockSpanner
class with:
StockSpanner()
: Initializes the objectnext(int price)
: Takes today's price and returns the span for that price
The key insight is that this problem can be solved efficiently using a monotonic stack. The stack maintains pairs of (price, span)
in decreasing order of prices. When a new price comes in, we pop all elements with prices less than or equal to the current price, accumulating their spans. This accumulated span plus 1 (for the current day) gives us the final span for the current price.
Intuition
When we need to find the span for a given day's price, we're essentially asking: "How many consecutive previous days (including today) had prices less than or equal to today's price?"
The naive approach would be to store all prices and, for each new price, iterate backward counting days until we find a higher price. This would take O(n)
time for each query in the worst case.
But here's the key observation: if we have a day with price p1
and span s1
, and a later day comes with price p2
where p2 >= p1
, then all the days covered by p1
's span are automatically included in p2
's span. We don't need to check those days individually again - we can directly add s1
to p2
's span count.
This leads us to think about maintaining only the "significant" prices - those that could potentially stop a future price's span. These are prices that form a decreasing sequence when viewed from the most recent day backward.
Consider this sequence: [7, 2, 1, 3]
. When price 3
arrives:
- We compare with
1
: since3 >= 1
, we include its span - We compare with
2
: since3 >= 2
, we include its span too - We compare with
7
: since3 < 7
, we stop
After processing 3
, we don't need to keep 1
and 2
anymore because any future price that's greater than 3
will automatically be greater than 1
and 2
. The price 3
"blocks" them from being relevant to future calculations.
This pattern suggests using a stack where we maintain prices in decreasing order (from bottom to top). Each element stores both the price and its calculated span. When a new price arrives, we pop all smaller or equal prices, accumulating their spans, because all days they represent are now part of the current day's span.
Learn more about Stack, Data Stream and Monotonic Stack patterns.
Solution Approach
We implement the solution using a monotonic decreasing stack that stores pairs of (price, span)
.
Data Structure
self.stk
: A stack that maintains price-span pairs in decreasing order of prices (from bottom to top)
Algorithm for next(price)
-
Initialize span counter: Start with
cnt = 1
to count the current day itself. -
Process the stack: While the stack is not empty AND the price at the top of the stack is less than or equal to the current
price
:- Pop the top element
(old_price, old_span)
from the stack - Add
old_span
to our currentcnt
:cnt += self.stk.pop()[1]
- This step consolidates all consecutive days with prices ≤ current price
- Pop the top element
-
Push current price: After processing, push
(price, cnt)
onto the stack. This pair represents:- The current price
- The total span calculated (including all popped elements' spans)
-
Return the span: Return
cnt
as the span for the current day
Why This Works
The stack maintains a decreasing sequence of prices. Each element's span represents how many consecutive days (before encountering a higher price) that price dominated.
When a new price arrives:
- If it's higher than some stack elements, those elements' spans get absorbed into the new price's span
- The new price then "blocks" those removed prices from affecting future calculations
- If it's lower than the top of the stack, it simply gets added with span 1
Example Walkthrough
Let's trace through prices [100, 80, 60, 70, 60, 75, 85]
:
next(100)
: Stack empty, push(100, 1)
, return1
next(80)
:80 < 100
, push(80, 1)
, return1
next(60)
:60 < 80
, push(60, 1)
, return1
next(70)
:70 >= 60
, pop(60, 1)
,cnt = 1 + 1 = 2
, push(70, 2)
, return2
next(60)
:60 < 70
, push(60, 1)
, return1
next(75)
:75 >= 60
, pop(60, 1)
,75 >= 70
, pop(70, 2)
,cnt = 1 + 1 + 2 = 4
, push(75, 4)
, return4
next(85)
:85 >= 75
, pop(75, 4)
,85 >= 80
, pop(80, 1)
,cnt = 1 + 4 + 1 = 6
, push(85, 6)
, return6
Time and Space Complexity
- Time:
O(1)
amortized per operation. Each price is pushed and popped at most once. - Space:
O(n)
in the worst case when prices are strictly decreasing.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a simple example with prices [100, 80, 60, 70, 75]
to see how the monotonic stack solution works.
Initial State: Stack is empty []
Day 1: price = 100
- Stack is empty, so no elements to pop
cnt = 1
(just counting today)- Push
(100, 1)
onto stack - Stack:
[(100, 1)]
- Return:
1
Day 2: price = 80
- Top of stack is
(100, 1)
where100 > 80
, so we don't pop anything cnt = 1
(just counting today)- Push
(80, 1)
onto stack - Stack:
[(100, 1), (80, 1)]
- Return:
1
Day 3: price = 60
- Top of stack is
(80, 1)
where80 > 60
, so we don't pop anything cnt = 1
(just counting today)- Push
(60, 1)
onto stack - Stack:
[(100, 1), (80, 1), (60, 1)]
- Return:
1
Day 4: price = 70
- Top of stack is
(60, 1)
where60 <= 70
, so we pop it - Add its span to our count:
cnt = 1 + 1 = 2
- Next top is
(80, 1)
where80 > 70
, so we stop popping - Push
(70, 2)
onto stack (price 70 with span 2) - Stack:
[(100, 1), (80, 1), (70, 2)]
- Return:
2
(meaning prices on day 3 and day 4 are both ≤ 70)
Day 5: price = 75
- Top of stack is
(70, 2)
where70 <= 75
, so we pop it - Add its span to our count:
cnt = 1 + 2 = 3
- Next top is
(80, 1)
where80 > 75
, so we stop popping - Push
(75, 3)
onto stack - Stack:
[(100, 1), (80, 1), (75, 3)]
- Return:
3
(meaning prices on days 3, 4, and 5 are all ≤ 75)
Notice how when we popped (70, 2)
in the last step, we inherited its span of 2, which already accounted for days 3 and 4. This is the key efficiency: we don't need to look back at those individual days again because the span in our stack already captured that information.
Solution Implementation
1class StockSpanner:
2 def __init__(self):
3 # Stack to maintain pairs of (price, span)
4 # The stack maintains a monotonic decreasing order of prices
5 self.stack = []
6
7 def next(self, price: int) -> int:
8 # Calculate the span for the current price
9 # Span is the number of consecutive days (including today)
10 # where the price was less than or equal to today's price
11
12 # Start with span of 1 (today itself)
13 span = 1
14
15 # Pop all elements from stack that have price <= current price
16 # Add their spans to current span (they are all part of current span)
17 while self.stack and self.stack[-1][0] <= price:
18 previous_price, previous_span = self.stack.pop()
19 span += previous_span
20
21 # Push current price and its calculated span to stack
22 self.stack.append((price, span))
23
24 return span
25
26
27# Your StockSpanner object will be instantiated and called as such:
28# obj = StockSpanner()
29# param_1 = obj.next(price)
30
1class StockSpanner {
2 // Stack to store pairs of [price, span count]
3 // Each element represents a price and the number of consecutive days
4 // (including itself) for which it was the maximum
5 private Deque<int[]> stack = new ArrayDeque<>();
6
7 /**
8 * Constructor to initialize the StockSpanner
9 */
10 public StockSpanner() {
11 }
12
13 /**
14 * Calculates the span of the stock's price for the current day
15 * The span is defined as the maximum number of consecutive days
16 * (starting from today and going backwards) for which the price
17 * of the stock was less than or equal to today's price
18 *
19 * @param price The stock price for the current day
20 * @return The span of the stock's price for the current day
21 */
22 public int next(int price) {
23 // Initialize count to 1 (current day always counts in its own span)
24 int count = 1;
25
26 // Pop all elements from stack that have price less than or equal to current price
27 // and accumulate their spans
28 while (!stack.isEmpty() && stack.peek()[0] <= price) {
29 // Add the span of the popped element to current count
30 count += stack.pop()[1];
31 }
32
33 // Push current price and its accumulated span to the stack
34 stack.push(new int[] {price, count});
35
36 // Return the calculated span for the current price
37 return count;
38 }
39}
40
41/**
42 * Your StockSpanner object will be instantiated and called as such:
43 * StockSpanner obj = new StockSpanner();
44 * int param_1 = obj.next(price);
45 */
46
1class StockSpanner {
2public:
3 // Constructor initializes an empty stock spanner
4 StockSpanner() {
5 }
6
7 // Calculates and returns the span of the current price
8 // Span is defined as the maximum number of consecutive days (including today)
9 // where the stock price was less than or equal to today's price
10 int next(int price) {
11 // Initialize span count to 1 (current day)
12 int span = 1;
13
14 // Pop all elements from stack that have price less than or equal to current price
15 // and accumulate their spans
16 while (!priceStack.empty() && priceStack.top().first <= price) {
17 span += priceStack.top().second;
18 priceStack.pop();
19 }
20
21 // Push current price and its calculated span onto the stack
22 priceStack.emplace(price, span);
23
24 return span;
25 }
26
27private:
28 // Stack stores pairs of (price, span)
29 // Maintains a monotonic decreasing stack of prices
30 std::stack<std::pair<int, int>> priceStack;
31};
32
33/**
34 * Your StockSpanner object will be instantiated and called as such:
35 * StockSpanner* obj = new StockSpanner();
36 * int param_1 = obj->next(price);
37 */
38
1// Stack to store [price, span] pairs
2// Each element represents a price and the number of consecutive days
3// where the stock price was less than or equal to that price
4let priceStack: [number, number][] = [];
5
6/**
7 * Calculates the stock span for the given price.
8 * The stock span is defined as the maximum number of consecutive days
9 * (starting from today and going backwards) for which the stock price
10 * was less than or equal to today's price.
11 *
12 * @param price - The current stock price
13 * @returns The span of the stock price
14 */
15function next(price: number): number {
16 // Initialize span count to 1 (current day)
17 let spanCount = 1;
18
19 // Pop all elements from stack where price is less than or equal to current price
20 // Accumulate their spans to get the total span for current price
21 while (priceStack.length > 0 && priceStack[priceStack.length - 1][0] <= price) {
22 const [_, previousSpan] = priceStack.pop()!;
23 spanCount += previousSpan;
24 }
25
26 // Push current price and its calculated span to the stack
27 priceStack.push([price, spanCount]);
28
29 return spanCount;
30}
31
32/**
33 * Resets the stock spanner to initial state
34 */
35function resetStockSpanner(): void {
36 priceStack = [];
37}
38
Time and Space Complexity
Time Complexity: O(n)
amortized, where n
is the total number of times next(price)
is called.
While a single call to next(price)
might iterate through multiple elements in the stack (appearing to be O(n)
worst case for that call), each element is pushed onto the stack exactly once and popped at most once throughout all operations. Therefore, across n
calls to next(price)
, the total number of push and pop operations is at most 2n
, giving us an amortized time complexity of O(1)
per operation, and O(n)
total for n
operations.
Space Complexity: O(n)
, where n
is the number of times next(price)
is called.
In the worst case, when prices are in strictly increasing order, no elements are popped from the stack, and each call to next(price)
adds one element to the stack. Therefore, the stack can grow to contain up to n
elements, resulting in O(n)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect Comparison Operator
Problem: Using strict inequality (<
) instead of less than or equal to (<=
) when comparing prices.
# WRONG - This will miss equal prices while self.stack and self.stack[-1][0] < price: # Should be <= previous_price, previous_span = self.stack.pop() span += previous_span
Why it's wrong: The span definition includes days where the price was equal to the current price, not just less than. If you use <
, you'll incorrectly exclude days with the same price from the span calculation.
Solution: Always use <=
for the comparison:
# CORRECT while self.stack and self.stack[-1][0] <= price: previous_price, previous_span = self.stack.pop() span += previous_span
Pitfall 2: Forgetting to Accumulate Spans
Problem: Only counting the number of popped elements instead of accumulating their spans.
# WRONG - Only counts individual days, not accumulated spans
def next(self, price: int) -> int:
span = 1
while self.stack and self.stack[-1][0] <= price:
self.stack.pop()
span += 1 # Wrong! Should add the popped element's span
self.stack.append((price, span))
return span
Why it's wrong: Each element in the stack already has an accumulated span representing multiple days. By only adding 1 for each popped element, you're losing the information about the days that element represented.
Solution: Extract and add the span value from each popped element:
# CORRECT while self.stack and self.stack[-1][0] <= price: previous_price, previous_span = self.stack.pop() span += previous_span # Add the entire span, not just 1
Pitfall 3: Not Storing the Span with Each Price
Problem: Storing only prices in the stack without their corresponding spans.
# WRONG - Stack only stores prices
class StockSpanner:
def __init__(self):
self.stack = [] # Just storing prices
def next(self, price: int) -> int:
span = 1
while self.stack and self.stack[-1] <= price:
self.stack.pop()
span += 1 # Can't know the actual span of popped element
self.stack.append(price) # Only pushing price
return span
Why it's wrong: Without storing the span, you can't know how many days each popped price represented. This leads to incorrect span calculations when prices are popped.
Solution: Store tuples of (price, span)
in the stack:
# CORRECT self.stack.append((price, span)) # Store both price and its span
The three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Median of Data Stream Given a stream of numbers find the median number at any given time accurate to 1 decimal place Example add_number 1 add_number 2 add_number 3 get_median 2 0 add_number 4 get_median 2 5 Try it yourself Explanation Intuition Brute force way is to sort the entire list every time we get a new number This would be O N log N for each add_number operation One step up is to notice that the list is sorted before we add any new number to it So every
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!