Pattern 09 · Arrays & Strings

Array Techniques

Seven small, sharp tools — prefix sums, Kadane's, cyclic sort, Dutch flag, Boyer‑Moore, difference arrays, matrix walks. Each turns a quadratic brute force into a linear pass with O(1) tricks, usually by using the array's own indices as free storage.

time O(n)space O(1) trickstools 7 techniquespractice 15 problems
01 the toolkit

Seven tools, one lookup table

TechniqueUse caseTime / Space
Prefix SumRange sum queries, subarray sumsO(n) / O(n)
Difference ArrayEfficient range updatesO(n) / O(n)
Kadane'sMaximum subarrayO(n) / O(1)
Cyclic SortArrays with values in [1, n]O(n) / O(1)
Dutch National FlagThree-way partitionO(n) / O(1)
Boyer-Moore VoteMajority elementO(n) / O(1)
Matrix OperationsRotate, spiral, set zeroesvaries

How to recognize which tool

  • "Subarray sum equals K" → prefix sum + hash map
  • "Range sum query" → prefix sum
  • "Product of array except self" → left/right products
  • "Maximum subarray" → Kadane's
  • Values are in range 1 to n → cyclic sort
  • "Rotate matrix 90 degrees" → transpose + reverse
  • "Spiral order" → boundary tracking
02 prefix sums

Precompute once, query in O(1)

Store cumulative sums so any range sum becomes a subtraction. The convention prefix[i] = sum of the first i elements (with prefix[0] = 0) makes the range formula clean.

Array: [2, 4, 1, 3, 5] Index: 0 1 2 3 4 Prefix: [0, 2, 6, 7, 10, 15] Index: 0 1 2 3 4 5 prefix[i] = sum of elements from index 0 to i-1 Range sum from index i to j (inclusive): sum(i, j) = prefix[j+1] - prefix[i] Example: sum(1, 3) = prefix[4] - prefix[1] = 10 - 2 = 8 Check: 4 + 1 + 3 = 8 (correct)
prefix_sum.pypython
class PrefixSum:
    def __init__(self, nums):
        self.prefix = [0]
        for num in nums:
            self.prefix.append(self.prefix[-1] + num)

    def range_sum(self, i, j):
        """Sum of nums[i..j] inclusive."""
        return self.prefix[j + 1] - self.prefix[i]

LC 560Subarray Sum Equals K

Key insight: if prefix[j] - prefix[i] == k, then sum(i..j-1) == k. So while scanning, count how many earlier prefixes equal curr_sum - k — a hash map makes that O(1).

nums = [1, 2, 3], k = 3 prefix sums: 0, 1, 3, 6 ^ At prefix=3: 3 - 3 = 0, and we've seen prefix=0 once --> found subarray [1,2] At prefix=6: 6 - 3 = 3, and we've seen prefix=3 once --> found subarray [3] Answer: 2
subarray_sum.pypython
def subarray_sum(nums, k):
    count = 0
    curr_sum = 0
    prefix_counts = {0: 1}   # Empty prefix has sum 0

    for num in nums:
        curr_sum += num
        # How many previous prefixes had sum = curr_sum - k?
        count += prefix_counts.get(curr_sum - k, 0)
        prefix_counts[curr_sum] = prefix_counts.get(curr_sum, 0) + 1

    return count

LC 238Product of Array Except Self

Same prefix idea, multiplicative: build left products and right products, multiply them. No division needed.

nums: [1, 2, 3, 4] left: [1, 1, 2, 6] left[i] = product of everything LEFT of i right: [24, 12, 4, 1] right[i] = product of everything RIGHT of i answer: [24, 12, 8, 6] answer[i] = left[i] * right[i]
product_except_self.pypython
def product_except_self(nums):
    n = len(nums)
    result = [1] * n

    # Left pass
    left = 1
    for i in range(n):
        result[i] = left
        left *= nums[i]

    # Right pass
    right = 1
    for i in range(n - 1, -1, -1):
        result[i] *= right
        right *= nums[i]

    return result

InverseDifference array — O(1) range updates

The prefix sum's mirror image: to add a value to a whole range, bump the start, debit one past the end, and reconstruct with a single prefix-sum pass at the end. Perfect when there are many range updates and one final read.

Want to add +3 to indices [1, 3]: diff array: [0, +3, 0, 0, -3, 0] ^start ^end+1 Prefix sum of diff gives actual values: [0, 3, 3, 3, 0, 0]
difference_array.pypython
def range_update(n, updates):
    """updates = list of (start, end, value)"""
    diff = [0] * (n + 1)

    for start, end, val in updates:
        diff[start] += val
        diff[end + 1] -= val

    # Reconstruct
    result = []
    curr = 0
    for i in range(n):
        curr += diff[i]
        result.append(curr)

    return result
03 kadane's algorithm

Extend or start fresh

Maximum-sum subarray in O(n). At each element there is exactly one decision: extend the running subarray, or restart at this element — whichever gives the bigger sum. A negative running sum can only hurt you, so drop it.

1 / 10walkthrough — kadane's on LC 53's classic input
-2i
1
-3
4
-1
2
1
-5
4
curr −2
best −2
Initialize with the first element: curr = best = −2. Never initialize with 0 — an all-negative array would break.
max_subarray.py — LC 53python
def max_subarray(nums):
    curr_sum = max_sum = nums[0]

    for num in nums[1:]:
        curr_sum = max(num, curr_sum + num)
        max_sum = max(max_sum, curr_sum)

    return max_sum

Variant — track the indices too

max_subarray_with_indices.pypython
def max_subarray_with_indices(nums):
    curr_sum = max_sum = nums[0]
    start = end = temp_start = 0

    for i in range(1, len(nums)):
        if nums[i] > curr_sum + nums[i]:
            curr_sum = nums[i]
            temp_start = i        # Restart here
        else:
            curr_sum += nums[i]

        if curr_sum > max_sum:
            max_sum = curr_sum
            start = temp_start
            end = i

    return max_sum, start, end
04 value as index

Cyclic sort — the array is its own hash map

When values are in range [1, n] (or [0, n−1]), each value has a "home" index: value − 1. Swap every element to its home and anomalies — duplicates, missing numbers — reveal themselves. O(n) time, zero extra space.

Place each value at index value-1: nums = [3, 1, 5, 4, 2] i=0: nums[0]=3, should be at index 2 --> swap(0,2) --> [5, 1, 3, 4, 2] i=0: nums[0]=5, should be at index 4 --> swap(0,4) --> [2, 1, 3, 4, 5] i=0: nums[0]=2, should be at index 1 --> swap(0,1) --> [1, 2, 3, 4, 5] i=0: nums[0]=1, correct position --> move on i=1: nums[1]=2, correct --> move on ... Result: [1, 2, 3, 4, 5] -- every value at its home index
cyclic_sort.pypython
def cyclic_sort(nums):
    i = 0
    while i < len(nums):
        correct = nums[i] - 1          # Where this value belongs
        if nums[i] != nums[correct]:
            nums[i], nums[correct] = nums[correct], nums[i]
        else:
            i += 1
    return nums
Why this is O(n), not O(n²)
Each swap puts at least one element in its final home forever, so there are at most n swaps total — the i that refuses to advance is amortized away.

LC 268Find Missing Number

missing_number.pypython
def missing_number(nums):
    """Values in [0, n], one missing. Use math: expected - actual."""
    n = len(nums)
    return n * (n + 1) // 2 - sum(nums)

LC 442Find All Duplicates — sign as a visited bit

Same family: use the sign at a value's home index as a free "seen it" marker.

find_duplicates.pypython
def find_duplicates(nums):
    """Values in [1, n], some appear twice. Mark visited by negating."""
    result = []
    for num in nums:
        idx = abs(num) - 1
        if nums[idx] < 0:
            result.append(abs(num))   # Seen before
        else:
            nums[idx] = -nums[idx]    # Mark visited
    return result
05 one-pass partitions

Dutch flag & Boyer-Moore

LC 75Dutch National Flag — three-way partition

Sort an array of 0s, 1s, 2s in one pass with three pointers. Invariant: everything left of lo is 0, everything right of hi is 2, and mid scans the unknown middle.

Sort Colors (LC 75): [2, 0, 2, 1, 1, 0] lo=0, mid=0, hi=5 mid=0: nums[0]=2, swap(mid,hi), hi-- --> [0, 0, 2, 1, 1, 2] lo=0 mid=0 hi=4 mid=0: nums[0]=0, swap(lo,mid), lo++, mid++ --> [0, 0, 2, 1, 1, 2] lo=1 mid=1 hi=4 mid=1: nums[1]=0, swap, lo++, mid++ --> [0, 0, 2, 1, 1, 2] lo=2 mid=2 hi=4 mid=2: nums[2]=2, swap(mid,hi), hi-- --> [0, 0, 1, 1, 2, 2] lo=2 mid=2 hi=3 mid=2: nums[2]=1, mid++ --> lo=2 mid=3 hi=3 mid=3: nums[3]=1, mid++ --> lo=2 mid=4 hi=3 mid > hi --> done: [0, 0, 1, 1, 2, 2]
sort_colors.pypython
def sort_colors(nums):
    lo, mid, hi = 0, 0, len(nums) - 1

    while mid <= hi:
        if nums[mid] == 0:
            nums[lo], nums[mid] = nums[mid], nums[lo]
            lo += 1
            mid += 1
        elif nums[mid] == 1:
            mid += 1
        else:  # nums[mid] == 2
            nums[mid], nums[hi] = nums[hi], nums[mid]
            hi -= 1
            # Don't advance mid -- swapped element needs checking

LC 169Boyer-Moore voting — majority element

The element that appears more than n/2 times survives a pairwise cancellation war. Keep a candidate and a count: matches vote +1, mismatches vote −1, and at count 0 a new candidate takes over. O(n) time, O(1) space.

nums = [2, 2, 1, 1, 1, 2, 2] candidate=2, count=1 candidate=2, count=2 count=1 (1 cancels one 2) count=0 (1 cancels) candidate=1, count=1 count=0 (2 cancels) candidate=2, count=1 Answer: 2
majority_element.pypython
def majority_element(nums):
    candidate = count = 0

    for num in nums:
        if count == 0:
            candidate = num
        count += 1 if num == candidate else -1

    return candidate
06 matrix operations

Rotate, spiral, zero out

LC 48Rotate Image 90° clockwise

Two mechanical steps: transpose, then reverse each row. No cell-by-cell index juggling required.

Original: Transpose: Reverse rows: 1 2 3 1 4 7 7 4 1 4 5 6 --> 2 5 8 --> 8 5 2 7 8 9 3 6 9 9 6 3
rotate_image.pypython
def rotate(matrix):
    n = len(matrix)

    # Transpose
    for i in range(n):
        for j in range(i + 1, n):
            matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

    # Reverse each row
    for row in matrix:
        row.reverse()

LC 54Spiral Matrix — four shrinking boundaries

Track top, bottom, left, right. Walk one edge, shrink that boundary, repeat until the boundaries cross.

Traversal order: --> --> --> --> ^ | | | | v <-- <-- <-- <-- Boundaries shrink inward after each pass.
spiral_order.pypython
def spiral_order(matrix):
    result = []
    top, bottom = 0, len(matrix) - 1
    left, right = 0, len(matrix[0]) - 1

    while top <= bottom and left <= right:
        # Right
        for col in range(left, right + 1):
            result.append(matrix[top][col])
        top += 1

        # Down
        for row in range(top, bottom + 1):
            result.append(matrix[row][right])
        right -= 1

        # Left
        if top <= bottom:
            for col in range(right, left - 1, -1):
                result.append(matrix[bottom][col])
            bottom -= 1

        # Up
        if left <= right:
            for row in range(bottom, top - 1, -1):
                result.append(matrix[row][left])
            left += 1

    return result

LC 73Set Matrix Zeroes — first row/col as scratch space

Instead of an O(m + n) marker set, reuse the matrix's own first row and first column as the markers — after first recording whether they themselves contained a zero.

set_zeroes.pypython
def set_zeroes(matrix):
    m, n = len(matrix), len(matrix[0])
    first_row_zero = any(matrix[0][j] == 0 for j in range(n))
    first_col_zero = any(matrix[i][0] == 0 for i in range(m))

    # Mark zeroes in first row/col
    for i in range(1, m):
        for j in range(1, n):
            if matrix[i][j] == 0:
                matrix[i][0] = 0
                matrix[0][j] = 0

    # Zero out cells based on markers
    for i in range(1, m):
        for j in range(1, n):
            if matrix[i][0] == 0 or matrix[0][j] == 0:
                matrix[i][j] = 0

    # Handle first row and column
    if first_row_zero:
        for j in range(n):
            matrix[0][j] = 0
    if first_col_zero:
        for i in range(m):
            matrix[i][0] = 0
07 next permutation

The four-step index dance

LC 31. Find the next lexicographic arrangement in place: locate the rightmost ascent, swap in the smallest larger element from the suffix, then reverse the suffix (which is descending, so reversing sorts it ascending — the smallest possible tail).

Algorithm: 1. Find largest i such that nums[i] < nums[i+1] (rightmost ascent) 2. Find largest j > i such that nums[j] > nums[i] 3. Swap nums[i] and nums[j] 4. Reverse nums[i+1:] Example: [1, 3, 5, 4, 2] Step 1: i=1 (nums[1]=3 < nums[2]=5) Step 2: j=3 (nums[3]=4 > nums[1]=3) Step 3: swap --> [1, 4, 5, 3, 2] Step 4: reverse [5,3,2] --> [1, 4, 2, 3, 5]
next_permutation.pypython
def next_permutation(nums):
    n = len(nums)

    # Step 1: find rightmost ascent
    i = n - 2
    while i >= 0 and nums[i] >= nums[i + 1]:
        i -= 1

    if i >= 0:
        # Step 2: find rightmost element > nums[i]
        j = n - 1
        while nums[j] <= nums[i]:
            j -= 1
        # Step 3: swap
        nums[i], nums[j] = nums[j], nums[i]

    # Step 4: reverse suffix
    left, right = i + 1, n - 1
    while left < right:
        nums[left], nums[right] = nums[right], nums[left]
        left += 1
        right -= 1
08 complexity

Complexity reference

Problem / TechniqueTimeSpace
Prefix Sum (build)O(n)O(n)
Prefix Sum (query)O(1)—
Subarray Sum Equals KO(n)O(n)
Product Except SelfO(n)O(1)*
Kadane's AlgorithmO(n)O(1)
Cyclic SortO(n)O(1)
Dutch National FlagO(n)O(1)
Boyer-Moore VotingO(n)O(1)
Rotate MatrixO(n²)O(1)
Spiral MatrixO(m·n)O(1)*
Set Matrix ZeroesO(m·n)O(1)
Next PermutationO(n)O(1)

* excluding the output array.

09 common mistakes

Where arrays bite back

Mistake 1 — prefix sum off-by-one
The range formula is prefix[j+1] - prefix[i], not prefix[j] - prefix[i]. The leading 0 in the prefix array exists precisely to make this work.
Mistake 2 — forgetting the initial {0: 1}
In the subarray-sum hash map, the empty prefix has sum 0. Omit it and every subarray starting at index 0 goes uncounted.
Mistake 3 — Kadane's with all negatives
Initialize with nums[0], not 0. Otherwise [-3, -1, -2] returns 0 for a subarray that doesn't exist.
Mistake 4 — advancing mid after a hi-swap
In Dutch National Flag, the element swapped down from hi is unknown — do NOT advance mid after that swap; inspect it again.
Mistake 5 — rotation direction
Transpose + reverse rows = clockwise. Transpose + reverse columns = counter-clockwise. Easy to say backwards under pressure.
Mistake 6 — spiral boundary checks
The left and up passes need guards (if top <= bottom, if left <= right) or single-row / single-column matrices get double-visited.
10 practice

Practice ladder

In this order — easiest first. Tick each one as you solve it, and re-solve anything that took more than 25 minutes two days later.

15 problems
easyLC 1
Hash map of value -> index; look up the complement in one pass.
easyLC 303
Basic prefix sum. The warm-up.
easyLC 268
Sum formula (or XOR) — expected minus actual.
easyLC 169
Boyer-Moore vote counting.
mediumLC 238
Left/right product passes — no division.
mediumLC 53
Kadane's — extend or restart.
mediumLC 560
Prefix sum + hash map of complements.
mediumLC 75
Dutch National Flag — three pointers.
mediumLC 48
Transpose + reverse rows, in place.
mediumLC 54
Four boundaries, shrink inward.
mediumLC 73
First row/col as in-place markers.
mediumLC 36
One pass, three sets per row/col/box - box key is (r // 3, c // 3).
mediumLC 289
In-place state encoding - store old and new state in two bits, then shift.
mediumLC 31
Rightmost ascent — the 4-step algorithm.
hardLC 41
Cyclic sort — the index-as-hash boss fight.