Learn/DSA Patterns
DSA PatternsArrays & Two Pointersmedium14 min read

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.

#kadane#dynamic-programming#arrays#max-subarray#greedy#beginner#interview
Table of contents

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:

  1. Extend: take the subarray ending at i-1 and append nums[i] → value = prev + nums[i].
  2. 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 at i either:
    • 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. ✓

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.

ApproachTimeSpace
Check all subarraysO(n²)O(1)
Kadane's AlgorithmO(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:

  1. A normal (non-wrapping) subarray → use Kadane's directly.
  2. 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 = 0 instead of nums[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 to best_max.
  • Maximum product: not tracking both max AND min. Forgetting the minimum tracking causes bugs when two negatives multiply to a large positive.
BugFix
current = 0 initialisationUse current = nums[0]; start loop from index 1
Circular variant returns 0 for all-negative inputif best_min == total: return best_max
Max product: only tracking cur_maxAlso 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

ProblemDifficultyWhat to noticeLink
Maximum SubarrayMediumPure Kadane's — lock in the templateLC #53
Best Time to Buy and Sell StockEasyKadane's on day-over-day differencesLC #121
Maximum Product SubarrayMediumTrack both current max AND min (sign flips)LC #152
Maximum Sum Circular SubarrayMediummax(kadane_max, total - kadane_min); handle all-negativeLC #918
Longest Turbulent SubarrayMediumKadane's with a state machine for alternating signsLC #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.