GUIDE · Patterns

Arrays

single-pass and in-place array techniques that avoid O(n^2).

lesson13 sectionsruns in browser38 asserts178 lines

At a glance

FIRST VISIT
From Session 9.2, read the BIG-O row and section 1, prefix sums: one O(n) pass buys O(1) range sums, the trade you measured today. Stop there; sections 2 to 13 are the catalogue 9.3 and 9.4 draw from, one per tell.
WHAT
single-pass and in-place array techniques that avoid O(n^2).
TELL
"subarray", "in place", "O(1) extra space", "numbers 1..n", "majority", "range sum queries", "rotate".
SAY
prefix sumKadanetriple reverseDutch national flag (3-way partition)cyclic sortFloyd's tortoise-and-hareBoyer-Moore votingtwo pointers
BIG-O
almost everything here is O(n) time, O(1) or O(n) space.

The idea#

Picture a cash-register receipt. Each line is a number. Beside every line the register prints the running total from the top of the tape down to here. That column of running totals is a prefix sum. Once it is printed, the total of any stretch of lines is one subtraction: the running total just after the stretch ends, minus the running total just before the stretch starts. You paid for a linear walk once. Every range question after that is constant time.

Kadane's algorithm is a second habit on the same tape. You walk the lines once, keeping the best streak that ends on the line you are touching. That streak is either this line alone, if gluing it onto the previous streak would make things worse, or the previous streak plus this line. You also keep a separate note of the best streak you have seen anywhere. You never look two lines back. You never restart from scratch for every possible start. One pass, a couple of numbers in your head.

The rest of the patterns are the same promise: one walk, and a constant amount of extra memory unless the answer itself needs a new array. A hash map for complements, three reversals to rotate, three zones for three colors, a cycle of indexes when the values are 1..n.

Why it matters#

A nested loop over every pair of indexes is quadratic, and a lot of array problems are disguised pair loops. You reach for these patterns when the problem says subarray, in place, constant extra space, numbers in 1..n, a majority, range-sum queries, or a rotation. The tell is the shape of the question. The tool is a single pass that leaves behind either a prefix, a candidate, or a rearranged tape.

Almost everything on this sheet is O(n) time, with O(1) or O(n) extra space.

A small example#

JS
function rangeSum(nums, left, right) {  const pre = [0];  for (const x of nums) pre.push(pre[pre.length - 1] + x);  return pre[right + 1] - pre[left];} const total = rangeSum([2, 4, 6, 8], 1, 2);

pre is the receipt: [0, 2, 6, 12, 20]. The stretch from index 1 through 2 is 12 - 2. total is 10, which is 4 + 6. Building the tape is linear. Reading one stretch is constant.

What the code below is doing#

Prefix sums are section 1: pre[i] is the sum of the half-open prefix nums[0..i), and sum(l..r) is pre[r + 1] - pre[l]. Kadane is section 2: cur = max(x, cur + x) and best = max(best, cur), the best subarray in linear time and constant space. Two-sum stores each value's complement as you scan, so the pair is a hash lookup. Rotate-right by k is three reversals: reverse the whole tape, reverse the first k, reverse the rest. No extra array.

Dutch national flag is three pointers, lo, mid, and hi, partitioning 0s, 1s, and 2s in one pass. Product of array except self runs a left product into the answer, then a right product back across it, constant extra space besides the answer. Merging two sorted arrays is two pointers taking the smaller head and then draining the rest. Cyclic sort is for values 1..n: while a value is not home, swap it toward index x - 1. Each value moves at most once.

Missing number, for 0..n with one hole, is the expected sum minus the actual sum, or the same idea with XOR. The duplicate in 1..n, read-only and constant space, is Floyd's tortoise and hare on the arrow i -> nums[i]. Majority, the value that appears more than n/2 times, is Boyer-Moore voting: a candidate and a count that cancels out on disagreement. Best time to buy and sell once tracks the minimum price so far and the best difference. Move zeroes keeps a write pointer for the non-zero values, then fills the tail with zeroes so the original order survives.

When you talk about it, say this: one pass over the tape, and name the thing you are carrying, a prefix, a streak, a candidate, or a write pointer.

Everything below is the demonstration. Same idea, written as code you can run.

JSalgorithms/arrays.js · setup
'use strict';const assert = require('node:assert/strict');
01

Prefix sums (range sum query)#

pre[i] = sum(nums[0..i)); sum(l..r) = pre[r+1]-pre[l]

O(n) build / O(1) query
JS
function buildPrefix(nums) {  const pre = new Array(nums.length + 1).fill(0);  for (let i = 0; i < nums.length; i++) pre[i + 1] = pre[i] + nums[i];  return pre;}const rangeSum = (pre, l, r) => pre[r + 1] - pre[l]; // inclusive l..rconst pre1 = buildPrefix([1, 2, 3, 4, 5]);
Verified by 3 asserts
JS
assert.equal(rangeSum(pre1, 1, 3), 9);assert.equal(rangeSum(pre1, 0, 4), 15);assert.deepEqual(buildPrefix([]), [0]);
02

Kadane's algorithm (max subarray)#

cur = max(x, cur + x); best = max(best, cur)

O(n) / O(1)
JS
function maxSubarray(nums) {  let cur = nums[0], best = nums[0];  for (let i = 1; i < nums.length; i++) {    cur = Math.max(nums[i], cur + nums[i]);    best = Math.max(best, cur);  }  return best;}
Verified by 3 asserts
JS
assert.equal(maxSubarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]), 6);assert.equal(maxSubarray([-3, -1, -2]), -1); // all negatives: pick the max singleassert.equal(maxSubarray([5]), 5);
03

Two-sum (hash map)#

store complement -> index while scanning

O(n) / O(n)
JS
function twoSum(nums, target) {  const seen = new Map(); // value -> index  for (let i = 0; i < nums.length; i++) {    const need = target - nums[i];    if (seen.has(need)) return [seen.get(need), i];    seen.set(nums[i], i);  }  return [];}
Verified by 3 asserts
JS
assert.deepEqual(twoSum([2, 7, 11, 15], 9), [0, 1]);assert.deepEqual(twoSum([3, 3], 6), [0, 1]);assert.deepEqual(twoSum([1, 2], 10), []);
04

Rotate array right by k in place (triple reverse)#

reverse all, reverse [0,k), reverse [k,n)

O(n) / O(1)
JS
function reverseRange(a, i, j) {  while (i < j) { [a[i], a[j]] = [a[j], a[i]]; i++; j--; }}function rotate(nums, k) {  const n = nums.length;  if (n === 0) return nums;  k = ((k % n) + n) % n;  reverseRange(nums, 0, n - 1);  reverseRange(nums, 0, k - 1);  reverseRange(nums, k, n - 1);  return nums;}
Verified by 3 asserts
JS
assert.deepEqual(rotate([1, 2, 3, 4, 5, 6, 7], 3), [5, 6, 7, 1, 2, 3, 4]);assert.deepEqual(rotate([1, 2], 3), [2, 1]); // k > nassert.deepEqual(rotate([], 2), []);
05

Dutch national flag (sort colors / 3-way partition)#

lo, mid, hi pointers; swap into zones

O(n) / O(1)
JS
function sortColors(nums) {  let lo = 0, mid = 0, hi = nums.length - 1;  while (mid <= hi) {    if (nums[mid] === 0) { [nums[lo], nums[mid]] = [nums[mid], nums[lo]]; lo++; mid++; }    else if (nums[mid] === 2) { [nums[mid], nums[hi]] = [nums[hi], nums[mid]]; hi--; } // do NOT advance mid    else mid++;  }  return nums;}
Verified by 3 asserts
JS
assert.deepEqual(sortColors([2, 0, 2, 1, 1, 0]), [0, 0, 1, 1, 2, 2]);assert.deepEqual(sortColors([2, 2, 2]), [2, 2, 2]);assert.deepEqual(sortColors([]), []);
06

Product of array except self#

left-running product, then right-running product multiplied in

O(n) / O(1) extra
JS
function productExceptSelf(nums) {  const n = nums.length, out = new Array(n).fill(1);  for (let i = 1; i < n; i++) out[i] = out[i - 1] * nums[i - 1];  let right = 1;  for (let i = n - 1; i >= 0; i--) { out[i] *= right; right *= nums[i]; }  return out.map((x) => x + 0); // +0 normalizes -0 (strict deepEqual distinguishes -0 from 0)}
Verified by 3 asserts
JS
assert.deepEqual(productExceptSelf([1, 2, 3, 4]), [24, 12, 8, 6]);assert.deepEqual(productExceptSelf([-1, 1, 0, -3, 3]), [0, 0, 9, 0, 0]);assert.deepEqual(productExceptSelf([5]), [1]);
07

Merge two sorted arrays#

two pointers, take smaller; drain remainder

O(n+m) / O(n+m)
JS
function mergeSorted(a, b) {  const out = [];  let i = 0, j = 0;  while (i < a.length && j < b.length) out.push(a[i] <= b[j] ? a[i++] : b[j++]);  while (i < a.length) out.push(a[i++]);  while (j < b.length) out.push(b[j++]);  return out;}
Verified by 3 asserts
JS
assert.deepEqual(mergeSorted([1, 3, 5], [2, 4, 6]), [1, 2, 3, 4, 5, 6]);assert.deepEqual(mergeSorted([], [1, 2]), [1, 2]);assert.deepEqual(mergeSorted([1, 1], [1]), [1, 1, 1]);
08

Cyclic sort#

values 1..n: put x at index x-1 by swapping until fixed point

O(n) / O(1)
JS
function cyclicSort(nums) {  let i = 0;  while (i < nums.length) {    const j = nums[i] - 1;    if (nums[i] !== nums[j]) [nums[i], nums[j]] = [nums[j], nums[i]];    else i++;  }  return nums;}
Verified by 2 asserts
JS
assert.deepEqual(cyclicSort([3, 1, 5, 4, 2]), [1, 2, 3, 4, 5]);assert.deepEqual(cyclicSort([1]), [1]);
09

Find missing number (0..n, one missing)#

expected sum minus actual (or XOR)

O(n) / O(1)
JS
const missingNumber = (nums) => (nums.length * (nums.length + 1)) / 2 - nums.reduce((s, x) => s + x, 0);
Verified by 3 asserts
JS
assert.equal(missingNumber([3, 0, 1]), 2);assert.equal(missingNumber([0]), 1);assert.equal(missingNumber([]), 0);
10

Find the duplicate (1..n, one repeated, read-only, O(1) space)#

Floyd's cycle detection on i -> nums[i]

O(n) / O(1)
JS
function findDuplicate(nums) {  let slow = nums[0], fast = nums[0];  do { slow = nums[slow]; fast = nums[nums[fast]]; } while (slow !== fast);  slow = nums[0];  while (slow !== fast) { slow = nums[slow]; fast = nums[fast]; } // meet at cycle entrance = duplicate  return slow;}
Verified by 3 asserts
JS
assert.equal(findDuplicate([1, 3, 4, 2, 2]), 2);assert.equal(findDuplicate([3, 1, 3, 4, 2]), 3);assert.equal(findDuplicate([1, 1]), 1);
11

Majority element (> n/2)#

Boyer-Moore voting: candidate + count; cancel out

O(n) / O(1)
JS
function majorityElement(nums) {  let cand = null, count = 0;  for (const x of nums) {    if (count === 0) cand = x;    count += x === cand ? 1 : -1;  }  return cand; // if majority not guaranteed, do a second pass to verify count > n/2}
Verified by 3 asserts
JS
assert.equal(majorityElement([3, 2, 3]), 3);assert.equal(majorityElement([2, 2, 1, 1, 1, 2, 2]), 2);assert.equal(majorityElement([7]), 7);
12

Best time to buy and sell stock (one transaction)#

track min so far, best = max(price - min)

O(n) / O(1)
JS
function maxProfit(prices) {  let min = Infinity, best = 0;  for (const p of prices) { min = Math.min(min, p); best = Math.max(best, p - min); }  return best;}
Verified by 3 asserts
JS
assert.equal(maxProfit([7, 1, 5, 3, 6, 4]), 5);assert.equal(maxProfit([7, 6, 4, 3, 1]), 0); // decreasing: no profitassert.equal(maxProfit([]), 0);
13

Move zeroes (keep order, in place)#

write pointer for non-zeros, then fill zeros

O(n) / O(1)
JS
function moveZeroes(nums) {  let w = 0;  for (const x of nums) if (x !== 0) nums[w++] = x;  while (w < nums.length) nums[w++] = 0;  return nums;}
Verified by 3 asserts
JS
assert.deepEqual(moveZeroes([0, 1, 0, 3, 12]), [1, 3, 12, 0, 0]);assert.deepEqual(moveZeroes([0]), [0]);assert.deepEqual(moveZeroes([1, 2]), [1, 2]);

See also