Kadane's Algorithm Explained — Maximum Subarray Sum in O(n)
- Kadane's core decision is:
currentSum = Math.max(element, currentSum + element)— this single line encodes the entire greedy insight that a negative prefix can never help you. - Always seed currentSum and maxSumFound with the first element, not with 0 — seeding with 0 silently breaks the all-negative case and gives a wrong answer without throwing an error.
- When extending to track subarray indices, only commit bestStart when you confirm a new global maximum — premature commitment on reset is the most common bug in the extended version.
- Core decision: currentSum = max(arr[i], currentSum + arr[i])
- Tracks two values: currentSum (best ending here) and maxSumFound (global best)
- Time: O(n). Space: O(1). Single pass, two variables.
- Seed with arr[0], not 0 — otherwise all-negative arrays return wrong answer.
- Financial analytics: find peak profitability window in time-series data.
- Signal processing: detect strongest signal burst in noisy data.
- Seeding currentSum with 0. Returns 0 for [-3, -1, -2] instead of correct answer -1.
Returns 0 for all-negative input.
Print initial values: System.out.println("currentSum=" + currentSum + " maxSumFound=" + maxSumFound)Run with input [-3, -1, -2] — expected answer: -1Correct sum but wrong subarray boundaries.
Add trace: System.out.println("i=" + i + " reset=" + (values[i] > currentSum + values[i]) + " newMax=" + (currentSum > maxSumFound))Verify bestStart is only updated inside the 'new global max' blockWrong answer on large arrays with big values.
Print currentSum at each step and watch for sign flip (positive to negative)Change currentSum and maxSumFound from int to longAlgorithm is O(n²) despite using Kadane's pattern.
Count total operations: add a counter incremented inside the loopVerify counter equals arr.length - 1Production Incident
Production Debug GuideSymptom-first investigation path for maximum subarray bugs.
Maximum subarray sum — finding the contiguous segment of an array with the largest sum — appears in financial analytics (peak profitability windows), signal processing (strongest signal bursts), and genomics (highest-expression DNA regions). The naive approach checks every start-end pair: O(n²). For a 10,000-element array, that is 50 million operations. Kadane's Algorithm solves it in a single linear pass: O(n), O(1) space.
The algorithm maintains two values: currentSum (the best sum ending at the current position) and maxSumFound (the global best). At each element, it decides: extend the current subarray or start fresh. The decision is greedy and provably correct — a negative prefix can never improve a subarray's sum, so it is always discarded.
The common misconception is that this is 'just a DP problem.' While Kadane's recurrence (dp[i] = max(arr[i], dp[i-1] + arr[i])) is a DP formulation, the O(1) space optimization makes it a distinct pattern. Understanding the greedy intuition — not just the recurrence — is what separates candidates who can explain it on a whiteboard from those who just memorize the code.
How Kadane's Algorithm Works — Plain English
Kadane's algorithm finds the maximum-sum contiguous subarray in O(n) time and O(1) space. The key insight: at each position, either extend the previous subarray or start a fresh one — whichever gives a higher sum.
Step-by-step: 1. Initialize current_sum = arr[0], max_sum = arr[0]. 2. For i from 1 to n-1: a. current_sum = max(arr[i], current_sum + arr[i]). b. max_sum = max(max_sum, current_sum). 3. Return max_sum.
Worked example on [-2, 1, -3, 4, -1, 2, 1, -5, 4]: i=0: current=-2, max=-2. i=1: current=max(1,-2+1)=1. max=1. i=2: current=max(-3,1-3)=-2. max=1. i=3: current=max(4,-2+4)=4. max=4. i=4: current=max(-1,4-1)=3. max=4. i=5: current=max(2,3+2)=5. max=5. i=6: current=max(1,5+1)=6. max=6. i=7: current=max(-5,6-5)=1. max=6. i=8: current=max(4,1+4)=5. max=6. Maximum sum subarray = 6, from [4,-1,2,1].
package io.thecodeforge.algo; import java.util.Arrays; public class KadanePlainEnglish { public static long findMaxSubarraySum(int[] arr) { long currentSum = arr[0]; long maxSum = arr[0]; for (int i = 1; i < arr.length; i++) { // Core decision: extend or restart? currentSum = Math.max(arr[i], currentSum + arr[i]); maxSum = Math.max(maxSum, currentSum); } return maxSum; } public static void main(String[] args) { int[] arr = {-2, 1, -3, 4, -1, 2, 1, -5, 4}; System.out.println("Array: " + Arrays.toString(arr)); System.out.println("Max subarray sum: " + findMaxSubarraySum(arr)); // 6 // Edge case: all negatives int[] allNeg = {-8, -3, -6, -2, -5}; System.out.println("\nAll negatives: " + findMaxSubarraySum(allNeg)); // -2 // Edge case: single element int[] single = {42}; System.out.println("Single element: " + findMaxSubarraySum(single)); // 42 } }
Max subarray sum: 6
All negatives: -2
Single element: 42
- currentSum > 0: extend. The running sum boosts future elements.
- currentSum <= 0: restart. The running sum is dead weight.
- maxSumFound: tracks the global best across all positions.
- Each element is visited exactly once. O(n) time, O(1) space.
- The algorithm never backtracks. It makes the locally optimal choice at each step.
The Core Intuition — Why the Greedy Decision Works
Before touching code, let's lock in the mental model. At every index i, you face exactly one binary choice: does the current element join the existing subarray, or does it become the start of a brand-new one?
The rule is beautifully simple: if your running sum so far is negative, it's dead weight. Carrying it into the next element can only make that element's contribution worse. So you drop it. You set your running sum equal to the current element and start fresh. If your running sum is positive, it's a booster — keep it.
This is a greedy decision, and it's correct because the optimal subarray can never benefit from a negative prefix. Think about it: if the subarray [3, -5, 2, 4] were the optimal answer, you could always remove the [3, -5] prefix (which sums to -2) and get [2, 4] with a larger sum. That contradiction proves negative prefixes can never be part of the true maximum subarray.
The algorithm tracks two values at all times: currentSum (the best sum ending at the current position) and maxSumFound (the global best seen so far). Every step, you update both. When the loop finishes, maxSumFound is your answer.
package io.thecodeforge.algo; public class KadaneBasic { /** * Finds the maximum sum of any contiguous subarray. * Uses Kadane's Algorithm — O(n) time, O(1) space. */ public static long findMaxSubarraySum(int[] profits) { // Seed with first element — NEVER seed with 0 long currentSum = profits[0]; long maxSumFound = profits[0]; for (int day = 1; day < profits.length; day++) { // Core decision: extend the current run, or start fresh today? currentSum = Math.max(profits[day], currentSum + profits[day]); maxSumFound = Math.max(maxSumFound, currentSum); } return maxSumFound; } public static void main(String[] args) { int[] dailyProfits = {-2, 1, -3, 4, -1, 2, 1, -5, 4}; System.out.println("Max subarray sum: " + findMaxSubarraySum(dailyProfits)); // 6 int[] allLosses = {-8, -3, -6, -2, -5}; System.out.println("All negatives: " + findMaxSubarraySum(allLosses)); // -2 int[] oneDay = {42}; System.out.println("Single element: " + findMaxSubarraySum(oneDay)); // 42 } }
All negatives: -2
Single element: 42
currentSum and maxSumFound to 0 breaks the all-negative case. If the input is [-5, -1, -3], the correct answer is -1, but seeding with 0 gives you 0 — which corresponds to an empty subarray. Most problem statements require at least one element to be chosen. Always seed with profits[0].Tracking the Actual Subarray — Not Just the Sum
The basic version tells you the maximum sum, but real-world use cases often need to know which subarray produced it. Imagine a financial dashboard that needs to highlight the exact date range of peak profitability — the number alone isn't enough.
Extending Kadane's to track indices takes almost no extra code. You record a potentialStart index whenever you decide to restart (i.e., when starting fresh beats extending). The moment that fresh start leads to a new global maximum, you commit it: you update bestStart, bestEnd, and maxSumFound together as an atomic snapshot.
This is a subtle but important pattern. You only want to commit the start index when you've confirmed it produced a better result, not optimistically when you reset. That distinction separates a correct implementation from a buggy one that snaps the wrong start index.
package io.thecodeforge.algo; public class KadaneWithIndices { static class SubarrayResult { final long maxSum; final int startIndex; final int endIndex; SubarrayResult(long maxSum, int startIndex, int endIndex) { this.maxSum = maxSum; this.startIndex = startIndex; this.endIndex = endIndex; } @Override public String toString() { return String.format("Max Sum: %d | Start: %d | End: %d", maxSum, startIndex, endIndex); } } /** * Kadane's extended to return subarray boundaries. O(n) time, O(1) space. */ public static SubarrayResult findMaxSubarray(int[] values) { long currentSum = values[0]; long maxSumFound = values[0]; int potentialStart = 0; int bestStart = 0; int bestEnd = 0; for (int index = 1; index < values.length; index++) { // If starting fresh beats extending, reset and mark tentative start if (values[index] > currentSum + values[index]) { currentSum = values[index]; potentialStart = index; } else { currentSum += values[index]; } // Only commit boundaries when we confirm a new global maximum if (currentSum > maxSumFound) { maxSumFound = currentSum; bestStart = potentialStart; bestEnd = index; } } return new SubarrayResult(maxSumFound, bestStart, bestEnd); } public static void main(String[] args) { int[] stockReturns = {-2, 1, -3, 4, -1, 2, 1, -5, 4}; SubarrayResult result = findMaxSubarray(stockReturns); System.out.println(result); // Print the actual subarray System.out.print("Subarray: ["); for (int i = result.startIndex; i <= result.endIndex; i++) { System.out.print(stockReturns[i] + (i < result.endIndex ? ", " : "]\n")); } } }
Subarray: [4, -1, 2, 1]
- potentialStart: updated on every reset. Tentative — may never become the answer.
- bestStart: updated only when currentSum > maxSumFound. Confirmed winner.
- Premature commit: setting bestStart = potentialStart on reset gives wrong boundaries.
- Lazy commit: setting bestStart = potentialStart only inside the max-update block is correct.
- Test with [1, 2, -10, 3, 7, -1, 8]. Expected: start=3, end=6, sum=17.
Kadane's vs. Brute Force vs. Divide and Conquer — Choosing Wisely
Understanding why Kadane's is the right choice requires knowing what you're giving up and getting compared to the alternatives.
The brute-force approach uses two nested loops to check every (start, end) pair. It's O(n²) and straightforward to reason about, which makes it fine for arrays under a few hundred elements in non-critical paths. Divide and Conquer solves the problem in O(n log n) — better than brute force, but still worse than Kadane's. Its advantage is that it's parallelizable: you can split the array across multiple cores and merge results, which matters in distributed systems processing massive datasets.
Kadane's wins on pure time complexity and simplicity for the single-machine case. One pass, two variables, done. The only scenario where you'd reach for Divide and Conquer over Kadane's is when you need to run this computation across distributed partitions where a central linear scan isn't possible — think MapReduce jobs over terabyte-scale log files.
For coding interviews, Kadane's is the expected answer the moment you see 'maximum subarray.' Mentioning the Divide and Conquer alternative shows depth and will genuinely impress a senior interviewer.
package io.thecodeforge.algo; public class KadaneVsBruteForce { /** Brute Force: O(n^2) */ public static long bruteForceMaxSum(int[] values) { long globalMax = values[0]; for (int start = 0; start < values.length; start++) { long runningSum = 0; for (int end = start; end < values.length; end++) { runningSum += values[end]; globalMax = Math.max(globalMax, runningSum); } } return globalMax; } /** Kadane's: O(n) */ public static long kadaneMaxSum(int[] values) { long currentSum = values[0]; long globalMax = values[0]; for (int index = 1; index < values.length; index++) { currentSum = Math.max(values[index], currentSum + values[index]); globalMax = Math.max(globalMax, currentSum); } return globalMax; } public static void main(String[] args) { int[] data = {-2, 1, -3, 4, -1, 2, 1, -5, 4}; long t1 = System.nanoTime(); long brute = bruteForceMaxSum(data); long bruteTime = System.nanoTime() - t1; long t2 = System.nanoTime(); long kadane = kadaneMaxSum(data); long kadaneTime = System.nanoTime() - t2; System.out.println("Brute: " + brute + " (" + bruteTime + " ns)"); System.out.println("Kadane: " + kadane + " (" + kadaneTime + " ns)"); System.out.println("Match: " + (brute == kadane)); } }
Kadane: 6 (1203 ns)
Match: true
- Brute force: O(n^2). Simple. Fine for arrays under 1,000 elements.
- Divide and conquer: O(n log n). Parallelizable. Best for distributed systems.
- Kadane's: O(n). Simple. Best for single-machine linear scans.
- Interview expected answer: Kadane's. Mention D&C for bonus points.
- D&C advantage: split array across machines, compute max in each partition, merge by checking the crossing subarray.
| Aspect | Brute Force | Divide & Conquer | Kadane's Algorithm |
|---|---|---|---|
| Time Complexity | O(n^2) | O(n log n) | O(n) |
| Space Complexity | O(1) | O(log n) stack | O(1) |
| Code Complexity | Simple | Moderate | Simple |
| Handles All-Negatives | Yes | Yes | Yes (if seeded correctly) |
| Parallelizable | No | Yes | No (sequential by design) |
| Interview Expectation | Starting point only | Bonus depth answer | Primary expected answer |
| Real-world fit | Small arrays, prototyping | Distributed/MapReduce jobs | Standard single-machine use |
| Tracks Subarray Indices | Yes (trivially) | Yes (with merge logic) | Yes (with lazy commit pattern) |
| Overflow Risk | Low (small n) | Low (small n) | High (large n, use long) |
| Best Input Size | < 1,000 elements | Any (distributed) | Any (single machine) |
🎯 Key Takeaways
- Kadane's core decision is:
currentSum = Math.max(element, currentSum + element)— this single line encodes the entire greedy insight that a negative prefix can never help you. - Always seed currentSum and maxSumFound with the first element, not with 0 — seeding with 0 silently breaks the all-negative case and gives a wrong answer without throwing an error.
- When extending to track subarray indices, only commit bestStart when you confirm a new global maximum — premature commitment on reset is the most common bug in the extended version.
- Kadane's is O(n) time and O(1) space — if an interviewer asks about the trade-off vs. Divide and Conquer, mention that D&C is more parallelizable for distributed systems, while Kadane's wins for single-machine linear scans.
- Use long for currentSum and maxSumFound to prevent integer overflow on large arrays. A 10,000-element array with values averaging 10^6 overflows int.
- For circular arrays, the maximum circular subarray sum = total sum - minimum subarray sum. Run Kadane's twice: once for max, once for min.
- The greedy decision is provably correct: if a subarray has a negative prefix, removing the prefix strictly increases the sum. No negative prefix can be part of the optimal solution.
- Test Kadane's with four edge cases: all-negative, single element, all-positive, and mixed. If all four pass, the implementation is correct.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QWalk me through Kadane's Algorithm step by step on the array [-2, 1, -3, 4, -1, 2, 1, -5, 4] — what is currentSum and maxSumFound at each index?
- QHow would you modify Kadane's Algorithm to return the actual subarray (start and end indices), not just the maximum sum? What is the tricky part of tracking the start index correctly?
- QWhat does Kadane's Algorithm return if every element in the array is negative — and why does the initialization strategy matter for correctness? How would your answer change if the problem allowed choosing an empty subarray?
- QHow would you extend Kadane's to find the maximum sum subarray in a circular array? What is the relationship between the maximum circular subarray and the minimum subarray?
- QExplain why Kadane's Algorithm is a greedy algorithm, not just a DP algorithm. What is the greedy choice and why is it provably correct?
- QHow would you solve the 2D maximum subarray problem (maximum sum rectangle in a matrix) using Kadane's? What is the time complexity?
- QIs Kadane's Algorithm parallelizable? If not, what alternative would you use for distributed systems processing terabyte-scale arrays?
- QHow would you modify Kadane's to find the maximum product subarray instead of maximum sum? Why is this harder than the sum version?
- QGiven an array of integers, find the subarray with the largest sum that has length at most k. How does this modify the basic Kadane's approach?
- QHow would you use Kadane's Algorithm to find the most profitable consecutive trading days, and what edge cases would you test?
Frequently Asked Questions
Does Kadane's Algorithm work when all numbers in the array are negative?
Yes, but only if you initialize correctly. Seed currentSum and maxSumFound with the first element (not with 0). That way, on an array like [-8, -3, -6], the algorithm correctly returns -3 — the least negative value. Seeding with 0 would return 0, which represents an empty subarray and is almost always the wrong answer for this class of problem.
What is the difference between Kadane's Algorithm and dynamic programming?
Kadane's Algorithm is a specific application of dynamic programming. The DP state is: 'what is the maximum subarray sum ending exactly at index i?' Kadane's solves this with the recurrence dp[i] = max(arr[i], dp[i-1] + arr[i]). Because each state only depends on the previous one, you can collapse the DP table into a single variable (currentSum), which is why it runs in O(1) space.
Can Kadane's Algorithm handle a 2D array (maximum subarray in a matrix)?
Not directly, but you can extend it. The standard approach is to fix the left and right column boundaries, then compress each row within those boundaries into a single value by summing across columns. This gives you a 1D array on which you run Kadane's normally. Repeating for all O(n^2) column boundary pairs gives an overall O(n^3) solution for an n*n matrix.
How do you find the actual subarray bounds, not just the sum?
Track potentialStart, bestStart, and bestEnd. When resetting (arr[i] > currentSum + arr[i]), set potentialStart = i. When updating maxSumFound (currentSum > maxSumFound), set bestStart = potentialStart and bestEnd = i. Only commit indices on new global maximum, not on reset.
How would you find the maximum circular subarray sum?
The maximum circular subarray either does not wrap (standard Kadane's) or wraps around (total sum minus the minimum subarray). Run Kadane's for the maximum, run a modified Kadane's (using min instead of max) for the minimum, then compare: max(maxKadane, totalSum - minKadane). Edge case: if all elements are negative, totalSum - minKadane = 0 (empty subarray), so return maxKadane.
Why is Kadane's considered greedy when it is also a DP formulation?
The recurrence dp[i] = max(arr[i], dp[i-1] + arr[i]) is a DP formulation. But the decision at each step is greedy: extend if beneficial, restart if not. The greedy property is that the locally optimal choice (discard negative prefix) is provably globally optimal. The DP table collapse to O(1) space is possible because each state depends only on the previous state — a property that makes the greedy interpretation natural.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.