Part 4 · Lesson 24 · 24 min read
Kadane's Algorithm
The maximum subarray sum in a single pass, and the stock-trading problems built on the same idea.
On this page
- The largest sum of any subarray
- Brute force: every start, every end
- The key question: extend or start again?
- Traced: Kadane on the classic example
- Returning the subarray itself
- Best time to buy and sell stock
- Maximum product: track the minimum too
- Circular subarrays
- Practice questions (6)
- Make it stick
- What's next
The largest sum of any subarray
You get an array with positive and negative numbers. Find the subarray with the largest sum. (A subarray is a block of items that sit next to each other.) For [-2, 1, -3, 4, -1, 2, 1, -5, 4] the answer is 6, from [4, -1, 2, 1]. Notice that this block includes a −1. Sometimes it is worth accepting a small loss to reach bigger gains later.
A sliding window does not work here (Lesson 23). With negative numbers, shrinking the window can make the sum bigger or smaller, so the window rules break. Kadane's algorithm solves the problem in one pass with a different idea. It is named after Jay Kadane, who found it.
Brute force: every start, every end
function maxSubArrayBrute(nums) { let best = -Infinity; for (let i = 0; i < nums.length; i++) { let sum = 0; for (let j = i; j < nums.length; j++) { sum += nums[j]; // sum of nums[i..j] best = Math.max(best, sum); } } return best;}console.log(maxSubArrayBrute([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // 6For each start, we keep a running sum as the end moves. This avoids an O(n³) solution (adding up every subarray from scratch). But it still costs O(n²) steps.
The key question: extend or start again?
Walk from left to right and keep one number called cur. It is the best sum of a subarray that ends exactly at the current index. For the next item x there are only two choices:
- Extend the best subarray ending at the previous index:
cur + x. - Start again with x alone:
x.
cur = Math.max(x, cur + x). If the sum so far is negative, it can only make the new sum smaller. So drop it and start again from x. The answer is the largest cur you ever see.
Traced: Kadane on the classic example
cur is the best sum of a subarray that ends exactly here. Only one subarray ends at index 0.
1const nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4];▶let cur = nums[0];3let best = nums[0];4for (let i = 1; i < nums.length; i++) {5 cur = Math.max(nums[i], cur + nums[i]);6 best = Math.max(best, cur);7}8console.log(best);| nums | [-2, 1, -3, 4, -1, 2, 1, -5, 4] |
| cur | -2 |
| i | x | cur | best | choice |
|---|---|---|---|---|
| 0 | −2 | −2 | −2 | first item |
| 1 | 1 | 1 | 1 | start again |
| 2 | −3 | −2 | 1 | extend |
| 3 | 4 | 4 | 4 | start again |
| 4 | −1 | 3 | 4 | extend |
| 5 | 2 | 5 | 5 | extend |
| 6 | 1 | 6 | 6 | extend |
| 7 | −5 | 1 | 6 | extend |
| 8 | 4 | 5 | 6 | extend |
It is important to start cur and best at nums[0], not at 0. Take an array where every number is negative, like [-3, -1, -2]. The answer is −1. If you start at 0, the code wrongly returns 0. This is also your first dynamic programming solution. Dynamic programming means you build the answer for each step from the answer for the previous step (Part 14).
Returning the subarray itself
Sometimes you must return the subarray itself, not just its sum. To do this, remember where the current run started. Whenever you find a new best sum, save that start position:
function maxSubArrayRange(nums) { let cur = nums[0], best = nums[0]; let start = 0, bestStart = 0, bestEnd = 0; for (let i = 1; i < nums.length; i++) { if (nums[i] > cur + nums[i]) { // starting again is better cur = nums[i]; start = i; } else { cur += nums[i]; } if (cur > best) { best = cur; bestStart = start; bestEnd = i; } } return { best, sub: nums.slice(bestStart, bestEnd + 1) };}console.log(maxSubArrayRange([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // { best: 6, sub: [ 4, -1, 2, 1 ] }Best time to buy and sell stock
You get the price of a stock for each day. Buy on one day and sell on a later day, to make the largest profit. The same “best ending here” thinking works. If you sell today, the best you can do is to have bought at the cheapest price seen so far. Keep track of that lowest price as you walk through the prices.
function maxProfit(prices) { let minPrice = Infinity, best = 0; for (const p of prices) { minPrice = Math.min(minPrice, p); // cheapest day to buy so far best = Math.max(best, p - minPrice); // sell today? } return best;}console.log(maxProfit([7, 1, 5, 3, 6, 4])); // 5 buy at 1, sell at 6console.log(maxProfit([7, 6, 4, 3, 1])); // 0 never profitableThis is Kadane's idea in another form. The profit from day i to day j equals the sum of the daily price changes between them. So the best trade is the maximum subarray of the list of daily changes.
Maximum product: track the minimum too
For the largest product, one number is not enough. A very negative product can become the largest positive product when you multiply it by another negative number. So keep two numbers: the largest product and the smallest product that end at the current index:
function maxProduct(nums) { let hi = nums[0], lo = nums[0], best = nums[0]; for (let i = 1; i < nums.length; i++) { const x = nums[i]; const candidates = [x, hi * x, lo * x]; hi = Math.max(...candidates); // largest product ending here lo = Math.min(...candidates); // smallest (most negative) product ending here best = Math.max(best, hi); } return best;}console.log(maxProduct([2, 3, -2, 4])); // 6console.log(maxProduct([-2, 3, -4])); // 24 the two negatives multiply| x | candidates | hi | lo | best |
|---|---|---|---|---|
| −2 | — | −2 | −2 | −2 |
| 3 | 3, −6, −6 | 3 | −6 | 3 |
| −4 | −4, −12, 24 | 24 | −12 | 24 |
The smallest product −6 (from −2 × 3) became the largest, 24, when multiplied by −4.
Circular subarrays
Now imagine the array is a circle, so a subarray may wrap from the end back to the start. A wrapping subarray is everything except a block in the middle. So the best wrapping sum is total − (the minimum subarray sum). Run Kadane for the maximum and for the minimum together. Then take the better of the two cases:
function maxSubarraySumCircular(nums) { let total = 0; let curMax = 0, bestMax = -Infinity; let curMin = 0, bestMin = Infinity; for (const x of nums) { total += x; curMax = Math.max(x, curMax + x); bestMax = Math.max(bestMax, curMax); curMin = Math.min(x, curMin + x); bestMin = Math.min(bestMin, curMin); } if (bestMax < 0) return bestMax; // all negative: the wrap trick would give 0 return Math.max(bestMax, total - bestMin);}console.log(maxSubarraySumCircular([5, -3, 5])); // 10 wraps around: [5, 5]console.log(maxSubarraySumCircular([-3, -2, -3])); // -2There is one edge case. If every value is negative, the minimum subarray is the whole array. Then total − bestMin would be 0, which means an empty subarray, and that is not allowed. In this case, return the normal maximum.
Practice questions
For each question, first say what “the best answer that ends at index i” means. Then write the one-line update.
Return the largest sum of any subarray that has at least one item.
Hint 1
Show all 2 solutions with explanations
This is the brute force method. It tries every start and every end.
function maxSubArray(nums) { let best = -Infinity; for (let i = 0; i < nums.length; i++) { let s = 0; for (let j = i; j < nums.length; j++) best = Math.max(best, (s += nums[j])); } return best;}console.log(maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // 6O(n²). It is too slow for 105 values.
At each item, extend the current run or start again. Keep the best sum you have seen.
function maxSubArray(nums) { let cur = nums[0], best = nums[0]; for (let i = 1; i < nums.length; i++) { cur = Math.max(nums[i], cur + nums[i]); best = Math.max(best, cur); } return best;}console.log(maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // 6console.log(maxSubArray([1])); // 1console.log(maxSubArray([-3, -1, -2])); // -1O(n) time, O(1) space.
Kadane is the answer interviewers expect. Be ready to return the start and end indices as a follow-up question. (LeetCode 53.)
Choose one day to buy and a later day to sell. Return the largest profit. If you cannot make a profit, return 0.
Hint 1
Show all 2 solutions with explanations
Try every buy day with every later sell day.
function maxProfit(prices) { let best = 0; for (let i = 0; i < prices.length; i++) for (let j = i + 1; j < prices.length; j++) best = Math.max(best, prices[j] - prices[i]); return best;}console.log(maxProfit([7, 1, 5, 3, 6, 4])); // 5O(n²).
Keep the lowest price seen so far. On each day, try to sell.
function maxProfit(prices) { let minPrice = Infinity, best = 0; for (const p of prices) { minPrice = Math.min(minPrice, p); best = Math.max(best, p - minPrice); } return best;}console.log(maxProfit([7, 1, 5, 3, 6, 4])); // 5console.log(maxProfit([7, 6, 4, 3, 1])); // 0O(n) time, O(1) space. First update the lowest price, then try to sell. This way you never sell before you buy. (If you buy and sell on the same day, the profit is 0.)
This is a very common first question. (LeetCode 121.)
You may buy and sell as many times as you like, but you can hold at most one share at a time. Return the largest profit.
Hint 1
Show the solution with explanations
Add prices[i] − prices[i − 1] whenever the result is positive.
function maxProfit(prices) { let profit = 0; for (let i = 1; i < prices.length; i++) { if (prices[i] > prices[i - 1]) profit += prices[i] - prices[i - 1]; } return profit;}console.log(maxProfit([7, 1, 5, 3, 6, 4])); // 7console.log(maxProfit([1, 2, 3, 4, 5])); // 4O(n) time. A long rise from 1 to 5 earns the same as its daily steps, 1+1+1+1. So splitting a trade into smaller trades never loses money.
Read the rules with care. One trade (Question 2) and many trades need very different solutions. (LeetCode 122.)
Return the largest product of any subarray that has at least one item.
Hint 1
Show the solution with explanations
For each x, the new largest and smallest products come from three choices: x alone, hi × x and lo × x.
function maxProduct(nums) { let hi = nums[0], lo = nums[0], best = nums[0]; for (let i = 1; i < nums.length; i++) { const x = nums[i]; const a = hi * x, b = lo * x; hi = Math.max(x, a, b); lo = Math.min(x, a, b); best = Math.max(best, hi); } return best;}console.log(maxProduct([2, 3, -2, 4])); // 6console.log(maxProduct([-2, 0, -1])); // 0console.log(maxProduct([-2, 3, -4])); // 24O(n) time, O(1) space. Work out a and b before you change hi. The new lo must use the old hi.
This is a classic follow-up to Question 1. (LeetCode 152.)
The array is circular, so the end connects back to the start. Return the largest sum of a subarray that has at least one item. The subarray may wrap around.
Hint 1
Show the solution with explanations
Make one pass. Work out the total, the best maximum and the best minimum.
function maxSubarraySumCircular(nums) { let total = 0, curMax = 0, bestMax = -Infinity, curMin = 0, bestMin = Infinity; for (const x of nums) { total += x; curMax = Math.max(x, curMax + x); bestMax = Math.max(bestMax, curMax); curMin = Math.min(x, curMin + x); bestMin = Math.min(bestMin, curMin); } return bestMax < 0 ? bestMax : Math.max(bestMax, total - bestMin);}console.log(maxSubarraySumCircular([1, -2, 3, -2])); // 3console.log(maxSubarraySumCircular([5, -3, 5])); // 10console.log(maxSubarraySumCircular([-3, -2, -3])); // -2| Case | Value |
|---|---|
| Best non-wrapping (Kadane) | 7 ([5, −3, 5]) |
| Total | 7 |
| Smallest subarray | −3 |
| Best wrapping = 7 − (−3) | 10 |
O(n) time, O(1) space. The key idea is to change “wraps around” into “everything except a middle part”. (LeetCode 918.)
Return the largest absolute value of the sum of any subarray. The empty subarray, with sum 0, is allowed.
Hint 1
Show the solution with explanations
Run both versions in one pass. Return the larger of the best maximum and −(the best minimum).
function maxAbsoluteSum(nums) { let curMax = 0, curMin = 0, best = 0; for (const x of nums) { curMax = Math.max(x, curMax + x); curMin = Math.min(x, curMin + x); best = Math.max(best, curMax, -curMin); } return best;}console.log(maxAbsoluteSum([1, -3, 2, 3, -4])); // 5console.log(maxAbsoluteSum([2, -5, 1, -4, 3, -2])); // 8O(n) time, O(1) space. There is another way to see it. The answer equals (largest prefix sum) − (smallest prefix sum), using Lesson 20.
When you know the max version and the min version of Kadane, many variations take only a few lines. (LeetCode 1749.)
Make it stick
- Write Kadane's algorithm from memory and explain “extend or start again”.
- Say why
curandbeststart atnums[0]and not 0. - Explain why the maximum product needs the minimum product too.
- Explain the circular case using “total minus the minimum subarray”.
Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.
What's next
Lesson 25 finishes Part 4 with 2-D arrays. A 2-D array is a grid with rows and columns. You will transpose a matrix (swap its rows and columns), rotate it, read it in a spiral, and search a sorted matrix. These are all common interview questions.
Practise on LeetCode
Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series