title: Best Time to Buy and Sell Stock
slug: best-time-to-buy-and-sell-stock
difficulty: easy
leetcode_id: 121
leetcode_url: https://leetcode.com/problems/best-time-to-buy-and-sell-stock/
categories:
  - arrays
  - dynamic-programming
patterns:
  - greedy

function_signature: "def max_profit(prices: list[int]) -> int:"

test_cases:
  visible:
    - input: { prices: [7, 1, 5, 3, 6, 4] }
      expected: 5
    - input: { prices: [7, 6, 4, 3, 1] }
      expected: 0
  hidden:
    - input: { prices: [1, 2, 3, 4, 5] }
      expected: 4
    - input: { prices: [2, 4, 1] }
      expected: 2
    - input: { prices: [3, 3, 3, 3] }
      expected: 0
    - input: { prices: [1] }
      expected: 0

description: |
  You are given an array `prices` where `prices[i]` is the price of a given stock on the i<sup>th</sup> day.

  You want to maximize your profit by choosing a **single day** to buy one stock and choosing a **different day in the future** to sell that stock.

  Return *the maximum profit you can achieve from this transaction*. If you cannot achieve any profit, return `0`.

constraints: |
  - `1 <= prices.length <= 10^5`
  - `0 <= prices[i] <= 10^4`

examples:
  - input: "prices = [7,1,5,3,6,4]"
    output: "5"
    explanation: "Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6 - 1 = 5. Note that buying on day 2 and selling on day 1 is not allowed because you must buy before you sell."
  - input: "prices = [7,6,4,3,1]"
    output: "0"
    explanation: "In this case, no transactions are done and the max profit = 0."

explanation:
  intuition: |
    Imagine visualising the stock prices as a line graph over time.

    Your goal is to find the **largest vertical distance** between a "valley" (lowest price) and a subsequent "peak" (highest price). The key constraint is the direction of time: you must buy *before* you sell. Therefore, you cannot simply pick the global minimum and maximum from the entire array — the minimum must appear at an earlier index than the selling price.

    Think of it like this: as you walk through the days one by one, you want to keep track of the **lowest price you have seen so far**. For every new day, you ask yourself: "If I had bought at that lowest price and sold today, how much profit would I make?"

    By continuously tracking this running minimum and comparing potential profits, you can find the optimal answer in a single pass.

  approach: |
    We solve this using a **Single Pass (Greedy) Approach**:

    **Step 1: Initialise two variables**

    - `min_price`: Set to infinity initially, so the first price we encounter becomes the minimum
    - `max_profit`: Set to `0`, since we return `0` if no profit is possible

    &nbsp;

    **Step 2: Iterate through the prices array**

    - For each price, check if it's lower than our current `min_price`
    - If yes, update `min_price` — this ensures we're always considering buying at the lowest point found so far
    - If no, calculate the potential profit: `current_price - min_price`
    - Compare this with `max_profit` and keep the larger value

    &nbsp;

    **Step 3: Return the result**

    - Return `max_profit` after checking all days

    &nbsp;

    This greedy approach works because at each step we make the locally optimal choice (tracking the minimum and maximum profit so far), which leads to the globally optimal solution.

  common_pitfalls:
    - title: The Brute Force Trap
      description: |
        A common first instinct is to use nested loops to compare every pair of days:
        - Outer loop `i` from `0` to `n-1` (buy day)
        - Inner loop `j` from `i+1` to `n-1` (sell day)

        This results in **O(n²) time complexity**. With the constraint `prices.length <= 10^5`, an O(n²) solution means up to 10 billion operations — this will cause a **Time Limit Exceeded (TLE)** error.
      wrong_approach: "Nested loops comparing all pairs"
      correct_approach: "Single pass tracking running minimum"

    - title: Buying After Selling
      description: |
        Don't find the global minimum and global maximum independently without considering their order.

        For example, with `prices = [5, 4, 3, 2, 1]`, the minimum is `1` (last day) and maximum is `5` (first day). But you can't sell on day 1 and buy on day 5 — time only moves forward!

        By tracking `min_price` as we iterate left-to-right, we automatically ensure we only consider selling *after* buying.
      wrong_approach: "min(prices) and max(prices) separately"
      correct_approach: "Track running minimum while iterating forward"

    - title: Forgetting the No-Profit Case
      description: |
        When prices strictly decrease (e.g., `[7, 6, 4, 3, 1]`), no profitable transaction exists. Your algorithm should return `0`, not a negative number.

        Initialising `max_profit = 0` handles this automatically — we only update it when we find a positive profit.

  key_takeaways:
    - "**Greedy pattern**: Make locally optimal choices (update min/max) at each step to find the global optimum"
    - "**One-pass efficiency**: When looking for maximum differences in sequential data, try maintaining running variables rather than re-scanning"
    - "**Foundation for harder problems**: This logic extends to variants like multiple transactions, cooldown periods, or transaction fees"
    - "**Time-space tradeoff**: We sacrifice no space (O(1)) and gain optimal time (O(n)) by being clever about what we track"

  time_complexity: "O(n). We traverse the list of prices exactly once."
  space_complexity: "O(1). We only use two variables (`min_price` and `max_profit`), regardless of the input size."

solutions:
  - approach_name: Single Pass (Greedy)
    is_optimal: true
    code: |
      def max_profit(prices: list[int]) -> int:
          # Start with infinity so first price becomes minimum
          min_price = float('inf')
          # Start with 0 since that's our answer if no profit possible
          max_profit = 0

          for price in prices:
              # Found a new lowest price? Update our buy point
              if price < min_price:
                  min_price = price

              # Calculate profit if we sold today at this price
              else:
                  current_profit = price - min_price
                  # Keep track of the best profit we've seen
                  if current_profit > max_profit:
                      max_profit = current_profit

          return max_profit
    explanation: |
      **Time Complexity:** O(n) — Single pass through the array.

      **Space Complexity:** O(1) — Only two variables used.

      We iterate once, updating our minimum buy price and maximum achievable profit as we go. The greedy choice at each step (buy at lowest seen, sell at current if profitable) guarantees the optimal solution.

  - approach_name: Brute Force
    is_optimal: false
    code: |
      def max_profit(prices: list[int]) -> int:
          max_profit = 0
          n = len(prices)

          # Try every possible buy day
          for i in range(n):
              # Try every possible sell day after buy day
              for j in range(i + 1, n):
                  profit = prices[j] - prices[i]
                  max_profit = max(max_profit, profit)

          return max_profit
    explanation: |
      **Time Complexity:** O(n²) — Nested loops comparing all pairs.

      **Space Complexity:** O(1) — Only tracking max_profit.

      This approach checks every valid (buy, sell) pair. While correct, it's too slow for large inputs and will result in TLE on LeetCode. Included here to illustrate why the greedy approach is necessary.
