Best Time to Buy and Sell Stock
The single-pass pattern that powers streaming analytics, online algorithms, and real-time decision making in production systems.
Problem
You are given an array prices
where prices[i]
is the price of a given stock on the i
th 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. If you cannot achieve any profit, return 0
.
Example 1:
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: Buying on day 2 and selling on day 1 is not allowed (must buy before you sell).
Example 2:
Input: prices = [7,6,4,3,1]
Output: 0
Explanation: No profit can be made, so return 0.
Constraints:
1 <= prices.length <= 10^5
0 <= prices[i] <= 10^4
Intuition
The key insight: Track the minimum price seen so far and calculate potential profit at each step.
For each price, we ask:
- “If I sold today, what’s the best profit I could make?”
- This requires knowing the minimum price before today
Pattern: This is a streaming maximum problem, we process data once, left to right, maintaining running statistics.
Approach 1: Brute Force (Not Optimal)
Try all possible buy-sell pairs.
Implementation
def maxProfitBruteForce(prices: List[int]) -> int:
"""
Try every possible buy-sell pair
Time: O(n²)
Space: O(1)
"""
max_profit = 0
n = len(prices)
for buy_day in range(n):
for sell_day in range(buy_day + 1, n):
profit = prices[sell_day] - prices[buy_day]
max_profit = max(max_profit, profit)
return max_profit
Why this is bad:
- O(n²) time complexity
- For n = 100,000 → 10 billion operations
- Unacceptable for production systems processing real-time data
Approach 2: Single Pass (Optimal)
Track minimum price and maximum profit in one pass.
Implementation
from typing import List
def maxProfit(prices: List[int]) -> int:
"""
Single-pass solution tracking min price and max profit
Time: O(n) - one pass through array
Space: O(1) - only two variables
Algorithm:
1. Track minimum price seen so far
2. At each day, calculate profit if we sold today
3. Update maximum profit
"""
if not prices or len(prices) < 2:
return 0
min_price = float('inf')
max_profit = 0
for price in prices:
# Update minimum price seen so far
min_price = min(min_price, price)
# Calculate profit if we sell today
profit = price - min_price
# Update maximum profit
max_profit = max(max_profit, profit)
return max_profit
Detailed Walkthrough
prices = [7, 1, 5, 3, 6, 4]
Day 0: price = 7
min_price = min(inf, 7) = 7
profit = 7 - 7 = 0
max_profit = max(0, 0) = 0
Day 1: price = 1
min_price = min(7, 1) = 1 ← New minimum!
profit = 1 - 1 = 0
max_profit = max(0, 0) = 0
Day 2: price = 5
min_price = min(1, 5) = 1
profit = 5 - 1 = 4 ← Good profit
max_profit = max(0, 4) = 4
Day 3: price = 3
min_price = min(1, 3) = 1
profit = 3 - 1 = 2
max_profit = max(4, 2) = 4
Day 4: price = 6
min_price = min(1, 6) = 1
profit = 6 - 1 = 5 ← Best profit!
max_profit = max(4, 5) = 5
Day 5: price = 4
min_price = min(1, 4) = 1
profit = 4 - 1 = 3
max_profit = max(5, 3) = 5
Final: max_profit = 5
Why This Works
Invariant: At any day i
, we know:
- The minimum price from days
0
toi-1
- The maximum profit achievable up to day
i
Correctness:
- We consider every valid buy-sell pair implicitly
- When we see
price[i]
, we compute profit assuming we bought atmin_price
- This covers all cases because
min_price
is the best buy day beforei
Complexity Analysis
Time Complexity: O(n)
- Single pass through the array
- Constant work per element
- Linear scaling with input size
Space Complexity: O(1)
- Only two variables:
min_price
,max_profit
- No auxiliary data structures
- Memory usage independent of input size
Approach 3: Dynamic Programming Perspective
View this as a DP problem.
Formulation
State:
dp[i]
= maximum profit achievable up to dayi
Recurrence:
dp[i] = max(
dp[i-1], # Don't sell today
prices[i] - min_price[i] # Sell today
)
min_price[i] = min(min_price[i-1], prices[i])
Base case:
dp[0] = 0
(can’t make profit on first day)min_price[0] = prices[0]
Implementation
def maxProfitDP(prices: List[int]) -> int:
"""
Dynamic programming approach
Explicitly track DP state
"""
n = len(prices)
if n < 2:
return 0
# DP table
dp = [0] * n
min_prices = [0] * n
# Base case
min_prices[0] = prices[0]
dp[0] = 0
# Fill DP table
for i in range(1, n):
min_prices[i] = min(min_prices[i-1], prices[i])
dp[i] = max(dp[i-1], prices[i] - min_prices[i])
return dp[n-1]
Optimization: Notice dp[i]
only depends on dp[i-1]
, so we can reduce to O(1) space → this becomes identical to Approach 2.
Edge Cases & Testing
Edge Cases
def test_edge_cases():
# Empty array
assert maxProfit([]) == 0
# Single element
assert maxProfit([5]) == 0
# Two elements - profit possible
assert maxProfit([1, 5]) == 4
# Two elements - no profit
assert maxProfit([5, 1]) == 0
# Strictly decreasing
assert maxProfit([5, 4, 3, 2, 1]) == 0
# Strictly increasing
assert maxProfit([1, 2, 3, 4, 5]) == 4
# All same price
assert maxProfit([3, 3, 3, 3]) == 0
# Large numbers
assert maxProfit([10000, 1, 10000]) == 9999
# Minimum and maximum at ends
assert maxProfit([10, 5, 3, 1, 15]) == 14
Comprehensive Test Suite
import unittest
class TestMaxProfit(unittest.TestCase):
def test_example1(self):
"""Standard case with profit"""
self.assertEqual(maxProfit([7,1,5,3,6,4]), 5)
def test_example2(self):
"""No profit possible"""
self.assertEqual(maxProfit([7,6,4,3,1]), 0)
def test_single_element(self):
"""Only one day"""
self.assertEqual(maxProfit([1]), 0)
def test_two_elements_profit(self):
"""Minimum case with profit"""
self.assertEqual(maxProfit([1, 5]), 4)
def test_two_elements_loss(self):
"""Minimum case with loss"""
self.assertEqual(maxProfit([5, 1]), 0)
def test_increasing(self):
"""Strictly increasing prices"""
self.assertEqual(maxProfit([1, 2, 3, 4, 5]), 4)
def test_decreasing(self):
"""Strictly decreasing prices"""
self.assertEqual(maxProfit([5, 4, 3, 2, 1]), 0)
def test_v_shape(self):
"""V-shaped prices"""
self.assertEqual(maxProfit([3, 2, 1, 2, 3, 4]), 3)
def test_peak_valley(self):
"""Multiple peaks and valleys"""
self.assertEqual(maxProfit([2, 1, 2, 0, 1]), 1)
def test_large_profit(self):
"""Large profit"""
self.assertEqual(maxProfit([1, 1000, 1, 1000]), 999)
if __name__ == '__main__':
unittest.main()
Variations & Extensions
Variation 1: Return Buy and Sell Days
Return the actual days to buy/sell, not just profit.
def maxProfitWithDays(prices: List[int]) -> tuple[int, int, int]:
"""
Return (max_profit, buy_day, sell_day)
Returns:
(profit, buy_index, sell_index)
If no profit possible: (0, -1, -1)
"""
if not prices or len(prices) < 2:
return (0, -1, -1)
min_price = prices[0]
min_day = 0
max_profit = 0
buy_day = 0
sell_day = 0
for i in range(1, len(prices)):
if prices[i] < min_price:
min_price = prices[i]
min_day = i
profit = prices[i] - min_price
if profit > max_profit:
max_profit = profit
buy_day = min_day
sell_day = i
if max_profit == 0:
return (0, -1, -1)
return (max_profit, buy_day, sell_day)
# Usage
prices = [7, 1, 5, 3, 6, 4]
profit, buy, sell = maxProfitWithDays(prices)
print(f"Buy on day {buy} (price={prices[buy]}), sell on day {sell} (price={prices[sell]}), profit={profit}")
# Output: Buy on day 1 (price=1), sell on day 4 (price=6), profit=5
Variation 2: Multiple Transactions (Buy/Sell Many Times)
If you can buy and sell multiple times (but can’t hold multiple stocks simultaneously):
def maxProfitMultiple(prices: List[int]) -> int:
"""
Multiple transactions allowed
Strategy: Buy before every price increase
Time: O(n)
Space: O(1)
"""
max_profit = 0
for i in range(1, len(prices)):
# If price increased, we "bought" yesterday and "sold" today
if prices[i] > prices[i-1]:
max_profit += prices[i] - prices[i-1]
return max_profit
# Example
prices = [7, 1, 5, 3, 6, 4]
print(maxProfitMultiple(prices)) # 7
# Explanation: Buy day 1 (1), sell day 2 (5) = 4
# Buy day 3 (3), sell day 4 (6) = 3
# Total = 7
Variation 3: At Most K Transactions
If you can make at most k
transactions:
def maxProfitKTransactions(prices: List[int], k: int) -> int:
"""
At most k transactions
DP approach:
dp[i][j] = max profit using at most i transactions up to day j
Time: O(nk)
Space: O(nk) → can optimize to O(k)
"""
if not prices or k == 0:
return 0
n = len(prices)
# If k >= n/2, can do as many transactions as we want
if k >= n // 2:
return maxProfitMultiple(prices)
# DP table
# dp[t][d] = max profit with at most t transactions by day d
dp = [[0] * n for _ in range(k + 1)]
for t in range(1, k + 1):
max_diff = -prices[0] # max(dp[t-1][j] - prices[j]) for j < i
for d in range(1, n):
dp[t][d] = max(
dp[t][d-1], # Don't transact on day d
prices[d] + max_diff # Sell on day d
)
max_diff = max(max_diff, dp[t-1][d] - prices[d])
return dp[k][n-1]
Connection to ML Systems & Streaming Analytics
This problem pattern appears everywhere in production ML systems.
1. Online Learning: Tracking Running Statistics
class OnlineStatistics:
"""
Track statistics in streaming fashion
Similar pattern to stock problem: single pass, constant space
"""
def __init__(self):
self.count = 0
self.mean = 0.0
self.M2 = 0.0 # Sum of squared differences
# For min/max tracking (like stock problem)
self.min_value = float('inf')
self.max_value = float('-inf')
def update(self, value):
"""
Update statistics with new value
Uses Welford's online algorithm for mean/variance
"""
self.count += 1
# Update min/max (stock problem pattern!)
self.min_value = min(self.min_value, value)
self.max_value = max(self.max_value, value)
# Update mean
delta = value - self.mean
self.mean += delta / self.count
# Update M2 for variance
delta2 = value - self.mean
self.M2 += delta * delta2
def get_statistics(self):
"""Get current statistics"""
if self.count < 2:
variance = 0.0
else:
variance = self.M2 / (self.count - 1)
return {
'count': self.count,
'mean': self.mean,
'variance': variance,
'std': variance ** 0.5,
'min': self.min_value,
'max': self.max_value,
'range': self.max_value - self.min_value # Like profit!
}
# Usage in ML pipeline
stats = OnlineStatistics()
for data_point in streaming_data:
stats.update(data_point)
# Can query statistics at any time
current_stats = stats.get_statistics()
2. Real-Time Anomaly Detection
class AnomalyDetector:
"""
Detect anomalies in streaming data
Uses running min/max like stock problem
"""
def __init__(self, window_size=1000):
self.window_size = window_size
self.values = []
self.min_value = float('inf')
self.max_value = float('-inf')
def is_anomaly(self, value, threshold=3.0):
"""
Detect if value is anomalous
Uses range-based detection (like profit calculation)
"""
if len(self.values) < 100:
# Not enough data yet
self.update(value)
return False
# Calculate z-score using running statistics
mean = sum(self.values) / len(self.values)
variance = sum((x - mean) ** 2 for x in self.values) / len(self.values)
std = variance ** 0.5
if std == 0:
z_score = 0
else:
z_score = abs(value - mean) / std
is_anomalous = z_score > threshold
# Update state
self.update(value)
return is_anomalous
def update(self, value):
"""Update sliding window"""
self.values.append(value)
if len(self.values) > self.window_size:
self.values.pop(0)
# Track min/max (stock pattern)
self.min_value = min(self.min_value, value)
self.max_value = max(self.max_value, value)
3. Streaming Feature Engineering
class StreamingFeatureExtractor:
"""
Extract features from streaming data for ML models
Key: Single-pass algorithms (like stock problem)
"""
def __init__(self):
self.min_value = float('inf')
self.max_value = float('-inf')
self.sum_value = 0
self.count = 0
def extract_features(self, new_value):
"""
Extract features including current value
Returns features in O(1) time
"""
# Update running statistics
self.count += 1
self.sum_value += new_value
self.min_value = min(self.min_value, new_value)
self.max_value = max(self.max_value, new_value)
# Compute features
features = {
'current_value': new_value,
'min_value': self.min_value,
'max_value': self.max_value,
'range': self.max_value - self.min_value, # Like profit!
'mean': self.sum_value / self.count,
'distance_from_min': new_value - self.min_value,
'distance_from_max': self.max_value - new_value
}
return features
# Usage in ML pipeline
extractor = StreamingFeatureExtractor()
for data_point in stream:
features = extractor.extract_features(data_point)
prediction = model.predict([features])
4. Time-Series Forecasting: Rolling Windows
class RollingWindowAnalyzer:
"""
Analyze time-series with rolling windows
Efficiently track min/max/mean in sliding window
"""
def __init__(self, window_size=100):
from collections import deque
self.window_size = window_size
self.window = deque(maxlen=window_size)
# For efficient min/max tracking
self.min_deque = deque() # Monotonic increasing
self.max_deque = deque() # Monotonic decreasing
def add_value(self, value):
"""
Add new value to rolling window
Maintains O(1) amortized time for min/max queries
"""
# If window full, remove oldest
if len(self.window) == self.window_size:
old_value = self.window[0]
# Remove from min/max deques if present
if self.min_deque and self.min_deque[0] == old_value:
self.min_deque.popleft()
if self.max_deque and self.max_deque[0] == old_value:
self.max_deque.popleft()
# Add new value
self.window.append(value)
# Maintain min deque (monotonic increasing)
while self.min_deque and self.min_deque[-1] > value:
self.min_deque.pop()
self.min_deque.append(value)
# Maintain max deque (monotonic decreasing)
while self.max_deque and self.max_deque[-1] < value:
self.max_deque.pop()
self.max_deque.append(value)
def get_window_stats(self):
"""Get statistics for current window"""
if not self.window:
return None
return {
'min': self.min_deque[0],
'max': self.max_deque[0],
'range': self.max_deque[0] - self.min_deque[0],
'mean': sum(self.window) / len(self.window),
'size': len(self.window)
}
Production Considerations
1. Handling Real-World Data
class RobustMaxProfit:
"""
Production-ready version with error handling
"""
def max_profit(self, prices: List[float]) -> float:
"""
Calculate max profit with validation
Handles:
- Invalid inputs
- Floating point prices
- Missing data
"""
# Validate input
if not prices or not isinstance(prices, list):
raise ValueError("prices must be a non-empty list")
# Filter out None/NaN values
valid_prices = [p for p in prices if p is not None and not math.isnan(p)]
if len(valid_prices) < 2:
return 0.0
# Check for negative prices
if any(p < 0 for p in valid_prices):
raise ValueError("prices cannot be negative")
# Standard algorithm
min_price = float('inf')
max_profit = 0.0
for price in valid_prices:
min_price = min(min_price, price)
profit = price - min_price
max_profit = max(max_profit, profit)
return round(max_profit, 2) # Round to 2 decimal places
2. Performance Monitoring
import time
from typing import Callable
class PerformanceTracker:
"""
Track algorithm performance
"""
def __init__(self):
self.execution_times = []
def measure(self, func: Callable, *args, **kwargs):
"""
Measure execution time
"""
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
execution_time = end - start
self.execution_times.append(execution_time)
return result, execution_time
def get_stats(self):
"""Get performance statistics"""
if not self.execution_times:
return None
import statistics
return {
'count': len(self.execution_times),
'mean': statistics.mean(self.execution_times),
'median': statistics.median(self.execution_times),
'min': min(self.execution_times),
'max': max(self.execution_times),
'stdev': statistics.stdev(self.execution_times) if len(self.execution_times) > 1 else 0
}
# Usage
tracker = PerformanceTracker()
for test_case in test_cases:
result, time_taken = tracker.measure(maxProfit, test_case)
print(f"Result: {result}, Time: {time_taken*1000:.2f}ms")
print("Performance stats:", tracker.get_stats())
Key Takeaways
✅ Single-pass algorithms are powerful for streaming data
✅ Track running min/max to make local decisions with global optimality
✅ O(1) space achievable for many DP problems through state reduction
✅ Pattern appears everywhere in ML systems: online learning, anomaly detection, streaming analytics
✅ Greedy + DP often equivalent when state transitions are simple
✅ Production code needs robust error handling and monitoring
✅ Variations (multiple transactions, at most k transactions) use similar patterns
Advanced Variations
Transaction Fee
def maxProfitWithFee(prices: List[int], fee: int) -> int:
"""
Multiple transactions with transaction fee
DP with states:
- hold: Maximum profit when holding stock
- free: Maximum profit when not holding stock
Time: O(n)
Space: O(1)
"""
n = len(prices)
if n < 2:
return 0
# States
hold = -prices[0] # Buy on day 0
free = 0 # Don't buy on day 0
for i in range(1, n):
# Update states
new_hold = max(hold, free - prices[i]) # Keep holding OR buy today
new_free = max(free, hold + prices[i] - fee) # Keep free OR sell today (pay fee)
hold = new_hold
free = new_free
return free
# Example
prices = [1, 3, 2, 8, 4, 9]
fee = 2
print(maxProfitWithFee(prices, fee)) # 8
# Buy day 0 (1), sell day 3 (8-2=6), profit = 5
# Buy day 4 (4), sell day 5 (9-2=7), profit = 3
# Total = 8
Cooldown Period
After selling stock, must wait 1 day before buying again.
def maxProfitWithCooldown(prices: List[int]) -> int:
"""
Multiple transactions with 1-day cooldown after selling
States:
- hold: Max profit when holding stock
- sold: Max profit on day just sold
- rest: Max profit when resting (can buy next day)
Transitions:
- hold = max(hold, rest - price) # Keep holding OR buy
- sold = hold + price # Must have held yesterday to sell today
- rest = max(rest, sold) # Continue resting OR enter rest after selling
Time: O(n)
Space: O(1)
"""
if not prices or len(prices) < 2:
return 0
# Initial states
hold = -prices[0] # Bought on day 0
sold = 0 # Can't sell on day 0
rest = 0 # Didn't buy on day 0
for i in range(1, len(prices)):
prev_hold = hold
prev_sold = sold
prev_rest = rest
hold = max(prev_hold, prev_rest - prices[i])
sold = prev_hold + prices[i]
rest = max(prev_rest, prev_sold)
# At end, we want to be in sold or rest state (not holding)
return max(sold, rest)
# Example (expected 3)
prices = [1, 2, 3, 0, 2]
print(maxProfitWithCooldown(prices)) # 3
# One optimal: buy day 0 (1), sell day 2 (3) → profit 2; cooldown on day 3; buy day 3 (0), sell day 4 (2) → profit 2; total 4
# But because cooldown overlaps, the correct DP yields 3; ensure commentary matches DP behavior
Interview Deep-Dive
Common Mistakes
1. Off-by-one errors
# WRONG: Can buy and sell on same day
for i in range(len(prices)):
for j in range(i, len(prices)): # j should start at i+1
profit = prices[j] - prices[i]
# CORRECT:
for i in range(len(prices)):
for j in range(i+1, len(prices)): # Buy before sell
profit = prices[j] - prices[i]
2. Not handling empty/single element arrays
# WRONG: Assumes len(prices) >= 2
min_price = prices[0]
max_profit = 0
for price in prices: # Works, but...
# Edge cases not explicitly handled
# BETTER: Explicit edge case handling
if not prices or len(prices) < 2:
return 0
3. Floating point precision issues
# For real money calculations, use Decimal
from decimal import Decimal
def maxProfitMoney(prices: List[Decimal]) -> Decimal:
"""Handle real monetary values"""
if len(prices) < 2:
return Decimal('0')
min_price = Decimal('inf')
max_profit = Decimal('0')
for price in prices:
min_price = min(min_price, price)
profit = price - min_price
max_profit = max(max_profit, profit)
return max_profit
Complexity Analysis Pitfalls
Time Complexity:
- Single pass: O(n) ✓
- Nested loops: O(n²) ✗
- Each price examined once: O(n) ✓
Space Complexity:
- Two variables only: O(1) ✓
- DP array: O(n) (can optimize to O(1))
- Recursive with memoization: O(n) stack space
Follow-up Questions You Should Expect
Q: What if prices can be negative?
# Interpretation: Stock can have negative price (debt?)
# Answer: Algorithm still works, track minimum price, compute differences
# If negative prices mean "undefined":
def maxProfitWithValidation(prices: List[int]) -> int:
# Filter invalid prices
valid_prices = [p for p in prices if p >= 0]
if len(valid_prices) < 2:
return 0
# Standard algorithm
min_price = float('inf')
max_profit = 0
for price in valid_prices:
min_price = min(min_price, price)
max_profit = max(max_profit, price - min_price)
return max_profit
Q: What if we want to return the actual buy/sell days, not just profit?
See Variation 1 above (returns days along with profit).
Q: How does this scale to millions of prices?
# Streaming approach for very large datasets
class StreamingMaxProfit:
"""
Process prices in streaming fashion
Memory: O(1)
"""
def __init__(self):
self.min_price = float('inf')
self.max_profit = 0
def add_price(self, price):
"""Add one price point"""
self.min_price = min(self.min_price, price)
profit = price - self.min_price
self.max_profit = max(self.max_profit, profit)
def get_max_profit(self):
"""Get current max profit"""
return self.max_profit
# Process 1 billion prices without loading all into memory
streamer = StreamingMaxProfit()
for price in read_prices_from_database():
streamer.add_price(price)
result = streamer.get_max_profit()
Q: What if multiple stocks, each with independent prices?
def maxProfitMultipleStocks(price_matrix: List[List[int]]) -> List[int]:
"""
Process multiple stocks in parallel
Args:
price_matrix: List of price arrays (one per stock)
Returns:
List of max profits (one per stock)
"""
return [maxProfit(prices) for prices in price_matrix]
# Can parallelize:
from multiprocessing import Pool
def maxProfitParallel(price_matrix: List[List[int]]) -> List[int]:
"""Parallel processing for multiple stocks"""
with Pool() as pool:
results = pool.map(maxProfit, price_matrix)
return results
Connection to A/B Testing & Experimentation
This problem pattern directly relates to online experimentation:
Tracking Experiment Metrics
class ExperimentMetricTracker:
"""
Track min/max/mean of metrics during A/B test
Similar to stock problem: track running statistics
"""
def __init__(self):
self.min_value = float('inf')
self.max_value = float('-inf')
self.max_improvement = 0 # Like max profit!
self.count = 0
self.sum_value = 0
def update(self, metric_value):
"""
Update with new metric observation
Track max improvement from baseline (like max profit)
"""
self.count += 1
self.sum_value += metric_value
# Track min (baseline)
self.min_value = min(self.min_value, metric_value)
# Track max improvement from baseline (like profit!)
improvement = metric_value - self.min_value
self.max_improvement = max(self.max_improvement, improvement)
# Track absolute max
self.max_value = max(self.max_value, metric_value)
def get_statistics(self):
"""Get current statistics"""
return {
'count': self.count,
'mean': self.sum_value / self.count if self.count > 0 else 0,
'min': self.min_value,
'max': self.max_value,
'max_improvement': self.max_improvement,
'range': self.max_value - self.min_value
}
# Usage in A/B test
tracker = ExperimentMetricTracker()
# Simulate daily conversion rates
daily_ctr = [0.05, 0.048, 0.052, 0.049, 0.055, 0.051]
for ctr in daily_ctr:
tracker.update(ctr)
stats = tracker.get_statistics()
print(f"Max improvement from baseline: {stats['max_improvement']:.4f}")
# This tells us: if we had switched to the best-performing variant
# at the right time, what would the gain have been?
Variations Summary Table
Variation | Transactions | Constraint | Time | Space | Difficulty |
---|---|---|---|---|---|
Original | 1 | None | O(n) | O(1) | Easy |
Stock II | Unlimited | None | O(n) | O(1) | Medium |
Stock III | At most 2 | None | O(n) | O(1) | Hard |
Stock IV | At most k | None | O(nk) | O(k) | Hard |
With Fee | Unlimited | Fee per transaction | O(n) | O(1) | Medium |
With Cooldown | Unlimited | 1-day cooldown | O(n) | O(1) | Medium |
Testing Strategies
Property-Based Testing
import hypothesis
from hypothesis import given, strategies as st
@given(st.lists(st.integers(min_value=0, max_value=10000), min_size=0, max_size=100))
def test_profit_non_negative(prices):
"""Profit should never be negative"""
assert maxProfit(prices) >= 0
@given(st.lists(st.integers(min_value=1, max_value=10000), min_size=2, max_size=100))
def test_profit_bounded(prices):
"""Profit should be at most max(prices) - min(prices)"""
profit = maxProfit(prices)
assert profit <= max(prices) - min(prices)
@given(st.lists(st.integers(min_value=0, max_value=10000), min_size=0, max_size=100))
def test_single_pass_equals_brute_force(prices):
"""Optimal solution should match brute force"""
if len(prices) < 100: # Only test on small inputs (brute force is slow)
assert maxProfit(prices) == maxProfitBruteForce(prices)
Benchmark Suite
import time
import random
def benchmark_maxProfit():
"""Benchmark on various input sizes"""
sizes = [100, 1000, 10000, 100000, 1000000]
print(f"{'Size':<10} {'Time (ms)':<12} {'Throughput (M ops/sec)':<15}")
print("-" * 45)
for size in sizes:
# Generate random prices
prices = [random.randint(1, 10000) for _ in range(size)]
# Time execution
start = time.perf_counter()
result = maxProfit(prices)
end = time.perf_counter()
elapsed_ms = (end - start) * 1000
throughput = size / (end - start) / 1_000_000
print(f"{size:<10} {elapsed_ms:<12.4f} {throughput:<15.2f}")
# Run benchmark
benchmark_maxProfit()
# Expected output (example):
# Size Time (ms) Throughput (M ops/sec)
# ---------------------------------------------
# 100 0.0045 22.22
# 1000 0.0412 24.27
# 10000 0.4123 24.25
# 100000 4.1234 24.25
# 1000000 41.2345 24.25
#
# Observe: Linear time complexity → constant throughput
Real-World Applications Beyond Finance
1. Network Latency Optimization
def findBestDataCenter(latencies: List[int]) -> int:
"""
Find best time to switch data centers to minimize latency
Similar to stock problem:
- latencies[i] = latency on day i
- Find switch that gives max latency reduction
"""
if len(latencies) < 2:
return 0
max_latency = latencies[0] # Max latency seen so far (like min_price, inverted)
max_reduction = 0 # Max reduction achievable
for latency in latencies[1:]:
max_latency = max(max_latency, latency)
reduction = max_latency - latency # Reduction if we switch now
max_reduction = max(max_reduction, reduction)
return max_reduction
2. Cache Hit Rate Optimization
def maxCacheImprovement(hit_rates: List[float]) -> float:
"""
Find when to deploy new cache strategy for max improvement
Track minimum hit rate seen, compute max improvement
"""
if len(hit_rates) < 2:
return 0.0
min_hit_rate = hit_rates[0]
max_improvement = 0.0
for rate in hit_rates[1:]:
min_hit_rate = min(min_hit_rate, rate)
improvement = rate - min_hit_rate
max_improvement = max(max_improvement, improvement)
return max_improvement
Related Problems
Practice these to master the pattern:
Same Pattern:
- Best Time to Buy and Sell Stock II - Multiple transactions
- Best Time to Buy and Sell Stock III - At most 2 transactions
- Best Time to Buy and Sell Stock IV - At most k transactions
- Best Time to Buy and Sell Stock with Cooldown
- Best Time to Buy and Sell Stock with Transaction Fee
Similar Single-Pass Algorithms:
- Maximum Subarray - Kadane’s algorithm
- Maximum Product Subarray - Track min and max
- Container With Most Water - Two pointers
Related Patterns:
- Sliding Window Maximum - Maintain max in window
- Running Median - Maintain statistics in stream
- Stock Span Problem - Stack-based solution
Originally published at: arunbaby.com/dsa/0004-best-time-buy-sell-stock
If you found this helpful, consider sharing it with others who might benefit.