title: Online Stock Span
slug: online-stock-span
difficulty: medium
leetcode_id: 901
leetcode_url: https://leetcode.com/problems/online-stock-span/
categories:
  - stack
patterns:
  - slug: monotonic-stack
    is_optimal: true

function_signature: "class StockSpanner"

test_cases:
  visible:
    - input:
        operations: ["StockSpanner", "next", "next", "next", "next", "next", "next", "next"]
        args: [[], [100], [80], [60], [70], [60], [75], [85]]
      expected: [null, 1, 1, 1, 2, 1, 4, 6]
  hidden:
    - input:
        operations: ["StockSpanner", "next", "next", "next"]
        args: [[], [100], [100], [100]]
      expected: [null, 1, 2, 3]
    - input:
        operations: ["StockSpanner", "next", "next", "next", "next"]
        args: [[], [31], [41], [48], [59]]
      expected: [null, 1, 2, 3, 4]
    - input:
        operations: ["StockSpanner", "next", "next", "next", "next"]
        args: [[], [100], [90], [80], [70]]
      expected: [null, 1, 1, 1, 1]
    - input:
        operations: ["StockSpanner", "next", "next", "next", "next", "next"]
        args: [[], [28], [14], [28], [35], [46]]
      expected: [null, 1, 1, 3, 4, 5]

description: |
  Design an algorithm that collects daily price quotes for some stock and returns **the span** of that stock's price for the current day.

  The **span** of the stock's price in one day is the maximum number of consecutive days (starting from that day and going backward) for which the stock price was less than or equal to the price of that day.

  - For example, if the prices of the stock in the last four days is `[7,2,1,2]` and the price of the stock today is `2`, then the span of today is `4` because starting from today, the price of the stock was less than or equal `2` for `4` consecutive days.
  - Also, if the prices of the stock in the last four days is `[7,34,1,2]` and the price of the stock today is `8`, then the span of today is `3` because starting from today, the price of the stock was less than or equal `8` for `3` consecutive days.

  Implement the `StockSpanner` class:

  - `StockSpanner()` Initialises the object of the class.
  - `int next(int price)` Returns the **span** of the stock's price given that today's price is `price`.

constraints: |
  - `1 <= price <= 10^5`
  - At most `10^4` calls will be made to `next`

examples:
  - input: '["StockSpanner", "next", "next", "next", "next", "next", "next", "next"], [[], [100], [80], [60], [70], [60], [75], [85]]'
    output: "[null, 1, 1, 1, 2, 1, 4, 6]"
    explanation: "stockSpanner.next(100) returns 1, next(80) returns 1, next(60) returns 1, next(70) returns 2 (spans days with prices 60 and 70), next(60) returns 1, next(75) returns 4 (spans 60, 70, 60, 75), next(85) returns 6 (spans 60, 70, 60, 75, 80, 85... wait, let's trace: prices seen are [100, 80, 60, 70, 60, 75, 85]. For 85, going back: 75<=85 (span 4), 60<=85 (span 1), 70<=85 (span 2), 60<=85 (span 1), 80<=85 (span 1). Total = 1+4+1 = 6)."

explanation:
  intuition: |
    Imagine you're a stock analyst looking at a chart of daily prices. For each new price, you need to count how many consecutive days (looking backward) had prices less than or equal to today's price.

    The naive approach would be to look back day by day each time a new price arrives. But this is inefficient — if prices keep increasing, you'd repeatedly scan through the same history.

    Here's the key insight: **once a smaller price is "absorbed" into a larger price's span, you never need to look at it again**. If today's price is 85 and yesterday's was 75, then any future price ≥ 85 will automatically include yesterday's span too.

    Think of it like a game where smaller prices get "swallowed" by larger ones. We only need to remember prices that could potentially stop a future price's span — these are prices in **decreasing order**. This is exactly what a *monotonic decreasing stack* tracks.

    The stack stores pairs of `(price, span)`. When a new price arrives, we pop all smaller-or-equal prices and accumulate their spans. The remaining stack top (if any) is the first price larger than today's — that's where our span stops.

  approach: |
    We solve this using a **Monotonic Decreasing Stack**:

    **Step 1: Initialise the data structure**

    - `stack`: A list storing tuples of `(price, span)` in decreasing price order
    - The stack will always maintain prices in strictly decreasing order from bottom to top

    &nbsp;

    **Step 2: Process each new price in the `next` method**

    - Start with `span = 1` (the current day counts)
    - While the stack is non-empty AND the top price is ≤ current price:
      - Pop the top element
      - Add its span to our current span (we "absorb" those days)
    - Push `(price, span)` onto the stack
    - Return the calculated span

    &nbsp;

    **Step 3: Understanding why this works**

    - When we pop smaller prices, we're effectively saying "these days are now part of the current day's span"
    - We store the accumulated span with each price, so we don't need to revisit individual days
    - The stack only keeps prices that are strictly greater than all prices that came after them
    - Each price is pushed once and popped at most once, giving amortised O(1) per operation

  common_pitfalls:
    - title: Scanning Backward Each Time
      description: |
        The brute force approach stores all prices in a list and scans backward from the current index to count consecutive days with price ≤ current price.

        With up to `10^4` calls to `next`, and each call potentially scanning O(n) previous prices, this gives O(n^2) total time — far too slow.

        Example: if prices are strictly increasing `[1, 2, 3, ..., 10000]`, each call scans all previous prices, resulting in 1 + 2 + 3 + ... + 10000 ≈ 50 million operations.
      wrong_approach: "Store all prices and scan backward each time"
      correct_approach: "Use monotonic stack to track only relevant prices"

    - title: Forgetting to Include the Current Day
      description: |
        The span always includes the current day itself. Initialise `span = 1`, not `span = 0`.

        If today's price is lower than all previous prices, the span should be `1` (just today), not `0`.
      wrong_approach: "Initialise span = 0"
      correct_approach: "Initialise span = 1 to include current day"

    - title: Using Wrong Comparison Operator
      description: |
        The span includes days where price was **less than or equal** to today's price. Use `<=` when comparing.

        If you use `<` instead, you'll miss days with equal prices and return incorrect spans.

        Example: prices `[100, 80, 80]`. The span for the second `80` should be `2` (both 80s), not `1`.
      wrong_approach: "Pop while stack top < current price"
      correct_approach: "Pop while stack top <= current price"

  key_takeaways:
    - "**Monotonic stack pattern**: When you need to find the nearest larger/smaller element or count elements until a condition breaks, a monotonic stack often provides O(n) amortised time"
    - "**Absorbing spans**: By storing accumulated spans with each stack entry, we avoid re-counting — each day is counted exactly once across all operations"
    - "**Amortised analysis**: Each price is pushed once and popped at most once, so n operations cost O(n) total, giving O(1) amortised per call"
    - "**Design pattern**: This problem combines data structure design with the monotonic stack pattern — a common interview combination"

  time_complexity: "O(1) amortised per call to `next`. Each price is pushed and popped at most once across all operations, so n calls cost O(n) total."
  space_complexity: "O(n) where n is the number of calls to `next`. In the worst case (strictly decreasing prices), all prices remain on the stack."

solutions:
  - approach_name: Monotonic Decreasing Stack
    is_optimal: true
    code: |
      class StockSpanner:
          def __init__(self):
              # Stack stores (price, span) tuples in decreasing price order
              self.stack = []

          def next(self, price: int) -> int:
              # Current day always counts as 1
              span = 1

              # Pop all prices <= current price and absorb their spans
              while self.stack and self.stack[-1][0] <= price:
                  _, prev_span = self.stack.pop()
                  span += prev_span

              # Push current price with its accumulated span
              self.stack.append((price, span))

              return span
    explanation: |
      **Time Complexity:** O(1) amortised — Each price is pushed once and popped at most once.

      **Space Complexity:** O(n) — Stack can hold up to n prices in worst case.

      The monotonic stack maintains prices in strictly decreasing order. When a new price arrives, we pop all smaller-or-equal prices and add their spans to the current span. This "absorbs" multiple days in one operation, avoiding repeated backward scans.

  - approach_name: Brute Force (Store All Prices)
    is_optimal: false
    code: |
      class StockSpanner:
          def __init__(self):
              # Store all prices seen so far
              self.prices = []

          def next(self, price: int) -> int:
              self.prices.append(price)
              span = 1

              # Scan backward counting consecutive days <= current price
              i = len(self.prices) - 2
              while i >= 0 and self.prices[i] <= price:
                  span += 1
                  i -= 1

              return span
    explanation: |
      **Time Complexity:** O(n) per call in worst case — Must scan all previous prices.

      **Space Complexity:** O(n) — Stores all prices.

      This approach stores every price and scans backward each time. While correct, it's O(n) per call, giving O(n^2) total for n calls. With `10^4` calls, this approach will be too slow. Included to illustrate why the monotonic stack optimisation is necessary.
