Best Time to Buy and Sell Stock
Introduction
A daily stock-price series. One buy, one sell, in that order. Find the largest profit you can lock in. The brute force is obvious — try every pair — and gets to a correct answer in . The optimal answer is short enough to fit in seven lines, and the move from one to the other is exactly the kind of insight an interviewer is timing you on.
The deeper reason this problem keeps appearing is that it’s the gateway to a whole DP family. Once you’ve seen the running-state trick that makes a single buy/sell linear, the same trick generalizes to two transactions, k transactions, transactions with cooldown, transactions with a fee — half a dozen LeetCode hard problems collapse to variations on the recurrence you write here.
The interview move is asking what part of the past actually matters for the future. Once you can answer that in one number, the algorithm is linear and the chapter is over.
If you’re early in DP prep, walk both approaches in order. If you’ve coded the running-min answer before, jump to the follow-ups table — the family of harder buy/sell variants is the senior content.
What the interviewer is actually testing
- Spotting the redundancy in brute force. A nested loop computes
prices[j] - prices[i]for every pair; the sameprices[i]value appears in many of those subtractions. Naming the redundancy is the optimization prompt. - The running-state idea. At day , the only fact about the past you need is the cheapest price seen so far. Everything else can be discarded. That’s a constant-state DP.
- Initialization discipline. A common bug initializes the running min to
prices[0]and starts the loop at . Cleaner: initializeminSoFar = INT_MAX, loop from , and the first iteration handles itself. - Knowing the family exists. Buy/Sell II (unlimited transactions), III (at most two), IV (at most ), with cooldown, with fee. Naming them out loud signals you’ve been deeper into DP than the median candidate.
Problem statement
You are given an array prices where prices[i] is the price of a given stock on day . Choose a single day to buy and a later day to sell. Return the maximum profit you can achieve. If no profitable trade exists, return 0.
You must buy before you sell.
prices = [7, 1, 5, 3, 6, 4] 5 prices = [7, 6, 4, 3, 1] 0 prices = [3, 2, 6, 5, 0, 3] 4 Clarifying questions — and why each one matters
Clarification questions
- Q If no profitable trade exists, what's the expected return value?
- A The most important clarification. Standard answer: 0 — meaning 'don't trade.' Some variants require returning a negative number, treating the absence of a profit as the smallest non-trade loss. Confirm before assuming.
- Q Can you buy and sell on the same day?
- A Yes per the standard formulation, and the profit is 0. This affects the initialization of the running maximum: starting at 0 already covers the same-day case.
- Q Is only one transaction allowed, or can we do multiple?
- A This chapter solves the single-transaction problem (LC 121). The unlimited-transaction variant (LC 122) and the two-transaction variant (LC 123) are different problems with the same underlying DP shape — see the follow-ups.
- Q Can prices be negative or zero?
- A Standard formulation says non-negative integers. Negatives don't affect the algorithm but do raise the question of what 'profit' means in a market where prices can go below zero (oil futures, briefly, did this in 2020).
- Q What's the upper bound on n?
- A Up to 10⁵ in typical problems. O(n²) brute force is too slow at that size; the linear sweep is comfortable. If n is in the millions, mention that the linear pass is cache-friendly and parallelizable.
- Q Are we returning the profit, or the (buy, sell) day pair?
- A Default is just the profit. If asked for the pair, track two extra integers (the day where the running min was set, and the day where the best profit was achieved). No asymptotic change.
A mental framework — what does the past actually tell you?
Walk through the brute-force logic and ask: what about the past does day care about?
To evaluate “what’s the best profit if I sell on day ?”, you need to know the cheapest day to have bought before. Just one number. Not the whole price history; not the second-cheapest; not the average. The single fact min(prices[0..j-1]) is the entire useful summary of the past.
That observation collapses the problem. Walk the prices left to right, maintaining a running minimum. At each day, the candidate profit is prices[j] - minSoFar. Track the largest candidate seen and you have the answer in one pass.
This is the template for constant-state DP: the recurrence depends on a fixed-size summary of the past. Maximum Subarray follows the same shape (running endingHere). Climbing Stairs follows it (the previous two values). The buy/sell family follows it (a small set of running profits, indexed by transaction state).
Discussion
Brute force — every (buy, sell) pair
Two nested loops compare every valid buy day against every later sell day. Quadratic time, constant space, your starting line.
The interview narration
Say this out loud:
“A trade is determined by a buy day and a later sell day. There are such pairs. The brute force enumerates every pair and tracks the maximum spread. time, space. I’ll write it as a baseline and find something faster.”
How it works, step by step
The outer loop ranges over the buy day from to . The inner loop ranges over the sell day from to . For each pair, compute prices[j] - prices[i] and update the running maximum.
Two small details:
- Inner loop starts at
i + 1, noti. Buying and selling on the same day yields 0 profit, which is already the initial value ofbest. bestinitialized to 0, notINT_MIN. The “no-trade” option is always available, so any negative spread is automatically dominated.
The code
#include <algorithm>#include <vector>
int maxProfit(const std::vector<int>& prices) { const int n = static_cast<int>(prices.size()); int best = 0; for (int i = 0; i < n; ++i) { for (int j = i + 1; j < n; ++j) { best = std::max(best, prices[j] - prices[i]); } } return best;}Two nested loops, one comparison per pair. Initialization to 0 captures the no-trade case implicitly.
Why both operations are
The total number of pairs is . Each comparison is constant time. There’s no inner work beyond the subtraction and the max — it’s the number of pairs that’s quadratic, not the work per pair.
For this is operations — about a minute of wall time. Production code would never ship this, and the interviewer expects you to recognize that without prompting.
What it gets right
- Correctness is unambiguous. Every valid pair is examined, so the maximum can’t be missed.
- It exposes the redundancy. The same
prices[i]participates in subtractions. That redundancy is what the linear approach eliminates.
Running minimum — one pass Recommended
Walk left to right with two running values: the cheapest price seen so far, and the best profit so far. One subtraction per day. Linear time, constant space.
The insight, stated cleanly
The brute force recomputes — for every prefix — the same fact: what’s the cheapest price seen up to here? That fact is incremental: extending the prefix by one day either updates the running minimum or leaves it unchanged. Maintaining it costs per day.
Once you have the running minimum, the candidate profit at day is just prices[j] - minSoFar. Track the maximum candidate as you walk, and you’ve answered the entire problem in one pass.
minSoFar — the lowest price seen up to today. The best profit at each step is today's price minus that floor. Watch the floor drop, and watch the best buy/sell pair update when today's gap above it sets a new record.Start: minSoFar = +∞, best = 0. Walk left to right.
How it works, step by step
Maintain two running values:
minSoFar— the lowest price seen in any prefix processed so far. Initialize toINT_MAXso the first day’s price is automatically the new minimum.best— the largest profit seen so far. Initialize to 0; the no-trade option is always valid.
For each price p in left-to-right order:
- Update the floor:
minSoFar = min(minSoFar, p). Ifpis the new minimum, the running floor drops; otherwise it stays. - Try selling today:
best = max(best, p - minSoFar). Ifpis higher than the current floor, the gap is a candidate profit; update if it beats the current best.
After the loop, best is the answer. The order of the two updates matters: doing the min first ensures that the candidate p - minSoFar correctly evaluates to 0 on the day p itself was the new minimum (which protects the loop from a self-trade).
The code
#include <algorithm>#include <climits>#include <vector>
int maxProfit(const std::vector<int>& prices) { int minSoFar = INT_MAX; int best = 0; for (const int p : prices) { minSoFar = std::min(minSoFar, p); best = std::max(best, p - minSoFar); } return best;}Five lines of body. Two integers of state. The whole DP table fits in two registers.
Why this is the production answer
- Linear and cache-friendly. A single sequential pass over the input. The branch predictor and the memory prefetcher both love this code.
- Constant space. Two ints. Switch to
long longonly if prices can exceed 2³¹. - Easy to extend. Want to also report the buy/sell days? Track
minIndex(the day whereminSoFarwas set) and updatebestBuy/bestSellwheneverbestchanges. No asymptotic change.
Edge cases the mechanics handle for free
- Empty array (): the loop never runs;
beststays at 0. Correct under the standard “return 0 for no trade” convention. - Single day (): one iteration; the price becomes
minSoFar;beststays at 0. Correct (you can’t sell on the same day after buying with this convention). - Strictly decreasing prices: every day is a new minimum; the candidate
p - minSoFaris always 0;beststays at 0. Correct — no profitable trade exists. - All same price: every day’s candidate is 0;
beststays 0. Correct. - Single positive spike:
minSoFarlocks in early at the global minimum;bestupdates on the spike day.
Correctness, precisely
Invariant. After processing index :
minSoFarequals .bestequals the maximum ofprices[k] - prices[j]over all .
Proof sketch. Induction on .
Base: . After one iteration, minSoFar = prices[0] and best = prices[0] - prices[0] = 0. The maximum of prices[k] - prices[j] over the singleton range is 0. ✓
Inductive step: Assume the invariant holds at . At index :
minSoFarbecomes . ✓bestbecomes .
The new candidate prices[i] - new minSoFar equals the maximum profit over pairs ending at (because the new minSoFar is the cheapest valid buy day for any sell at ). So the new best is the maximum over pairs ending anywhere in . ✓
After the loop, best is the maximum profit over all valid pairs in the array.
Complexity
- Time: . One pass, two constant-time updates per iteration.
- Space: . Two integers.
Approach comparison
| Approach | Time | Space | When to reach for it |
|---|---|---|---|
| Brute force | Baseline in narration; never in production. | ||
| Running min (one pass) | Default. Linear time, constant space. |
Default answer: the running-min sweep. Name the invariant — “after each day, minSoFar is the cheapest price seen and best is the maximum profit over pairs ending at any prefix” — and the code writes itself.
Follow-ups to anticipate
The buy/sell family is one of the richest follow-up trees in interview DP. Each variant changes the state, but the running-state pattern survives.
| The interviewer asks… | What to do |
|---|---|
| ”Return the actual buy/sell days, not just the profit.” | Track minIndex (day of minSoFar) and bestBuy/bestSell indices. Update them whenever best changes. Same . |
| ”Multiple transactions allowed.” | LC 122. Sum every positive day-to-day delta: . Greedy, . |
| ”At most two transactions.” | LC 123. Two passes — one tracking max profit ending by day , one tracking max profit starting at day . Sum and take the max over splits. , . |
| ”At most transactions.” | LC 188. 2D DP: = best profit using transactions through day . The same running-state idea, just copies of it. . |
| ”With a 1-day cooldown after each sale.” | LC 309. Three running states: holding stock, just sold (cooldown), idle. Transition between them. , . |
| ”With a transaction fee per sell.” | LC 714. Two running states (holding / idle); subtract the fee on every transition from holding to idle. . |
| ”Streaming prices — answer must update after each new day.” | The single-transaction sweep is already streaming-friendly: each new price triggers two constant-time updates. The best value at any point is the answer for the prefix seen so far. |
| ”Find the longest stretch of consecutive profitable trades.” | Different question — combine the running-min idea with a “current run length” counter, similar to the Maximum Subarray template. |
Pattern recognition — constant-state DP, again
This problem teaches the same move as Maximum Subarray and Climbing Stairs: a recurrence that depends on a fixed-size summary of the past becomes a constant-space sweep. The fixed-size summary here is just one integer (minSoFar); for harder variants it grows to two or three; for the -transaction problem it grows to .
Other problems that fit the same shape:
- Maximum Subarray (Kadane’s). Running
endingHereandbest. One number from the past. - Climbing Stairs. The two previous values. Constant state, two integers.
- House Robber. and . Two integers, same shape.
- Largest Rectangle in Histogram with cumulative widths. Constant per-cell state with a monotonic stack on the side.
- Reservoir sampling for top-1. Track the single running candidate; the rest of the stream is summarized by it.
The reflex: when a problem walks an array left-to-right and asks for the “best” something over all prefixes, ask what’s the smallest summary of the prefix I’d need to answer the question? If that summary is constant-size, you have an / algorithm.
What to say out loud — the delivery script
Ten beats:
- “Let me restate: given a price series, find the maximum profit on a single buy-then-sell pair, or 0 if no profitable trade exists.”
- Ask: “single transaction? no-trade returns 0? profit or pair?”
- “Brute force enumerates every with and tracks the max spread — . Let me name it and look for something faster.”
- “For each candidate sell day, the only fact about the past I need is the cheapest price seen so far. That’s a single integer of state.”
- “So I walk left to right with two running values:
minSoFarandbest. On each day, update the floor, then try selling today against it.” - Write the code — five lines. “The order matters: update
minSoFarfirst, then compute the candidate profit. That way the day where the new minimum was set evaluates to 0, never negative.” - Trace example 1 on the whiteboard. Show that the running min drops to 1 on day 1 and the best profit hits 5 on day 4.
- “Complexity: time, space. The pass is sequential and cache-friendly.”
- “Edge cases: empty array, strictly decreasing prices, all same — all return 0 by the natural mechanics. No special-casing needed.”
- “For follow-ups: this is the gateway to the buy/sell DP family — multiple transactions, cooldown, fees. Each variant adds running states but keeps the constant-space sweep shape.”
Summary
The single-transaction buy/sell problem is short to state, short to solve, and reveals a pattern that generalizes across half a dozen harder variants. The interview signal isn’t the seven-line code; it’s articulating the move from “compare every pair” to “remember one number from the past.” That move is the constant-state DP recipe, and recognizing it is what makes the rest of the buy/sell family — multiple transactions, cooldowns, fees, -bounded — feel like rotations of a single template rather than five independent problems.
The wider lesson: when an algorithm walks a sequence and asks for the best answer over all prefixes, ask first what part of the past is actually load-bearing for the future. Often it’s a single running number. When it is, the algorithm is linear and the chapter is over.