Maximum Subarray in Java DSA: From Brute Force to Kadane’s Algorithm
-
Last Updated: July 29, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
Learn Maximum Subarray in Java DSA step by step — from brute force to Kadane’s algorithm, with full dry runs on an 8-element array and clean, beginner-friendly code.
Maximum Subarray in Java is a problem that looks tricky but hides a beautiful trick. You get an array of numbers, some positive and some negative. Your job is to find the run of numbers, sitting next to each other, that adds up to the biggest total.
The word that matters here is contiguous. The numbers must touch. You cannot cherry-pick the good ones and skip the bad ones in the middle. Once you start a run, the negatives inside it come along too.
We will build the answer in three steps, like always. Brute force tries every possible run with nested loops. A cleaner version keeps a running sum so it stops recomputing the same thing. The last one, Kadane’s algorithm, walks the array once and decides at each number whether to keep going or start fresh.
Every approach gets a full dry run on the same eight numbers. No step is skipped, so you can watch each variable change, line by line, and see exactly why the answer comes out to 6.
Let us nail down the rules before we touch any code.
For our array the best run is [4, -1, 2, 1], and it adds up to 6. Notice it even includes a negative, the -1. We keep it because dropping it would break the run, and the numbers around it are worth more than the -1 costs.
One more thing. The array can be all negative, like [-3, -1, -2]. Then the answer is just the single largest number, here -1. A good solution must handle that without returning zero.
A running sum is just a total you carry as you walk the array. You add each new number to it as you pass. This saves you from adding the same stretch of numbers over and over.
This is the heart of Kadane’s algorithm. At each number you face one choice. Do you glue this number onto the run you already have, or do you throw that run away and start a brand new run right here?
That single decision, made once per number, is why Kadane’s runs in one clean pass.
| 💡 Interview Insight A classic opener is “why does a negative running sum mean restart?” Say it plainly: a negative prefix can only shrink whatever comes next, so any future run is better off starting after it. |
Try every possible run. For each starting point, stretch the run out to every ending point, add the numbers up, and keep the biggest total you ever see. It is slow, but it makes the goal crystal clear.
best = -infinity
for start from 0 to n-1:
for end from start to n-1:
sum = 0
for k from start to end: // add the run from scratch
sum = sum + nums[k]
if sum > best:
best = sum
return bestpublic class MaxSubarrayBrute {
public static int maxSubArray(int[] nums) {
int best = Integer.MIN_VALUE;
for (int start = 0; start < nums.length; start++) {
for (int end = start; end < nums.length; end++) {
int sum = 0;
for (int k = start; k <= end; k++) {
sum += nums[k];
}
if (sum > best) {
best = sum;
}
}
}
return best;
}
public static void main(String[] args) {
int[] nums = { -2, 1, -3, 4, -1, 2, 1, -5 };
System.out.println(maxSubArray(nums)); // 6
}
}Array: [-2, 1, -3, 4, -1, 2, 1, -5]. We trace every start value and every end value, so all 36 runs are shown. Watch the sum column and the best column together.
Legend: start is the left end of the run, end is the right end, sum is the total of nums[start..end], and best is the largest sum seen so far.
| end (val) | run | sum | best after | record? |
|---|---|---|---|---|
| 0 (-2) | [-2] | -2 | -2 | yes |
| 1 (1) | [-2, 1] | -1 | -1 | yes |
| 2 (-3) | [-2, 1, -3] | -4 | -1 | no |
| 3 (4) | [-2, 1, -3, 4] | 0 | 0 | yes |
| 4 (-1) | [-2, 1, -3, 4, -1] | -1 | 0 | no |
| 5 (2) | [-2, 1, -3, 4, -1, 2] | 1 | 1 | yes |
| 6 (1) | [-2, 1, -3, 4, -1, 2, 1] | 2 | 2 | yes |
| 7 (-5) | […, 1, -5] | -3 | 2 | no |
| end (val) | run | sum | best after | record? |
|---|---|---|---|---|
| 1 (1) | [1] | 1 | 2 | no |
| 2 (-3) | [1, -3] | -2 | 2 | no |
| 3 (4) | [1, -3, 4] | 2 | 2 | no |
| 4 (-1) | [1, -3, 4, -1] | 1 | 2 | no |
| 5 (2) | [1, -3, 4, -1, 2] | 3 | 3 | yes |
| 6 (1) | [1, -3, 4, -1, 2, 1] | 4 | 4 | yes |
| 7 (-5) | [1, -3, 4, -1, 2, 1, -5] | -1 | 4 | no |
| end (val) | run | sum | best after | record? |
|---|---|---|---|---|
| 2 (-3) | [-3] | -3 | 4 | no |
| 3 (4) | [-3, 4] | 1 | 4 | no |
| 4 (-1) | [-3, 4, -1] | 0 | 4 | no |
| 5 (2) | [-3, 4, -1, 2] | 2 | 4 | no |
| 6 (1) | [-3, 4, -1, 2, 1] | 3 | 4 | no |
| 7 (-5) | [-3, 4, -1, 2, 1, -5] | -2 | 4 | no |
| end (val) | run | sum | best after | record? |
|---|---|---|---|---|
| 3 (4) | [4] | 4 | 4 | no |
| 4 (-1) | [4, -1] | 3 | 4 | no |
| 5 (2) | [4, -1, 2] | 5 | 5 | yes |
| 6 (1) | [4, -1, 2, 1] | 6 | 6 | yes |
| 7 (-5) | [4, -1, 2, 1, -5] | 1 | 6 | no |
| start | end (val) | run | sum | best after |
|---|---|---|---|---|
| 4 | 4 (-1) | [-1] | -1 | 6 |
| 4 | 5 (2) | [-1, 2] | 1 | 6 |
| 4 | 6 (1) | [-1, 2, 1] | 2 | 6 |
| 4 | 7 (-5) | [-1, 2, 1, -5] | -3 | 6 |
| 5 | 5 (2) | [2] | 2 | 6 |
| 5 | 6 (1) | [2, 1] | 3 | 6 |
| 5 | 7 (-5) | [2, 1, -5] | -2 | 6 |
| 6 | 6 (1) | [1] | 1 | 6 |
| 6 | 7 (-5) | [1, -5] | -4 | 6 |
| 7 | 7 (-5) | [-5] | -5 | 6 |
Let us walk the trace start by start and watch best climb.
Pass start = 0.
Here the run always begins at -2, so it carries a handicap from the very first number.
Next, start = 1.
Now we drop the leading -2 and start fresh at 1. This helps a lot.
Then start = 2.
This run opens on -3, a deep hole. It never digs out.
Now start = 3.
Here is the winning pass. It starts right on the 4.
Passes start = 4 to 7.
These late runs are short and start too far right to beat 6.
So 36 runs were checked in all, yet the winner was found back in the start = 3 pass. The brute force has no way to skip ahead, so it grinds through them anyway.
For eight numbers this is fine. For a few thousand it crawls, which is why we tighten it next.
The brute force wastes time re-adding the same numbers. We can fix that. Keep one run open and extend its sum by a single addition as the end moves right. That drops the third loop entirely.
best = -infinity
for start from 0 to n-1:
sum = 0
for end from start to n-1:
sum = sum + nums[end] // extend, no rebuild
if sum > best:
best = sum
return bestpublic class MaxSubarrayRunningSum {
public static int maxSubArray(int[] nums) {
int best = Integer.MIN_VALUE;
for (int start = 0; start < nums.length; start++) {
int sum = 0;
for (int end = start; end < nums.length; end++) {
sum += nums[end];
if (sum > best) {
best = sum;
}
}
}
return best;
}
public static void main(String[] args) {
int[] nums = { -2, 1, -3, 4, -1, 2, 1, -5 };
System.out.println(maxSubArray(nums)); // 6
}
}Array: [-2, 1, -3, 4, -1, 2, 1, -5]. This time each row is one extension of the run, so sum grows by exactly one number per step. We trace every start pass fully.
Legend: sum before is the running total from the previous step, +nums[end] is the number just added, sum after is the new total, and best is the record so far.
| end (val) | sum before | + nums[end] | sum after | best after |
|---|---|---|---|---|
| 0 (-2) | 0 | -2 | -2 | -2 |
| 1 (1) | -2 | +1 | -1 | -1 |
| 2 (-3) | -1 | -3 | -4 | -1 |
| 3 (4) | -4 | +4 | 0 | 0 |
| 4 (-1) | 0 | -1 | -1 | 0 |
| 5 (2) | -1 | +2 | 1 | 1 |
| 6 (1) | 1 | +1 | 2 | 2 |
| 7 (-5) | 2 | -5 | -3 | 2 |
| end (val) | sum before | + nums[end] | sum after | best after |
|---|---|---|---|---|
| 1 (1) | 0 | +1 | 1 | 2 |
| 2 (-3) | 1 | -3 | -2 | 2 |
| 3 (4) | -2 | +4 | 2 | 2 |
| 4 (-1) | 2 | -1 | 1 | 2 |
| 5 (2) | 1 | +2 | 3 | 3 |
| 6 (1) | 3 | +1 | 4 | 4 |
| 7 (-5) | 4 | -5 | -1 | 4 |
| end (val) | sum before | + nums[end] | sum after | best after |
|---|---|---|---|---|
| 2 (-3) | 0 | -3 | -3 | 4 |
| 3 (4) | -3 | +4 | 1 | 4 |
| 4 (-1) | 1 | -1 | 0 | 4 |
| 5 (2) | 0 | +2 | 2 | 4 |
| 6 (1) | 2 | +1 | 3 | 4 |
| 7 (-5) | 3 | -5 | -2 | 4 |
| end (val) | sum before | + nums[end] | sum after | best after |
|---|---|---|---|---|
| 3 (4) | 0 | +4 | 4 | 4 |
| 4 (-1) | 4 | -1 | 3 | 4 |
| 5 (2) | 3 | +2 | 5 | 5 |
| 6 (1) | 5 | +1 | 6 | 6 |
| 7 (-5) | 6 | -5 | 1 | 6 |
| start | end (val) | + nums[end] | sum after | best after |
|---|---|---|---|---|
| 4 | 4 (-1) | -1 | -1 | 6 |
| 4 | 5 (2) | +2 | 1 | 6 |
| 4 | 6 (1) | +1 | 2 | 6 |
| 4 | 7 (-5) | -5 | -3 | 6 |
| 5 | 5 (2) | +2 | 2 | 6 |
| 5 | 6 (1) | +1 | 3 | 6 |
| 5 | 7 (-5) | -5 | -2 | 6 |
| 6 | 6 (1) | +1 | 1 | 6 |
| 6 | 7 (-5) | -5 | -4 | 6 |
| 7 | 7 (-5) | -5 | -5 | 6 |
The answer matches the brute force exactly. What changed is speed, not results. Watch how sum only ever moves by one addition.
Pass start = 0.
Next, start = 1.
Then start = 2.
Now start = 3.
Passes start = 4 to 7.
Every value here matches the brute force column for column. We simply stopped rebuilding the sum, so each row is one cheap addition instead of a fresh loop.
| 💡 Interview Insight If asked “what did the running sum actually save?”, point at the inner loop. Brute force re-added the whole run each time; this version adds one number and reuses the rest. |
A solid jump over brute force. Still, we visit the same pairs many times, and interviewers usually want the single-pass version next.
Kadane’s algorithm walks the array once. At each number it makes the extend-or-restart choice. It keeps a running sum for the current run and a separate best for the largest run ever seen.
The trick is this. If the running sum ever goes negative, it can only hurt what comes after, so we throw it away and start again at the current number.
best = nums[0]
cur = nums[0]
for i from 1 to n-1:
// extend the old run, or restart at nums[i]
cur = max(nums[i], cur + nums[i])
best = max(best, cur)
return bestpublic class MaxSubarrayKadane {
public static int maxSubArray(int[] nums) {
int best = nums[0];
int cur = nums[0];
for (int i = 1; i < nums.length; i++) {
cur = Math.max(nums[i], cur + nums[i]);
best = Math.max(best, cur);
}
return best;
}
public static void main(String[] args) {
int[] nums = { -2, 1, -3, 4, -1, 2, 1, -5 };
System.out.println(maxSubArray(nums)); // 6
}
}Array: [-2, 1, -3, 4, -1, 2, 1, -5]. We seed both cur and best with nums[0] = -2, then trace every index from 1 to 7. Each row shows the extend value, the restart value, which one wins, and the new best.
Legend: extend = cur + nums[i] (glue onto the old run), restart = nums[i] (start fresh here), cur = the winner of those two, best = largest cur so far.
| i (index) | nums[i] | extend | restart | cur (winner) | best | record? |
|---|---|---|---|---|---|---|
| init | -2 | — | — | -2 | -2 | seed |
| 1 | 1 | -1 | 1 | 1 (restart) | 1 | yes |
| 2 | -3 | -2 | -3 | -2 (extend) | 1 | no |
| 3 | 4 | 2 | 4 | 4 (restart) | 4 | yes |
| 4 | -1 | 3 | -1 | 3 (extend) | 4 | no |
| 5 | 2 | 5 | 2 | 5 (extend) | 5 | yes |
| 6 | 1 | 6 | 1 | 6 (extend) | 6 | yes |
| 7 | -5 | 1 | -5 | 1 (extend) | 6 | no |
This is the whole algorithm in eight rows. Let us take each index and see why the winner was chosen.
Seed: cur = -2, best = -2.
We start the run on the only number available, -2. There is nothing to compare yet, so both cur and best just hold -2.
At index 1, nums[i] = 1.
Now index 2, nums[i] = -3.
Then index 3, nums[i] = 4.
At index 4, nums[i] = -1.
Now index 5, nums[i] = 2.
Then index 6, nums[i] = 1.
Finally index 7, nums[i] = -5.
One pass, one decision per number, and the answer falls out as 6. Notice best only ever moves up and never resets. It quietly records the high-water mark while cur does the day-to-day work.
| 💡 Interview Insight A common follow-up is “why seed with nums[0] instead of 0?”. For an all-negative array, seeding with 0 would wrongly return 0. Starting on nums[0] returns the largest single number, which is correct. |
| Approach | How it searches | Work per number | Speed feel |
|---|---|---|---|
| Brute force | Every run, sum rebuilt | A whole inner loop | Slow on big arrays |
| Running sum | Every run, sum extended | One addition | Much faster |
| Kadane’s | One pass, extend or restart | One comparison | Fast and lean |
Tables are exact, but a sketch often clicks faster. Here is the same Kadane trace drawn by hand.

All three return the same 6. They just pay different prices to get there.
| Approach | Time | Space | Note |
|---|---|---|---|
| Brute force | O(n³) | O(1) | Simple, but rebuilds every run |
| Running sum | O(n²) | O(1) | Faster, reuses the sum |
| Kadane’s | O(n) | O(1) | One pass, the expected answer |
The gap here is time, not memory. All three use O(1) extra space, yet Kadane’s finishes in a single walk while the others revisit the same stretches again and again.
In an interview, open with brute force, point out the wasted inner loop, tighten it to the running sum, then land on Kadane’s and explain the extend-or-restart rule. That climb is the story interviewers want to hear.
| 💡 Interview Insight If pushed on correctness, explain the invariant: cur always holds the best run ending at the current index. Since every subarray ends somewhere, and best watches every cur, no subarray can slip past unseen. |
A few small traps catch beginners on Maximum Subarray. Keep them close.
Run the all-negative case and the single-element case through your code before you call it done. They catch more bugs than any friendly input will.
A: Kadane’s algorithm walks the array once, keeping a running sum for the run ending at the current number. At each step it either extends that run or restarts at the current number, whichever is larger, and tracks the best sum ever seen. It runs in O(n) time and O(1) space.
A: Seeding with 0 breaks arrays that are all negative, since it would wrongly return 0. Starting on nums[0] makes the algorithm return the largest single number, which is the correct answer for an all-negative array.
A: Brute force is O(n³) because it rebuilds each run’s sum. A running-sum version drops that to O(n²). Kadane’s algorithm solves it in O(n) with a single pass, all using O(1) extra space.
A: Yes. The numbers must sit next to each other, so you cannot skip the negatives in the middle. That is why a run like [4, -1, 2, 1] keeps the -1 — dropping it would break the run.
Maximum Subarray in Java looks scary with its mix of positives and negatives. But once you spot the extend-or-restart idea, the fear melts away.
Our eight-number trace showed the payoff clearly. Brute force ground through all 36 runs. The running sum trimmed the wasted adds. Kadane’s made one clean pass and still landed on 6.
So take the pattern, not just the answer. Carry a running total. Ask at each step whether the past is helping or hurting. Then drop it the moment it turns negative.
That habit turns a slow O(n³) grind into a single O(n) walk. It will do the same for many array problems waiting further down the list.
javahandson.com | DSA Series | Arrays & Strings