Part IVDSA for InterviewsSliding window · 17 of 8925 min

17 · Best Time to Buy and Sell Stock

Given a share's price on each day, find the most you could make by buying once and selling once later.

Easy · G — the warm-up for this block

You get an array of prices, one per day, in order. You may buy one share on one day and sell it on a later day. Return the largest profit that plan can make. If prices only ever fall, the best plan is not to trade at all, and the answer is 0 — never a negative number.

PricesAnswer
[8, 3, 6, 2, 7, 4]5 — buy at 2 on day 3, sell at 7 on day 4
[4, 10, 1, 5]6 — buy at 4, sell at 10; the cheapest day, 1, comes too late to beat it
[9, 7, 4, 1]0 — prices only fall, so don't trade

Constraints that matter: between 1 and 100,000 days, each price between 0 and 10,000. Checking every buy day against every later sell day is O(n²), about five billion pairs at the top end; the target is a single pass, O(n), with O(1) memory.

Starter code and tests

In an Xcode test target:

import Testing

struct BestTimeToBuyAndSellStockTests {

    @Test(arguments: [
        ([8, 3, 6, 2, 7, 4], 5),
        ([4, 10, 1, 5], 6),
        ([9, 7, 4, 1], 0),
    ])
    func returnsTheBiggestGainFromOneBuyThenOneLaterSell(prices: [Int], expected: Int) {
        #expect(maxProfit(prices) == expected)
    }

    // MARK: - Privates
    private func maxProfit(_ prices: [Int]) -> Int {
        0
    }
}

In a playground:

func maxProfit(_ prices: [Int]) -> Int {
    0 // your solution
}

let cases: [([Int], Int)] = [
    ([8, 3, 6, 2, 7, 4], 5),
    ([4, 10, 1, 5], 6),
    ([9, 7, 4, 1], 0),
]
for (prices, expected) in cases {
    let got = maxProfit(prices)
    print(got == expected ? "PASS" : "FAIL", prices, "→", got, "expected", expected)
}
Pattern and cue

Sliding window with a running minimum. The cue is "buy on one day, sell on a later day": a pair of positions where the left one must come first, scanned once from left to right. The left edge of the window sits on the cheapest day seen so far; the right edge is today. The left edge only ever jumps forward, to a new low.

Approach

Walk through the days once and remember two numbers: the cheapest price seen so far, and the best profit seen so far. On each day, first ask "is today the cheapest day yet?" and update the cheapest price if so. Then ask "if I sold today, having bought on the cheapest day before it, would that beat my best?" and update the best profit if so. At the end, the best profit is the answer.

That covers every useful plan: for any sell day, the best buy day is simply the cheapest day before it, and the running minimum is exactly that.

Time O(n): one look at each day. Space O(1): two numbers.

Swift solution
func maxProfit(_ prices: [Int]) -> Int {
    var cheapest = Int.max
    var best = 0

    for price in prices {
        cheapest = min(cheapest, price)          // best day to have bought, up to today
        best = max(best, price - cheapest)       // sell today against that day
    }
    return best
}

Updating cheapest before computing the profit means a same-day buy and sell is considered, which is worth 0 and so never wins — harmless, and it removes a special case. best starts at 0, so falling prices and an empty array both return 0.

Verified with swift test on Swift 6.4 in Swift 6 mode: the three examples above, five more (empty, one day, flat prices, a single 0-to-10,000 jump, and a zigzag), and 3,000 random arrays checked against a brute-force search over every buy and sell pair.

Walk it through

[4, 10, 1, 5]

DayPriceCheapest so farProfit selling todayBest
04400
110466
21106
35146

Answer 6. This is the example that sinks "find the lowest price, then the highest price after it": the lowest price is 1, and the best you can do from there is 4.

[9, 7, 4, 1] — every day is a new low, so every "profit selling today" is 0 and best never moves off 0.

The Swift trap

A one-liner that is secretly O(n²). Swift makes it tempting to write prices.indices.map { prices[$0] - (prices[...$0].min() ?? 0) }.max(). It reads like one pass and it compiles cleanly, but prices[...$0] is an ArraySlice and .min() walks the whole slice every time, so the work grows with the square of the length — the brute force with nicer spelling. Slicing is O(1); anything you call on the slice is not. The other half of the trap is prices.min() returning an Optional: starting from prices[0] or prices.first! crashes on an empty array, which is why the solution starts cheapest at Int.max. price - Int.max is negative, not an overflow, so it is safe here.

What they ask next
  • "Return the two days, not the profit." → Keep the index of the cheapest day alongside its price, and record that index plus today's index whenever best improves.
  • "You can trade as many times as you like, one share at a time." → Add up every rise from one day to the next: max(0, prices[i] − prices[i − 1]) summed. Greedy, O(n).
  • "At most two trades." → Four running numbers — best after first buy, first sell, second buy, second sell — updated in that order each day. Still O(n), O(1); it's the step towards dynamic programming.

Sources for this chapter