Best Time to Buy and Sell Stock
Algorithm · Core
Step through it: Dynamic Programming pattern
Step 1 of 6
Goal: how many ways can you climb 5 stairs taking 1 or 2 steps at a time? You reach stair 5 from stair 4 or stair 3, so ways(5) = ways(4) + ways(3). Working that out naively repeats the same sub-questions many times. Instead write each answer down once.
Step 2 of 6
Base cases: there is 1 way to be on stair 0 (do nothing) and 1 way to reach stair 1.
Step 3 of 6
Stair 2 is reached from stair 1 or stair 0.
Step 4 of 6
Stair 3 comes from stairs 2 and 1.
Step 5 of 6
Stair 4: 3 + 2 = 5. Stair 5: 5 + 3 = 8. Each answer reuses two stored answers, so n stairs take n steps instead of exponential time. Storing sub-answers like this is dynamic programming.
Step 6 of 6
Your turn.
Task Description
Say you have an array prices for which the i-th element is the price of a given stock on day i.
Find the maximum profit. You may complete as many transactions as you like (i.e., buy one and sell one share of the stock multiple times).
Note: You may not engage in multiple transactions at the same time (i.e., you must sell the stock before you buy again).
Example #1
Input: [7, 1, 5, 3, 6, 4]
Output: 7Explanation: Buy on day 2 (price = 1) and sell on day 3 (price = 5), profit = 5-1 = 4. Then buy on day 4 (price = 3) and sell on day 5 (price = 6), profit = 6-3 = 3.
Example #2
Input: [1, 2, 3, 4, 5]
Output: 4Explanation: Buy on day 1 (price = 1) and sell on day 5 (price = 5), profit = 5-1 = 4. Note that you cannot buy on day 1, buy on day 2 and sell them later, as you are engaging multiple transactions at the same time. You must sell before buying again.
Example #3
Input: [7, 6, 4, 3, 1]
Output: 0Explanation: In this case, no transaction is done, i.e. max profit = 0.
Possible Solutions
Divide and conquer approach O(2^n)
We may try all combinations of buying and selling and find out the most profitable one by applying divide and conquer approach.
Let's say we have an array of prices [7, 6, 4, 3, 1] and we're on the 1st day of trading (at the very beginning of the array). At this point we may say that the overall maximum profit would be the maximum of two following values:
- Option 1: Keep the money → profit would equal to the profit from buying/selling the rest of the stocks →
keepProfit = profit([6, 4, 3, 1]). - Option 2: Buy/sell at current price → profit in this case would equal to the profit from buying/selling the rest of the stocks plus (or minus, depending on whether we're selling or buying) the current stock price →
buySellProfit = -7 + profit([6, 4, 3, 1]).
The overall profit would be equal to → overallProfit = Max(keepProfit, buySellProfit).
As you can see the profit([6, 4, 3, 1]) task is being solved in the same recursive manner.
See the full code example in dqBestTimeToBuySellStocks.js
Time Complexity
As you may see, every recursive call will produce 2 more recursive branches. The depth of the recursion will be n (size of prices array) and thus, the time complexity will equal to O(2^n).
As you may see, this is very inefficient. For example for just 20 prices the number of recursive calls will be somewhere close to 2M!
Additional Space Complexity
If we avoid cloning the prices array between recursive function calls and will use the array pointer then additional space complexity will be proportional to the depth of the recursion: O(n)
Peak Valley Approach O(n)
If we plot the prices array (i.e. [7, 1, 5, 3, 6, 4]) we may notice that the points of interest are the consecutive valleys and peaks
Image source: LeetCode
So, if we will track the growing price and will sell the stocks immediately before the price goes down we'll get the maximum profit (remember, we bought the stock in the valley at its low price).
See the full code example in peakvalleyBestTimeToBuySellStocks.js
Time Complexity
Since the algorithm requires only one pass through the prices array, the time complexity would equal O(n).
Additional Space Complexity
Except of the prices array itself the algorithm consumes the constant amount of memory. Thus, additional space complexity is O(1).
Accumulator Approach O(n)
There is even simpler approach exists. Let's say we have the prices array which looks like this [1, 7, 2, 3, 6, 7, 6, 7]:
Image source: LeetCode
You may notice, that we don't even need to keep tracking of a constantly growing price. Instead, we may simply add the price difference for all growing segments of the chart which eventually sums up to the highest possible profit,
See the full code example in accumulatorBestTimeToBuySellStocks.js
Time Complexity
Since the algorithm requires only one pass through the prices array, the time complexity would equal O(n).
Additional Space Complexity
Except of the prices array itself the algorithm consumes the constant amount of memory. Thus, additional space complexity is O(1).
Code
/**
* Finds the maximum profit from selling and buying the stocks.
* ACCUMULATOR APPROACH.
*
* @param {number[]} prices - Array of stock prices, i.e. [7, 6, 4, 3, 1]
* @param {function(): void} visit - Visiting callback to calculate the number of iterations.
* @return {number} - The maximum profit
*/
const accumulatorBestTimeToBuySellStocks = (prices, visit = () => {}) => {
visit();
let profit = 0;
for (let day = 1; day < prices.length; day += 1) {
visit();
// Add the increase of the price from yesterday till today (if there was any) to the profit.
profit += Math.max(prices[day] - prices[day - 1], 0);
}
return profit;
};
export default accumulatorBestTimeToBuySellStocks;/**
* Finds the maximum profit from selling and buying the stocks.
* DYNAMIC PROGRAMMING APPROACH.
*
* @param {number[]} prices - Array of stock prices, i.e. [7, 6, 4, 3, 1]
* @param {function(): void} visit - Visiting callback to calculate the number of iterations.
* @return {number} - The maximum profit
*/
const dpBestTimeToBuySellStocks = (prices, visit = () => {}) => {
visit();
let lastBuy = -prices[0];
let lastSold = 0;
for (let day = 1; day < prices.length; day += 1) {
visit();
const curBuy = Math.max(lastBuy, lastSold - prices[day]);
const curSold = Math.max(lastSold, lastBuy + prices[day]);
lastBuy = curBuy;
lastSold = curSold;
}
return lastSold;
};
export default dpBestTimeToBuySellStocks;/**
* Finds the maximum profit from selling and buying the stocks.
* DIVIDE & CONQUER APPROACH.
*
* @param {number[]} prices - Array of stock prices, i.e. [7, 6, 4, 3, 1]
* @param {function(): void} visit - Visiting callback to calculate the number of iterations.
* @return {number} - The maximum profit
*/
const dqBestTimeToBuySellStocks = (prices, visit = () => {}) => {
/**
* Recursive implementation of the main function. It is hidden from the users.
*
* @param {boolean} buy - Whether we're allow to sell or to buy now
* @param {number} day - Current day of trading (current index of prices array)
* @returns {number} - Max profit from buying/selling
*/
const recursiveBuyerSeller = (buy, day) => {
// Registering the recursive call visit to calculate the complexity.
visit();
// Quitting the recursion if this is the last day of trading (prices array ended).
if (day === prices.length) {
return 0;
}
// If we're buying - we're loosing money (-1), if we're selling we're getting money (+1).
const operationSign = buy ? -1 : +1;
return Math.max(
// Option 1: Don't do anything.
recursiveBuyerSeller(buy, day + 1),
// Option 2: Sell or Buy at the current price.
operationSign * prices[day] + recursiveBuyerSeller(!buy, day + 1),
);
};
const buy = true;
const day = 0;
return recursiveBuyerSeller(buy, day);
};
export default dqBestTimeToBuySellStocks;/**
* Finds the maximum profit from selling and buying the stocks.
* PEAK VALLEY APPROACH.
*
* @param {number[]} prices - Array of stock prices, i.e. [7, 6, 4, 3, 1]
* @param {function(): void} visit - Visiting callback to calculate the number of iterations.
* @return {number} - The maximum profit
*/
const peakvalleyBestTimeToBuySellStocks = (prices, visit = () => {}) => {
visit();
let profit = 0;
let low = prices[0];
let high = prices[0];
prices.slice(1).forEach((currentPrice) => {
visit();
if (currentPrice < high) {
// If price went down, we need to sell.
profit += high - low;
low = currentPrice;
high = currentPrice;
} else {
// If price went up, we don't need to do anything but increase a high record.
high = currentPrice;
}
});
// In case if price went up during the last day
// and we didn't have chance to sell inside the forEach loop.
profit += high - low;
return profit;
};
export default peakvalleyBestTimeToBuySellStocks;References
Practice
Dynamic Programming
- Climbing StairsEasy
- Fibonacci NumberEasy
- Min Cost Climbing StairsEasy
- Pascal's TriangleEasy
- House RobberMedium
- House Robber IIMedium
- Coin ChangeMedium
- Coin Change IIMedium
- Longest Increasing SubsequenceMedium
- Longest Common SubsequenceMedium
- Word BreakMedium
- Unique PathsMedium
- Unique Paths IIMedium
- Minimum Path SumMedium
- Jump GameMedium
- Partition Equal Subset SumMedium
- Target SumMedium
- Decode WaysMedium
- Maximum Product SubarrayMedium
- Longest Palindromic SubstringMedium
- Palindromic SubstringsMedium
- Best Time to Buy and Sell Stock with CooldownMedium
- TriangleMedium
- Maximal SquareMedium
- Edit DistanceHard
- Regular Expression MatchingHard
- Burst BalloonsHard
- Distinct SubsequencesHard
- Longest Valid ParenthesesHard
- Best Time to Buy and Sell Stock IVHard
Spotted a mistake or something unclear? Report it on GitHub.