Skip to content

122. Best Time to Buy and Sell Stock II (Medium)

Problem

You are given an integer array prices where prices[i] is the price of a stock on day i. Each day you may buy and/or sell, but you can hold at most one share at any time. You may complete as many transactions as you like. Return the maximum profit.

Unlike 121. Best Time to Buy and Sell Stock, which allows only a single buy-sell pair, this version allows unlimited transactions. That one change flips the problem from “find the single best window” to “collect every gain you can.”

Examples

  • [7,1,5,3,6,4]7: buy at 1 sell at 5 (+4), buy at 3 sell at 6 (+3).
  • [1,2,3,4,5]4: buy at 1, sell at 5. One climb, no dips.
  • [7,6,4,3,1]0: prices only fall, so never buy.

Constraints

  • 1prices.length3×1041 \leq \text{prices.length} \leq 3 \times 10^4
  • 0prices[i]1040 \leq \text{prices}[i] \leq 10^4

LeetCode 122 · Link · Medium

Try it yourself

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Peak and valley

The intuitive picture: profit comes from buying at a valley (local minimum) and selling at the next peak (local maximum). Walk the array, slide down to each valley, slide up to the following peak, and bank peak − valley. Repeat to the end.

def maxProfit(prices: list[int]) -> int:
n = len(prices)
i = 0
profit = 0
while i < n - 1: # L1: scan the array
while i < n - 1 and prices[i + 1] <= prices[i]: # L2: descend to a valley
i += 1
valley = prices[i]
while i < n - 1 and prices[i + 1] >= prices[i]: # L3: climb to a peak
i += 1
profit += prices[i] - valley # L4: bank the rise
return profit

Where the time goes, line by line

Variables: n = len(prices).

LinePer-call costTimes executedContribution
L2 descendO(1)O(1)nn total across all valleysO(n)O(n)
L3 climbO(1)O(1)nn total across all peaksO(n)O(n)
L4 bankO(1)O(1)peaks visitedO(n)O(n)

Complexity

  • Time: O(n)O(n), each index advanced once.
  • Space: O(1)O(1).

Approach 2: Greedy, sum every climb

Peak-and-valley is correct but fiddly. Here is the collapse: a rise from valley to peak equals the sum of every up-step in between. So you do not need to find peaks and valleys at all. Just add up every positive day-to-day difference.

prices = [1, 5, 3, 6]
valley-to-peak view: (5-1) + (6-3) = 4 + 3 = 7
day-to-day view: (5-1) + (6-3) = 4 + 3 = 7 ← skip the 5→3 drop

If the price rises today, pretend you bought yesterday and sell today. Down-days contribute nothing because you simply hold no share.

def maxProfit(prices: list[int]) -> int:
profit = 0
for i in range(1, len(prices)): # L1: scan adjacent pairs
if prices[i] > prices[i - 1]: # L2: only count up-days
profit += prices[i] - prices[i - 1] # L3: bank the daily gain
return profit

Why summing up-steps is optimal

A telescoping argument. Any profitable holding period from day b to day s has value prices[s] − prices[b], which equals the sum of the consecutive differences (prices[b+1] − prices[b]) + ... + (prices[s] − prices[s-1]). The negative differences inside that span only shrink the total, so dropping them (by not holding on down-days) can never lose money, and capturing every positive difference can never be beaten. Because you may transact freely, you are allowed to take exactly the positive steps and skip the rest.

Where the time goes, line by line

Variables: n = len(prices).

LinePer-call costTimes executedContribution
L1 loopO(1)O(1)n1n - 1O(n)O(n)
L3 bank gainO(1)O(1)up to n1n - 1O(n)O(n)

Complexity

  • Time: O(n)O(n), one pass.
  • Space: O(1)O(1).

Approach 3: State machine (hold vs cash)

The most general framing, the one that scales to the harder variants (cooldown, fees, capped transaction count). Track two running bests:

  • cash: most profit you can have holding no share right now.
  • hold: most profit you can have holding one share right now (a negative number early on, since buying costs money).

Each day, either act or do nothing, and keep the better of the two.

def maxProfit(prices: list[int]) -> int:
cash = 0 # L1: no share held
hold = float('-inf') # L2: one share held
for p in prices: # L3: each day
cash = max(cash, hold + p) # L4: sell or stay flat
hold = max(hold, cash - p) # L5: buy or keep holding
return cash

This is identical in result to Approach 2 but expresses the decision explicitly, which is why it generalizes. Returning cash (not hold) is correct because you never want to finish the day still holding a share, an unsold share is unrealized profit worth nothing at the close.

Complexity

  • Time: O(n)O(n), one pass.
  • Space: O(1)O(1), two scalars.

How to recognize this pattern

The signal: “as many transactions as you like” plus “at most one share.” That pairing is the tell. Unlimited transactions with no cooldown and no fee means there is nothing stopping you from capturing every single up-day. The moment a constraint removes friction like that, ask whether the answer is just “sum all the gains.”

The contrast that defines it. Compare the three stock problems:

ProblemConstraintOptimal idea
121. Buy and Sell Stockone transactionTrack min price so far, max single window
122. Buy and Sell Stock IIunlimited transactionsSum every positive day-to-day difference
309. With Cooldownunlimited, but 1-day rest after sellingState machine, the friction breaks the greedy sum

The single-transaction version (121) is a sliding-window/running-minimum problem. Add unlimited transactions and it collapses to the trivial greedy sum (122). Add a cooldown or a fee and the greedy sum breaks, friction forces you back to the state machine (Approach 3). Recognizing which friction is present tells you which tool to reach for.

The wrong first move: overthinking it as DP. Many people see “maximize profit over a sequence of decisions” and immediately reach for a 2D DP table over (day, holding-state). That works, but for this specific problem it is a sledgehammer. The greedy one-liner is correct and provably optimal because there is no friction. Save the DP for when a constraint actually couples your decisions across days.

The mental model. With no transaction limit, every up-day is independent free money. You are not choosing when to be in the market, you are choosing to ride every uphill and sit out every downhill. There is no opportunity cost to selling and re-buying because both can happen the same day.

ProblemSame shape
53. Maximum SubarrayAccumulate local gains, drop what hurts the running total
1710. Maximum Units on a TruckGreedily take the best available unit each step
605. Can Place FlowersTake every locally-valid opportunity, no lookahead needed

What a transaction fee breaks (and why)

The greedy sum works because, with no friction, splitting one climb into daily steps is free. Buy-and-sell every up-day, or buy once and hold through the whole climb, both capture the same total. Add a per-sale fee and that equivalence collapses, because every split now costs another fee.

Watch it break on a tiny case. Prices [1, 2, 3] with a $1 fee per sale:

day-to-day steps: +1, +1
decompose into daily trades (the greedy view):
buy 1 / sell 2 → +1 gross − $1 fee = 0
buy 2 / sell 3 → +1 gross − $1 fee = 0
total: $0 ← two trades, two fees, both wiped out
merge into one hold:
buy 1 / sell 3 → +2 gross − $1 fee = $1
total: $1 ← one trade, one fee

Same $2 gross climb, but the daily-step view pays the toll twice and nets nothing, while one long hold pays it once and nets a dollar. Summing positive daily steps and subtracting a fee per step gives the wrong answer here: it returns 0 when the real maximum is 1.

The lesson cuts both ways:

  • No friction → every up-day is independent → sum the positive steps. Splitting is free, so it never hurts.
  • Any friction (a fee, a cooldown) → days are coupled → you can no longer decompose into daily steps. Now you want the opposite move: merge consecutive up-days into one hold and pay the fee once. Whether to act today depends on whether you are already holding, which is exactly the state the day-to-day trick throws away.

That coupling is why the friction variants need the hold/cash state machine (Approach 3), not the greedy sum. The state machine carries “am I holding a share right now?” across days, so it can decide to ride through a dip rather than sell into it. 714. Best Time to Buy and Sell Stock with Transaction Fee subtracts the fee inside the cash = max(cash, hold + p - fee) step; 309. Best Time to Buy and Sell Stock with Cooldown adds a third “resting” state. Same skeleton, one extra rule each.

Key takeaways

  • Unlimited transactions with no friction means: sum every positive prices[i] − prices[i-1]. That is the whole solution.
  • A valley-to-peak rise equals the sum of the up-steps inside it, so peak-finding is unnecessary, the daily-difference sum captures the same profit.
  • Down-days contribute nothing because you simply hold no share through them.
  • The hold/cash state machine gives the same answer and is the version to remember, it survives when cooldowns or fees break the greedy sum.
  • A per-sale fee couples your days: splitting one climb into daily trades now pays the fee on every step, so you merge consecutive up-days into a single hold and pay once. Summing daily steps minus a per-step fee is wrong (it returns 0 on [1,2,3] with a $1 fee when the answer is 1).
  • Contrast with 121 (single transaction, running-min window): the constraint, not the surface, picks the technique.
  • Greedy Algorithms, the local choice pattern protected by an invariant about the best reachable future.
  • Array Scans, the linear pass habit of carrying just enough state while reading each item once.