Max Consecutive Ones: The Running-Count Pattern in One Pass
Solve Max Consecutive Ones in one linear pass with a running counter that resets on each zero — intuition, JavaScript code, a worked walkthrough, and complexity.
This article has an interactive companion
Don't just read it — step through it interactively, one state change at a time.
Max Consecutive Ones is one of those problems that looks too easy to teach anything — until you notice it's really a stripped-down version of every "longest run" question you'll ever meet. Strip away the noise and it's just this: walk an array once, keep a tally of how long your current good streak is, and remember the best streak you ever saw.
That two-variable move — a running count plus a best-so-far — is the whole pattern. It powers longest-substring problems, max-profit windows, and streak trackers alike. Get the reset logic right here and the harder versions stop feeling magical.
The problem
Given a binary array nums (every element is 0 or 1), return the maximum number of consecutive 1s in the array. "Consecutive" means uninterrupted — a single 0 anywhere resets the run.
Input: nums = [1, 1, 0, 1, 1, 1]
Output: 3 // the run at indices 3..5 is three 1s longThere are two streaks of 1s here: a run of length 2 at the start, and a run of length 3 at the end. The zero at index 2 severs them. The answer is the longer of the two, 3.
Two facts shape the solution:
- A
0doesn't shrink the answer — it just ends the current run. The best you've already recorded stays safe. - You never need to look backward. Everything you need to know lives in two numbers you carry as you scan.
The brute force baseline
The naive instinct is to treat every index as a possible streak start and count forward until you hit a zero:
function findMaxConsecutiveOnes(nums) {
let best = 0;
for (let i = 0; i < nums.length; i++) {
let count = 0;
let j = i;
while (j < nums.length && nums[j] === 1) {
count++;
j++;
}
best = Math.max(best, count);
}
return best;
}This is correct, but wasteful. When you start a fresh count at index i, the inner loop re-walks a stretch you may have already walked from an earlier starting point. On an array of all 1s — [1,1,1,1,1] — the outer loop kicks off a full forward scan from every position, giving roughly n + (n-1) + ... + 1 steps, which is O(n²). For a 10⁵-element array that's billions of comparisons to answer a question a single pass could handle.
The key insight: one running counter, one best
The redundancy in the brute force comes from restarting the count at every index. But you don't have to restart — a run only ever breaks in one place: a zero. So carry a single count across the whole scan and update it in place:
- See a
1? The current streak grows:count += 1. - See a
0? The streak is dead:count = 0.
After each step, compare count against best and keep the larger. That's the running-count pattern: instead of recomputing runs from scratch, you maintain the current run incrementally and snapshot the maximum as you go. One pass, no rescanning.
The reset is the load-bearing line. Because a zero drops count straight back to 0, the next 1 starts a brand-new streak from length 1 — exactly matching the "consecutive means uninterrupted" rule.
The optimal solution
This is the exact algorithm the visualizer steps through — a single for loop with a count and a best:
var findMaxConsecutiveOnes = function (nums) {
let count = 0;
let best = 0;
for (let i = 0; i < nums.length; i++) {
if (nums[i] === 1) {
count += 1;
} else {
count = 0;
}
best = Math.max(best, count);
}
return best;
};Two things make this clean. First, best = Math.max(best, count) runs on every iteration — including right after a reset — so there's no special case to remember. When count drops to 0, the max simply keeps the old best untouched. Second, best starts at 0, which is the correct answer for an all-zeros array or an empty one: the loop either never bumps count or never runs at all.
You could micro-optimize by only updating best inside the if branch (a zero can never produce a new maximum), but doing it unconditionally is simpler to read and costs nothing asymptotically.
Walkthrough
Tracing nums = [1, 1, 0, 1, 1, 1] — watch count climb, snap to zero at index 2, then climb again to overtake the old best:
| i | nums[i] | Branch | count | best |
|---|---|---|---|---|
| 0 | 1 | count += 1 | 1 | 1 |
| 1 | 1 | count += 1 | 2 | 2 |
| 2 | 0 | count = 0 | 0 | 2 |
| 3 | 1 | count += 1 | 1 | 2 |
| 4 | 1 | count += 1 | 2 | 2 |
| 5 | 1 | count += 1 | 3 | 3 |
The zero at index 2 is the pivotal row: count collapses to 0, but best holds at 2 because the Math.max keeps the larger value. The second streak then rebuilds from 1 up to 3, and at index 5 it finally beats the old record. The loop ends and returns best = 3.
Notice that best only actually changed three times (at i=0, i=1, i=5) even though we recomputed it six times. Those redundant recomputations are free — they're a single comparison each — and they buy you code with zero branching around the maximum.
Complexity
| Approach | Time | Space | Why |
|---|---|---|---|
| Brute force (restart per index) | O(n²) | O(1) | each index may re-scan the run ahead of it |
| Running count | O(n) | O(1) | one pass, one comparison and one update per element |
The optimal version touches each element exactly once and carries only two integers, count and best, regardless of input size. Linear time, constant space — you can't do asymptotically better, since you must at least look at every element once to know the longest run.
Common mistakes
- Forgetting to reset on zero. If you only ever increment
count, you're counting total ones, not consecutive ones. Thecount = 0on the else branch is what enforces "uninterrupted." - Updating
bestonly when you see a zero. A common bug is snapshotting the run's length only at the moment it breaks — which misses the final streak if the array ends on a 1. Updatingbestevery iteration (or checking after the loop too) avoids this. In[0, 1, 1, 1]the record run never hits a zero after it. - Initializing
bestto-Infinityor1. The answer for[0,0,0,0]is0, and an empty array should also return0. Startbestat0and both fall out for free. - Resetting
bestinstead ofcount. The whole point is thatbestnever goes down. Onlycountresets;bestis monotonic.
Where this pattern shows up next
The running-count idea — scan once, maintain a live value, snapshot the extreme — is the seed for a whole family of single-pass array problems:
- Best Time to Buy and Sell Stock — same shape, but you track a running minimum price and the best profit instead of a streak.
- Remove Element — a one-pass scan with a write pointer, filtering in place.
- Remove Duplicates from Sorted Array — carrying a "last kept value" across a single sweep.
- Reverse String — the two-pointer cousin, walking one pass from both ends.
You can also step through Max Consecutive Ones interactively to watch the counter climb, reset on each zero, and the best-so-far lock in — one element at a time.
FAQ
What is the time complexity of Max Consecutive Ones?
The optimal solution is O(n) time and O(1) space. You scan the array once, and at each element you do a single comparison (is it a 1?) and update two integers. Nothing scales with the size of any run, so total work is proportional to the array length. The brute-force version that restarts a count at every index is O(n²) and unnecessary.
Why do you reset the counter to zero instead of subtracting?
Because a zero ends the current run entirely — there's nothing to subtract from. "Consecutive" means the ones must be adjacent with no interruption, so the moment a zero appears, the next streak has to begin fresh from length 1. Setting count = 0 on a zero, then incrementing on the next 1, produces exactly that behavior. The already-recorded best is never touched, so no history is lost.
How does this handle an array with no ones at all?
It returns 0, which is correct. Both count and best start at 0. For an input like [0, 0, 0, 0], every iteration hits the else branch, count stays at 0, and Math.max(0, 0) keeps best at 0. The same initialization handles an empty array, since the loop body never runs and the function returns the initial best of 0.
Do I need to check the streak again after the loop ends?
Not in this version, because best = Math.max(best, count) runs on every single iteration — including the last one. That means if the array ends mid-streak, like [1, 1, 1], the final increment is captured before the loop exits. If you instead only update best at the moment a zero breaks a run, you would need a post-loop check to catch a trailing streak that never gets interrupted.
Make it stick: run this one yourself
Don't just read it — step through it interactively, one state change at a time.