Kadane's Algorithm
Find the maximum sum subarray in a single O(n) pass by tracking just one number: the best sum ending right here. We derive the idea from first principles, prove why a fresh start is sometimes the right move, and apply it to the circular variant.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- Watch it happen, frame by frame
- Now, the code — line by line
- Why initialise with nums[0] and not 0?
- Why does it never miss the best subarray?
- Why is it so fast?
- When should I reach for this? (the trigger list)
- The same trick in four disguises
- Disguise 1 — Maximum Product Subarray (LC #152)
- Disguise 2 — Maximum Circular Subarray (LC #918)
- Disguise 3 — Maximum Sum of Non-adjacent Elements
- Disguise 4 — Stock Buy and Sell (LC #121)
- Traps that catch beginners
- Say it like a pro (interview one-liner)
- Check yourself
- Practice problems
Before we start
This pattern sits right at the boundary between arrays and dynamic programming — it is often the first DP idea students encounter without realising it. By the end of this page you will be able to:
- See Kadane's algorithm as a decision made at every position: "extend the previous chain or start fresh?"
- Explain out loud why you only ever need to remember one number, not the whole history.
- Recognise the pattern and apply it to the circular variant — the classic interview hard follow-up.
Stop at every Pause & Think box. The decision logic there is the entire algorithm.
Picture this first (no code yet)
A real-life story
You are tracking your investment portfolio day by day. Each day, your net gain or loss is recorded:
Day: 1 2 3 4 5 6 7 8 9
+3 -1 +4 +1 -5 +9 +2 -6 +5
You want to find the single best streak — a consecutive run of days where your total gain was highest.
Here's how a smart investor thinks at each day:
- Standing at day 4 (gain: +1), you've been on a good run: +3 -1 +4 +1 = +7 total. You'd definitely continue this streak rather than throw away +7 and restart at just +1.
- Standing at day 5 (loss: -5), the streak is now +7 -5 = +2. Slightly worse but still positive. A rational investor continues — the streak still has a positive foundation.
- Standing at day 6 (gain: +9), the streak is now +2 +9 = +11. Continue.
But imagine a different version: suppose after day 3 the streak had somehow gone to -3 (deep negative). Then on day 4 (+1), you'd think: "Why carry a -3 millstone? I'm better off starting a fresh streak at just +1." Start fresh when the previous chain would drag you down.
That single decision — extend or restart — made greedily at every day — gives you the maximum subarray sum.
The actual problem
Given an integer array that may contain both positive and negative numbers, find the contiguous subarray with the largest sum. Return that sum.
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
answer = 6 (subarray [4, -1, 2, 1])
The array can have all negatives — in that case the best subarray is a single element (the least negative one).
First, the slow way (so you feel the pain)
The brute-force: try every possible subarray.
def max_subarray_slow(nums):
n = len(nums)
best = float('-inf')
for i in range(n):
total = 0
for j in range(i, n):
total += nums[j]
best = max(best, total)
return best
For every start index i, you extend to every end index j. That's roughly n²/2 subarrays:
n = 1,000 → ~500,000 pairs
n = 100,000 → ~5,000,000,000 pairs (five billion — too slow)
Kadane's does this in a single pass.
The turning point
Pause & think
You are at index i. You know: "the best subarray sum ending at index i-1" is some value prev.
There are only two choices for the best subarray ending at index i:
- Extend: take the subarray ending at
i-1and appendnums[i]→ value =prev + nums[i]. - Start fresh: begin a brand-new subarray at
i→ value =nums[i]alone.
Which choice should you make, and when?
You should extend when prev > 0 — adding prev makes nums[i] larger. You should start fresh when prev ≤ 0 — the previous chain drags you down; just take nums[i] alone.
In code: current = max(nums[i], prev + nums[i]).
But max(a, a + prev) chooses a when prev ≤ 0 and a + prev when prev > 0 — which is exactly max(nums[i], current + nums[i]).
And the global best is the maximum current seen across all positions.
The one idea to remember
The entire pattern in one sentence
At each position, the best subarray ending here is either nums[i] alone (start fresh) or nums[i] + best_ending_at_prev (extend) — whichever is larger; track the global maximum as you go.
You only ever need to remember one number — the best sum ending at the current position. No need to store the entire history.
Watch it happen, frame by frame
nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
i=0: nums[0]=-2. current = max(-2, 0 + (-2)) = -2. global_best = -2
i=1: nums[1]=1. current = max( 1, -2 + 1 ) = 1. global_best = 1
i=2: nums[2]=-3. current = max(-3, 1 + (-3)) = -2. global_best = 1
i=3: nums[3]=4. current = max( 4, -2 + 4 ) = 4. global_best = 4
i=4: nums[4]=-1. current = max(-1, 4 + (-1)) = 3. global_best = 4
i=5: nums[5]=2. current = max( 2, 3 + 2 ) = 5. global_best = 5
i=6: nums[6]=1. current = max( 1, 5 + 1 ) = 6. global_best = 6
i=7: nums[7]=-5. current = max(-5, 6 + (-5)) = 1. global_best = 6
i=8: nums[8]=4. current = max( 4, 1 + 4 ) = 5. global_best = 6
Answer: 6 ✅ (subarray [4,-1,2,1] from index 3 to 6)
Notice that at i=3, current resets effectively to 4 — the previous chain was -2 (negative), so starting fresh at +4 beats extending. That is the "start fresh" decision in action.
Pause & think
Cover the trace below. Apply Kadane's to [-1, -2, -3, -4]. What is the answer, and why?
Check your trace
i=0: current = max(-1, -inf + (-1)) = -1. global_best = -1
i=1: current = max(-2, -1 + (-2)) = -2. global_best = -1
i=2: current = max(-3, -2 + (-3)) = -3. global_best = -1
i=3: current = max(-4, -3 + (-4)) = -4. global_best = -1
Answer: -1 ✅
When all numbers are negative, the best subarray is a single element — the least negative one. Kadane's handles this correctly because at each step, "start fresh at nums[i]" beats "extend a deeply negative chain."
Now, the code — line by line
def maxSubArray(nums):
current = nums[0] # best sum of a subarray ending at the current position
global_best = nums[0] # best sum seen anywhere so far
for i in range(1, len(nums)):
current = max(nums[i], current + nums[i]) # extend or start fresh
global_best = max(global_best, current) # update the global champion
return global_best
Mapping to the portfolio story:
current = nums[0]— "Start on day 1; the streak is just that day."max(nums[i], current + nums[i])— "Is it better to continue the streak, or cut losses and start a new one today?"global_best = max(global_best, current)— "Record today's streak if it's the best we've ever seen."- The loop runs from index 1 — we already handled index 0 as the initial state.
Why initialise with nums[0] and not 0?
If we initialised current = 0, we'd allow the "empty subarray" (sum = 0) as a candidate. But the problem requires at least one element. Initialising with nums[0] guarantees at least the first element is included, and the loop handles the rest.
Why does it never miss the best subarray?
Claim: After processing index i, current holds the maximum sum of any subarray ending exactly at index i.
Proof by induction:
- Base:
i = 0. The only subarray ending at 0 is[nums[0]].current = nums[0]. ✓ - Step: Assume true for
i-1. Any subarray ending atieither:- consists of
nums[i]alone (sum =nums[i]), or - extends the best subarray ending at
i-1(sum =current_prev + nums[i]). max(nums[i], current_prev + nums[i])is the larger of the two. ✓
- consists of
global_best takes the maximum of all these, so it equals the maximum over all (start, end) pairs — which is the answer. □
Why is "extend or start fresh" a greedy choice — and why is greedy safe here?
Greedy works when a locally optimal choice leads to a globally optimal solution — i.e., when future choices don't regret past decisions. Here: if current_prev ≤ 0, adding it to nums[i] can only make things worse for every future extension too (because that prefix is a dead weight on everything that follows). So discarding it is always safe — no future subarray that starts at i would ever benefit from a negative prefix.
Why is it so fast?
One pass through the array. One comparison per element. No inner loops, no storing past subarrays.
| Approach | Time | Space |
|---|---|---|
| Check all subarrays | O(n²) | O(1) |
| Kadane's Algorithm | O(n) | O(1) |
When should I reach for this? (the trigger list)
Reach for Kadane's when you notice:
- The problem asks for the maximum (or minimum) sum subarray.
- The array has both positive and negative numbers — otherwise the answer is trivially the whole array.
- You see phrases like "contiguous subarray," "consecutive sequence," "best run."
- The brute force involves a double loop to check all subarrays.
- There's a circular variant (see below) — this is the classic extension.
Also recognise it in disguise: if "subarray" is replaced by "substring" (of integers), same pattern.
The same trick in four disguises
Disguise 1 — Maximum Product Subarray (LC #152)
Instead of sum, maximise the product. But products have a twist: a large negative number becomes a large positive when multiplied by another negative. So you must track both the current maximum and current minimum at each step.
def maxProduct(nums):
cur_max = cur_min = global_best = nums[0]
for x in nums[1:]:
candidates = (x, cur_max * x, cur_min * x)
cur_max = max(candidates) # a big negative × negative might be the new max
cur_min = min(candidates) # track minimum for future sign flips
global_best = max(global_best, cur_max)
return global_best
The "extend or start fresh" decision is the same; you just carry two running values instead of one.
Disguise 2 — Maximum Circular Subarray (LC #918)
The array is circular — the subarray can wrap around from the end back to the beginning.
Key insight: A circular maximum subarray is either:
- A normal (non-wrapping) subarray → use Kadane's directly.
- A wrapping subarray → the elements it excludes form a contiguous block in the middle → their sum is minimised → the answer is
total_sum − min_subarray_sum(run Kadane's on negated values).
def maxSubarraySumCircular(nums):
total = sum(nums)
# Case 1: standard Kadane's for non-circular max
cur = best_max = nums[0]
for x in nums[1:]:
cur = max(x, cur + x)
best_max = max(best_max, cur)
# Case 2: total - minimum subarray sum (circular wrap)
cur = best_min = nums[0]
for x in nums[1:]:
cur = min(x, cur + x)
best_min = min(best_min, cur)
# If all numbers are negative, best_min == total (entire array), giving total - total = 0
# which is wrong (empty subarray). In that case, case 1 gives the correct answer.
if best_min == total:
return best_max
return max(best_max, total - best_min)
Disguise 3 — Maximum Sum of Non-adjacent Elements
Not a Kadane variant, but looks similar. Track two states: "best including current" and "best excluding current." The underlying idea — carry forward a running decision — is the same spirit.
Disguise 4 — Stock Buy and Sell (LC #121)
Maximum profit = maximum of prices[j] - prices[i] for j > i. This is the maximum subarray problem in disguise, where the "array" is the day-over-day differences [prices[1]-prices[0], prices[2]-prices[1], ...]. Run Kadane's on those differences.
def maxProfit(prices):
min_price = prices[0]
max_profit = 0
for p in prices[1:]:
max_profit = max(max_profit, p - min_price)
min_price = min(min_price, p)
return max_profit
The min_price tracking is equivalent to "extending or restarting" based on the most advantageous buy point.
Traps that catch beginners
Watch out for these
- Initialising
current = 0instead ofnums[0]. This allows the empty subarray (sum = 0) as a valid answer. If all numbers are negative, the algorithm would wrongly return 0 instead of the least-negative element. - Starting the loop from index 0 instead of 1. If you start from 0 and initialise
current = nums[0], you double-count the first element. Always start the loop from index 1. - Forgetting the all-negatives case. Test mentally:
[-3, -1, -2]. Kadane's gives -1 ✅. Any variant with a 0 initialisation gives 0 ✗. - Circular variant: not handling the all-negatives edge case. If all numbers are negative,
total - best_min = total - total = 0(empty subarray). You must check and fall back tobest_max. - Maximum product: not tracking both max AND min. Forgetting the minimum tracking causes bugs when two negatives multiply to a large positive.
| Bug | Fix |
|---|---|
current = 0 initialisation | Use current = nums[0]; start loop from index 1 |
| Circular variant returns 0 for all-negative input | if best_min == total: return best_max |
Max product: only tracking cur_max | Also track cur_min; negatives can flip sign |
Say it like a pro (interview one-liner)
"I'll use Kadane's algorithm — a single pass that decides at each position whether to extend the previous subarray or start fresh. The decision is simply: is the running sum still positive? If yes, extend; if not, restart. I track the global best as I go. O(n) time, O(1) space."
For the circular variant, add:
"For the circular case, the answer is either a normal max subarray or the total minus the minimum subarray in the middle. I run Kadane's twice — once for max, once for min on the same array."
Remember this forever
Kadane's Algorithm — Maximum Subarray
At each position: current = max(nums[i], current + nums[i]) — extend if the chain adds value, restart if it drags you down. global_best = max(global_best, current) tracks the champion.
Initialise both with nums[0]. Start the loop at index 1.
Cost: O(n) time, O(1) space · Trigger: max/min sum of a contiguous subarray · Skeleton: cur = max(x, cur + x) at each step
Circular variant: max(kadane_max, total - kadane_min) — but guard against all-negative input.
Check yourself
Why initialise `current` with `nums[0]` instead of 0?
Initialising with 0 allows the algorithm to "choose" the empty subarray (which has sum 0) as a valid answer. The problem requires at least one element, so when all numbers are negative the answer should be the least negative element — not 0. Starting with nums[0] ensures the minimum subarray always contains at least one element.
At index i, the running sum went negative. Why is it always safe to restart?
Any subarray starting at index j ≤ i and ending at some future index k would have sum: sum(j..i) + sum(i+1..k). If sum(j..i) < 0, then dropping it and starting at i+1 gives sum(i+1..k) — strictly larger. A negative prefix is a dead weight on every future extension, so discarding it is always the right greedy choice.
How is the Maximum Stock Profit problem secretly a maximum subarray problem?
Transform the price array into day-over-day differences: diffs[i] = prices[i+1] - prices[i]. The profit from buying on day i and selling on day j equals diffs[i] + diffs[i+1] + … + diffs[j-1] — a contiguous sum of differences. Maximising that is exactly the maximum subarray problem. Kadane's on diffs gives the same answer as the standard min-tracking approach.
In the circular variant, what does `total - best_min` represent geometrically?
A circular subarray that wraps around has a "hole" in the middle — a contiguous block of elements that it skips. The sum of those skipped elements is total - circular_subarray_sum. Minimising the skipped block (finding best_min) maximises the circular subarray sum. So total - best_min is the best wrapping subarray sum.
Practice problems
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Maximum Subarray | Medium | Pure Kadane's — lock in the template | LC #53 |
| Best Time to Buy and Sell Stock | Easy | Kadane's on day-over-day differences | LC #121 |
| Maximum Product Subarray | Medium | Track both current max AND min (sign flips) | LC #152 |
| Maximum Sum Circular Subarray | Medium | max(kadane_max, total - kadane_min); handle all-negative | LC #918 |
| Longest Turbulent Subarray | Medium | Kadane's with a state machine for alternating signs | LC #978 |
Next up: Moore's Voting Algorithm, where a single clever cancellation trick finds the majority element in O(n) time and O(1) space — with no hashing, no sorting, and no counting.