Algorithm

Buy & Sell Stock

Dynamic Programming Pattern

Best Time to Buy and Sell Stock

You are given an array prices where prices[i] is the price of a stock on day i. You may complete at most one transaction: buy on one day and sell on a strictly later day. Return the maximum profit you can make. If no transaction can produce a profit, return 0 — you are allowed to do nothing.

CONSTRAINTS
  • 1 ≤ prices.length ≤ 10⁵
  • 0 ≤ prices[i] ≤ 10⁴
  • At most one buy and one sell, and the sell must come after the buy
  • Doing nothing is permitted, so the answer is never negative
EXAMPLE 1
Input: prices = [7, 1, 5, 3, 6, 4]
Output: 5
Buying on day 1 at price 1 and selling on day 4 at price 6 yields 5. The overall high of 6 and low of 1 happen to be in the correct order here.
EXAMPLE 2
Input: prices = [7, 6, 4, 3, 1]
Output: 0
Prices only fall, so every legal trade would lose money. Declining to trade is allowed, so the answer is 0 rather than a negative number.
EXAMPLE 3
Input: prices = [2, 9, 1]
Output: 7
The best trade is buy at 2 and sell at 9. The array's lowest price is 1, but it occurs after the 9, and a sale must come strictly after its purchase.
EXAMPLE 4
Input: prices = [5]
Output: 0
A single day gives no later day on which to sell, so no transaction is possible.
EXAMPLE 5
Input: prices = [3, 3, 3]
Output: 0
Every price is identical, so every trade breaks even and the best available profit is zero.
Can I buy and sell on the same day?
No, the sell must be on a strictly later day. It would make no difference to the answer here since it yields zero profit, but in variants where a transaction is mandatory or carries a fee it matters, so it is worth stating.
How many transactions am I allowed?
Exactly one buy and one matching sell, at most. This is the crucial constraint — with unlimited transactions the answer becomes the sum of every upward move, which is a much simpler greedy problem, and with at most k transactions it becomes a larger table.
What if every price is falling — should I return a negative number?
No, return 0. Doing nothing is a legal choice, so the profit can never be negative. Any solution that reports the least-bad trade has misread the contract.
Do I need to report which days to trade on?
Only the profit. Recovering the days means remembering the index at which the running minimum was set, and the index at which the best profit was achieved.
How large can the input be?
Up to 100000 days, which rules out the quadratic pair-checking approach outright. When a constraint is that size, the bound itself is telling you a single-pass solution is expected.
What is being asked, precisely

Pick a day to buy and a strictly later day to sell, and maximise sell price − buy price. You may also decline to trade at all, which is why the floor is 0 rather than a negative number. On a strictly falling market like [7, 6, 4, 3, 1], every possible trade loses money, so the answer is 0.

Note carefully: this is not "biggest number minus smallest number". On [2, 9, 1] the smallest is 1 and the largest is 9, but the 9 comes before the 1, so that pair is not a legal trade. The best legal trade is buy at 2 and sell at 9, for 7. Order is part of the constraint, and every wrong solution to this problem ignores it in some way.

The brute force, and the waste inside it

Try every pair of days with the buy first:

python
def max_profit(prices):
    best = 0
    for i in range(len(prices)):
        for j in range(i + 1, len(prices)):
            best = max(best, prices[j] - prices[i])
    return best

Count it: day 0 is compared against n-1 later days, day 1 against n-2, and so on, giving about N²/2 comparisons. With prices up to 100000 days, that is five billion — far too slow.

It is slow because it treats every pair as its own puzzle. But the real decision is per day: on each day I either own the share or I do not, and that alone decides what I can do next. Walk the days once, tracking that, and the pairs take care of themselves.

The state, as a story

Walk the days one at a time. Standing on day i, I am in one of two situations:

- I am holding the one share I'm allowed to buy, or
- I am not holding — I have not bought yet.

So besides the day, the thing that decides my options is: am I holding the share?

State Definitionf(i, holding) is the best profit I can still make from day i onward, given whether I already hold the share.

What can I do on day i?

- If I am holding, I can sell today and pocket prices[i] — my one trade is then finished, so nothing more is added — or keep holding: f(i, holding) = max( prices[i], f(i+1, holding) ).
- If I am not holding, I can buy today — and since I only get one trade, buying costs me prices[i] from an empty pocket — or wait: f(i, not holding) = max( -prices[i] + f(i+1, holding), f(i+1, not holding) ).

The single-trade rule lives in one place: selling ends the trade (it never loops back to buy again), and buying starts from zero.

Base case: past the last day (i == n) there is nothing left to earn, so f = 0. The answer is f(0, not holding).

From recursion to memo

Write the story down exactly. holding is a boolean:

python
def max_profit(prices):
    n = len(prices)

    def f(i, holding):
        if i == n: return 0
        if holding:
            return max(prices[i], f(i + 1, True))        # sell (trade done), or keep
        else:
            return max(-prices[i] + f(i + 1, True),      # buy today
                       f(i + 1, False))                  # wait

    return f(0, False)

Correct, but exponential — each day branches in two. Yet there are only n × 2 different (i, holding) questions, so memoise: solve each once, reuse after.

python
def max_profit(prices):
    n = len(prices)
    memo = {}

    def f(i, holding):
        if i == n: return 0
        if (i, holding) in memo: return memo[(i, holding)]
        if holding:
            best = max(prices[i], f(i + 1, True))
        else:
            best = max(-prices[i] + f(i + 1, True), f(i + 1, False))
        memo[(i, holding)] = best
        return best

    return f(0, False)

O(N) time and space.

Bottom up, then space optimized

Turn it into a table. Let dp[i][h] hold what f(i, h) returned. Each recurrence line becomes an array assignment. Every read is from day i+1, so fill from the last day backwards, starting with day n all zeros. The answer is dp[0][0]:

python
def max_profit(prices):
    n = len(prices)
    dp = [[0, 0] for _ in range(n + 1)]      # dp[i][0] = not holding, dp[i][1] = holding

    for i in range(n - 1, -1, -1):
        dp[i][1] = max(prices[i], dp[i + 1][1])                    # holding: sell or keep
        dp[i][0] = max(-prices[i] + dp[i + 1][1], dp[i + 1][0])    # empty: buy or wait

    return dp[0][0]

Each day reads only day i+1, so two variables replace the whole table:

python
def max_profit(prices):
    next_hold, next_free = 0, 0             # dp[i+1][1], dp[i+1][0]
    for p in reversed(prices):
        hold = max(p, next_hold)                    # sell today, or keep
        free = max(-p + next_hold, next_free)       # buy today, or wait
        next_hold, next_free = hold, free
    return next_free

O(N) time, O(1) space.

Worked Example:prices = [7, 1, 5, 3, 6, 4]
Sweep the days backwards, carrying (hold, free) = best profit from here if holding a share / if not:
- Start past the end: (0, 0).
- price 4: hold = max(4, 0) = 4 (sell for 4). free = max(−4+0, 0) = 0. → (4, 0)
- price 6: hold = max(6, 4) = 6. free = max(−6+4, 0) = 0. → (6, 0)
- price 3: hold = max(3, 6) = 6. free = max(−3+6, 0) = 3 (buy at 3, sell later at 6). → (6, 3)
- price 5: hold = max(5, 6) = 6. free = max(−5+6, 3) = 3. → (6, 3)
- price 1: hold = max(1, 6) = 6. free = max(−1+6, 3) = 5 (buy at 1, sell at 6). → (6, 5)
- price 7: hold = max(7, 6) = 7. free = max(−7+6, 5) = 5. → (7, 5)

Answer free = 5, buying at 1 and selling at 6.

And the falling market [7, 6, 4, 3, 1]: every buy costs more than any later sell, free never rises above 0, and the answer is 0 — we decline to trade.

The reflex

The move that solved this is the one to keep: when the day alone is not enough, ask what decides your options and add exactly that to the state. Here it was a single yes/no — am I holding the share? — and everything else followed the usual ladder: write the recursion straight from the two cases, memoise it, turn it into a table, then trim to two variables.

That two-state view is what carries into the harder variants. The rest of this family — cooldowns, fees, at most k transactions — keeps the same skeleton and only grows the state. Learn this version properly and the rest is bookkeeping.

Interactive Strategy Visualization

Stock Strategy Evolution

State Tabulation
D0$7
D1$1
D2$5
D3$3
EMPTY (Cash)
0
-
-
-
HOLDING (Stock)
-
-
-
-
Stock Prices: [7, 1, 5, 3]. Goal: Maximize profit through multi-day trading.
Exponential Recursion
O(N) Time · O(N) Space Memoisation
O(N) Time · O(N) Space Tabulation
O(N) Time · O(1) Space Two Variables