A visual, reasoning-first study guide

Array Algorithms: Understand the Thinking Behind the Code

Ten essential array problems explained from first principles. Move from brute force to optimization, build the right mental model, inspect each algorithm step by step, and learn the invariant that makes the solution correct.

How to use these notes

This is designed as a learning document, not a last-minute cheat sheet. For every problem, follow the same reasoning path:

01 · Model the problemIdentify what the output represents, what choices are legal, and what the constraints allow.
02 · Start with brute forceDescribe the direct search. This gives you a baseline and exposes repeated work.
03 · Find the reusable informationAsk what can be remembered so the same work is not repeated.
04 · Prove each decisionState the invariant. Explain why a candidate can safely be discarded or a state can be updated.
05 · Trace and implementSimulate small examples, code from the idea, then test boundaries and tricky inputs.
Important distinction: An intuition is the reason an algorithm feels promising. An invariant is the precise statement that stays true while it runs. A correctness argument connects the invariant to the final answer.

First: recognize the family of problem

Array problems often become easier once you identify what kind of information the algorithm needs to maintain.

FamilyProblems hereClue in the promptQuestion to ask
Hash-based lookupTwo Sum, Contains DuplicateFind a pair, detect a repeated valueCan I remember values already seen?
Running summaryStock, Maximum SubarrayBest answer depends on a prefix of the arrayWhat single summary of the past is sufficient?
Prefix / suffix aggregationProduct Except SelfFor each index, combine information on both sidesCan I compute left and right contributions in passes?
Dynamic programming with multiple statesMaximum Product SubarraySign changes can reverse which state is usefulWhat are the best and worst states ending here?
Binary search on structureRotated Minimum, Rotated SearchArray is sorted except for a rotationWhich half can I prove is ordered?
Sorting + two pointers3Sum, Container With Most WaterSorted values or a monotonic decisionWhat pointer movement safely eliminates candidates?

1. Two Sum

LeetCode 1 · Problem statement
HASH MAP

1.1 Understand the actual task

Given an array and a target, return the indices of two different elements whose values add up to the target. We need indices, not merely the values. The input is not necessarily sorted, and there may be negative values or repeated values.

Reframe the question: Instead of asking “Which two values should I combine?”, take one value and ask “What exact value would complete this pair?”

1.2 Derive it from brute force

The direct approach enumerates every pair (i, j) where i < j. For each pair, calculate nums[i] + nums[j]. In the worst case, the number of pairs is:

n(n - 1) / 2 → O(n²)

The expensive part is repeatedly searching all possible partners. But for a fixed value x, there is only one value that can pair with it:

partner = target - x

That changes the question from a pair search into a lookup question: “Have I already seen this partner?” A hash map is designed for fast membership lookup.

1.3 Visual trace: remember the past

nums = [2, 7, 11, 15], target = 9
Press Next to inspect the first element.

Blue = current element · Green = matching complement · The map shown is the set of earlier values and their indices.

Map: { }

1.4 The algorithm, precisely

  1. Start with an empty map seen, storing value → index.
  2. At index i, calculate need = target - nums[i].
  3. If need is in the map, the earlier index and current index form the answer.
  4. If it is absent, store the current value and index, then continue.

1.5 Why check before inserting?

The map must represent only elements from indices before i. This guarantees the pair uses two distinct positions. Consider [3, 3] with target 6: the first 3 is stored; the second 3 finds it. If you inserted first and then searched without carefully excluding the current index, a single element could match itself.

1.6 Correctness argument

1.7 Java implementation

int[] twoSum(int[] nums, int target) {
    Map<Integer, Integer> seen = new HashMap<>();

    for (int i = 0; i < nums.length; i++) {
        int need = target - nums[i];

        if (seen.containsKey(need)) {
            return new int[] { seen.get(need), i };
        }

        seen.put(nums[i], i);
    }
    return new int[0];
}
Complexity

Expected O(n) time: one pass, expected O(1) map operations. O(n) extra space for the map.

Common mistakes

Returning values instead of indices; inserting before checking; assuming input is sorted; forgetting duplicate values can be valid partners.

1.8 Edge cases to reason through

Transferable idea: “Find two values that satisfy a relation” often becomes efficient when you compute the complement and store previously seen values.

2. Best Time to Buy and Sell Stock

LeetCode 121 · Problem statement
GREEDY / RUNNING MIN

2.1 Understand the constraints

You choose one buy day and one later sell day. The order matters: buying must happen before selling. The objective is to maximize prices[sell] - prices[buy]. If every later price is lower, the best profit is zero because doing no transaction is allowed.

2.2 Brute force → useful observation

Brute force tries every pair of days buy < sell, giving O(n²) time. Notice that for a fixed sell day, we do not need every earlier price. We only need the smallest earlier price. Every other earlier buying price gives an equal or smaller profit for this sell day.

Best profit if selling today = today's price − minimum earlier price

2.3 Visual model

prices = [7, 1, 5, 3, 6, 4]
Step through the days. Watch the lowest price and best profit.

2.4 Why a running minimum is sufficient

At day i, all legal buy days are in the prefix [0, i-1]. Among them, the cheapest day is always at least as good as any more expensive day for selling today. Therefore, preserve only that minimum. The algorithm is not guessing the final buy day in advance; it evaluates the best sale possible at every day.

2.5 Invariant and correctness

After processing day i:

For each new day, any newly possible optimal transaction must sell today. The best such transaction buys at the minimum previous price. Comparing that profit with the previous best therefore preserves the invariant.

2.6 Java

int maxProfit(int[] prices) {
    int minPrice = Integer.MAX_VALUE;
    int bestProfit = 0;

    for (int price : prices) {
        minPrice = Math.min(minPrice, price);
        bestProfit = Math.max(bestProfit, price - minPrice);
    }
    return bestProfit;
}
Subtle order point: Updating minPrice before calculating profit is safe because buying and selling on the same day yields zero, never a positive invalid transaction. You can also calculate profit against the old minimum first, then update the minimum; both approaches work.

2.7 Complexity and variations

O(n) time and O(1) extra space. This exact one-transaction state does not directly solve “buy/sell multiple times”, “at most k transactions”, or “cooldown” variants. Those add transaction state or dynamic programming.

3. Contains Duplicate

LeetCode 217 · Problem statement
HASH SET

3.1 What information does the answer need?

The question asks whether any value occurs at least twice. It does not ask for the duplicate's index, frequency, or all duplicate values. That means a set is enough: it records which values have appeared, without storing their positions or counts.

3.2 Two ways to reason

Sorting

Sort the array. Equal values become adjacent. Scan neighbors. Time O(n log n), with possible input mutation or copy space.

Hash set

Scan once. If insertion says the value was already present, return true. Expected O(n) time, O(n) space.

3.3 Visual trace

nums = [4, 1, 2, 4]

3.4 Why Set.add is useful

In Java, HashSet.add(value) returns true if the set changed (the value was new), and false if it was already present. This lets one operation perform the membership test and insertion.

boolean containsDuplicate(int[] nums) {
    Set<Integer> seen = new HashSet<>();

    for (int value : nums) {
        if (!seen.add(value)) {
            return true;
        }
    }
    return false;
}

3.5 Correctness and edge cases

When an insertion fails, the same value was inserted earlier, so at least two occurrences exist. Conversely, if a duplicate exists, its later occurrence will fail to insert. Empty and one-element arrays return false. Negative numbers and zero need no special treatment.

Complexity: Expected O(n) time and O(n) space. Hash operations are expected constant time, not a mathematical worst-case guarantee for every hash table implementation.

4. Product of Array Except Self

LeetCode 238 · Problem statement
PREFIX / SUFFIX

4.1 Translate the output into a formula

For each index i, the answer is the product of every element except nums[i]. Split those elements into two non-overlapping groups: everything left of i and everything right of i.

answer[i] = product(nums[0 ... i-1]) × product(nums[i+1 ... n-1])

4.2 Why division is the tempting but wrong shortcut

You might calculate the total product and divide by nums[i]. But the problem prohibits division, and division fails when an element is zero. Prefix/suffix products solve the actual requirement directly and naturally handle zeroes.

4.3 Visualize the two directions

nums = [1, 2, 3, 4]
Index0123
Left product (excluding i)1126
Right product (excluding i)241241
Multiply them241286

The identity value 1 is important: multiplying by 1 leaves a product unchanged. It represents an empty side at the first or last index.

4.4 Build the optimized algorithm in two passes

  1. Left-to-right pass: Before visiting index i, prefix is the product of all values strictly before i. Write it to answer[i], then include nums[i] in prefix for the next index.
  2. Right-to-left pass: Before visiting i, suffix is the product of all values strictly after i. Multiply it into answer[i], then include nums[i] in suffix.
Notice the timing: store the running product before multiplying by the current value. This is how we exclude the current index without a conditional branch.

4.5 Java implementation

int[] productExceptSelf(int[] nums) {
    int n = nums.length;
    int[] answer = new int[n];

    int prefix = 1;
    for (int i = 0; i < n; i++) {
        answer[i] = prefix;
        prefix *= nums[i];
    }

    int suffix = 1;
    for (int i = n - 1; i >= 0; i--) {
        answer[i] *= suffix;
        suffix *= nums[i];
    }
    return answer;
}

4.6 Correctness proof by meaning of variables

After the first pass, answer[i] equals the product of all elements left of i. During the reverse pass, suffix equals the product of all elements right of the current i. Multiplying these two disjoint products gives exactly every array element except i.

4.7 Complexity and zero cases

O(n) time, two linear passes. O(1) auxiliary space if the output array is not counted. For [1, 2, 0, 4], the result is [0, 0, 8, 0]; for two zeroes, every output is zero. Use the integer type guaranteed by the problem constraints.

5. Maximum Subarray

LeetCode 53 · Problem statement
KADANE / DP

5.1 What makes a subarray special?

A subarray is contiguous: once you choose a start and end index, every element between them is included. The task is to find the contiguous range with the largest sum. This is different from a subsequence, where gaps may be skipped.

5.2 Brute force and the repeated work

Enumerating all start/end pairs gives O(n²) ranges. If you sum each range from scratch, the total work can be O(n³). Prefix sums can bring range-sum evaluation to O(1), producing an O(n²) solution. But we can do better by exploiting a local decision.

5.3 The key question: best subarray ending here

Imagine the current position is i. Any maximum-sum subarray that ends exactly at i must be one of only two forms:

Choice A — Extend

Take the best subarray ending at i−1 and append nums[i].

Choice B — Restart

Discard the earlier segment and begin a new subarray at i.

There is no third useful option. If an earlier segment has a negative total, adding it to the current value makes the sum smaller than starting fresh.

current = max(nums[i], current + nums[i])
best = max(best, current)

5.4 Step-by-step visualization

nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]

Blue = current index. Green = best sum-ending-here segment conceptually. The display tracks the current sum and global best.

5.5 Why all-negative arrays matter

For [-5, -2, -8], the answer is −2, not 0. The problem requires a non-empty subarray. Initialize both current and best from the first element, not from zero. This is a classic example of initialization encoding the problem's constraints.

5.6 Java

int maxSubArray(int[] nums) {
    int current = nums[0];
    int best = nums[0];

    for (int i = 1; i < nums.length; i++) {
        current = Math.max(nums[i], current + nums[i]);
        best = Math.max(best, current);
    }
    return best;
}

5.7 Invariant and proof idea

After processing index i, current is the maximum sum of any non-empty subarray that ends exactly at i. Any such subarray either starts at i or extends a subarray ending at i−1; choosing the larger option is therefore optimal. best is the maximum of these ending-at-each-index values seen so far, so it is the global maximum subarray sum.

Complexity: O(n) time and O(1) space.

Recognition signal: If the question asks for the best contiguous sum and you can describe the best segment that must end at the current index, think of Kadane's algorithm.

6. Maximum Product Subarray

LeetCode 152 · Problem statement
DUAL-STATE DP

6.1 Why the Maximum Subarray recurrence breaks

For sums, a negative running sum never helps a future extension. For products, the sign changes the story: a very negative product can become a very large positive product when multiplied by another negative number. Throwing away the minimum product would lose useful information.

Consider: [-2, 3, -4]
After valueMaximum product ending hereMinimum product ending hereWhy keep both?
-2-2-2Only one element so far
33-63 alone is best; -2 × 3 is worst
-424-12Previous minimum -6 × -4 becomes 24

The global answer is 24, from the entire array.

6.2 Define two states

For current value x, the new subarray can be just x, or extend either previous extreme:

candidates = { x, x × oldMax, x × oldMin }
newMax = max(candidates)
newMin = min(candidates)

6.3 Visual sign reasoning

x is positiveMultiplication preserves order. Previous maximum tends to remain maximum; previous minimum tends to remain minimum.
x is negativeMultiplication reverses order. Previous minimum may become the new maximum, and previous maximum may become the new minimum.
x is zeroAny extended product becomes zero. Starting a new subarray at zero is naturally considered by candidate x.

6.4 Java

int maxProduct(int[] nums) {
    int maxEnding = nums[0];
    int minEnding = nums[0];
    int answer = nums[0];

    for (int i = 1; i < nums.length; i++) {
        int x = nums[i];

        int fromMax = x * maxEnding;
        int fromMin = x * minEnding;

        int nextMax = Math.max(x, Math.max(fromMax, fromMin));
        int nextMin = Math.min(x, Math.min(fromMax, fromMin));

        maxEnding = nextMax;
        minEnding = nextMin;
        answer = Math.max(answer, maxEnding);
    }
    return answer;
}

6.5 Why use temporary variables?

Both new states must be computed from the previous index's states. If you overwrite maxEnding first and then calculate minEnding using that updated maximum, you mix two different time steps. Store nextMax and nextMin first, then assign both.

6.6 Correctness and complexity

Every subarray ending at i is either the single element x or an earlier subarray ending at i−1 extended by x. To find the largest or smallest resulting product, only the earlier maximum and minimum can matter because multiplication by x is monotonic when x is positive and reverses order when x is negative. Thus the two states are sufficient. O(n) time, O(1) space.

Common trap: Do not copy Kadane's sum recurrence. The sign-sensitive minimum state is the essential extra information.

7. Find Minimum in Rotated Sorted Array

LeetCode 153 · Problem statement
BINARY SEARCH

7.1 Understand what rotation preserves

A sorted array such as [0,1,2,4,5,6,7] can be rotated to [4,5,6,7,0,1,2]. The entire array is no longer sorted, but it is made of two individually sorted runs. The smallest value is the first value in the right run — the rotation pivot.

Rotated array
4567012

There is a single “drop” from 7 to 0. The minimum is at that boundary.

7.2 Why binary search is possible

We cannot compare the middle with a global target, but we can compare it with the rightmost value. With distinct elements:

7.3 Interactive interval narrowing

nums = [4, 5, 6, 7, 0, 1, 2]

Blue = current mid. Gold = current search interval. Green = final minimum when found.

7.4 Java

int findMin(int[] nums) {
    int left = 0;
    int right = nums.length - 1;

    while (left < right) {
        int mid = left + (right - left) / 2;

        if (nums[mid] > nums[right]) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return nums[left];
}

7.5 Why is the update different from ordinary binary search?

When the minimum may be at mid, we must keep mid by setting right = mid, not mid - 1. When mid is known to be larger than the minimum, we can safely discard it and everything left of it with left = mid + 1. The interval always contains the minimum.

Termination: The loop stops at left == right. Since the interval invariant says the minimum is inside the interval, the single remaining index must be the answer.

Complexity: O(log n) time, O(1) space. This version assumes distinct elements. Duplicates require handling ambiguous comparisons and can degrade worst-case time to O(n).

9. 3Sum

LeetCode 15 · Problem statement
SORT + TWO POINTERS

9.1 Reduce a three-number problem to a two-number problem

We need three values whose sum is zero. If we fix one value a, the remaining two must sum to -a. That is a Two Sum problem on the remaining suffix. Sorting lets us solve that pair problem with two pointers rather than another hash map.

a + b + c = 0 ⇒ b + c = −a

9.2 Why sorting changes everything

After sorting, values increase from left to right. For a fixed first value:

9.3 Visual example

Input: [-1, 0, 1, 2, -1, -4]
Sorted: [-4, -1, -1, 0, 1, 2]
-1-12012

Fix the first -1. Need a pair sum of +1. The pair (-1, 2) works. Continuing the scan also finds (0, 1), giving [-1, 0, 1].

9.4 Duplicate handling is part of the algorithm

The output asks for unique triplets, not unique index combinations. Sorting groups equal values together, which makes duplicates easy to skip:

9.5 Java implementation

List<List<Integer>> threeSum(int[] nums) {
    Arrays.sort(nums);
    List<List<Integer>> result = new ArrayList<>();

    for (int i = 0; i < nums.length - 2; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) continue;
        if (nums[i] > 0) break;

        int left = i + 1;
        int right = nums.length - 1;

        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];

            if (sum < 0) {
                left++;
            } else if (sum > 0) {
                right--;
            } else {
                result.add(Arrays.asList(
                    nums[i], nums[left], nums[right]
                ));
                left++;
                right--;

                while (left < right &&
                       nums[left] == nums[left - 1]) left++;
                while (left < right &&
                       nums[right] == nums[right + 1]) right--;
            }
        }
    }
    return result;
}

9.6 Why can we break when nums[i] > 0?

The array is sorted. If the fixed value is positive, every value to its right is also positive. Three positive values cannot sum to zero. The same monotonic ordering that supports pointer movement supports this early termination.

9.7 Complexity

Sorting costs O(n log n). The outer loop runs O(n) times, and each two-pointer scan is O(n), so total time is O(n²). Output storage is not counted as auxiliary working space; the result can contain O(n²) triplets in the general bound.

Common failure: Skipping duplicates only at the fixed index is not enough. Duplicate left/right values can still emit the same triplet more than once.

10. Container With Most Water

LeetCode 11 · Problem statement
TWO POINTERS / GREEDY

10.1 Translate the picture into a formula

Choose two vertical lines. They form a container whose width is the distance between their indices. Water height is limited by the shorter line; water cannot rise above the shorter wall.

area(left, right) = (right − left) × min(height[left], height[right])

10.2 Why brute force is expensive

Every pair of lines is a possible container. There are O(n²) pairs. The optimized algorithm begins with the widest pair — the two endpoints — then removes one candidate boundary at a time using a proof.

10.3 The crucial elimination proof

Suppose the left wall is shorter. The current area is limited by that left height. If we keep the same left wall and move the right wall inward:

So every pair that keeps this short left wall and chooses a closer right wall is already ruled out. We can safely move the left pointer. The symmetric argument applies when the right wall is shorter.

The algorithm does not claim the next move will improve the area. It claims that moving the taller wall cannot help, while moving the shorter wall is the only move that has a chance to find a taller limiting boundary.

10.4 Interactive pointer visualization

height = [1, 8, 6, 2, 5, 4, 8, 3, 7]

Blue = current boundaries. Green = best area found so far. The height of the water is the shorter of the two boundary heights.

10.5 Java

int maxArea(int[] height) {
    int left = 0;
    int right = height.length - 1;
    int best = 0;

    while (left < right) {
        int width = right - left;
        int limitingHeight =
            Math.min(height[left], height[right]);
        int area = width * limitingHeight;

        best = Math.max(best, area);

        if (height[left] < height[right]) {
            left++;
        } else {
            right--;
        }
    }
    return best;
}

10.6 Correctness and complexity

At each step, the current pair is evaluated. Then the shorter boundary is moved. The elimination proof shows that no unevaluated pair using that same shorter boundary and a narrower width can improve the current area. Thus no potentially optimal pair is discarded. Each pointer moves inward at most n−1 times: O(n) time and O(1) space.

10.7 Edge cases

Final pattern revision — what should you remember?

ProblemMental questionState / structureTimeSpace
Two SumWhat complement completes the current value?Map value → earlier indexO(n) expectedO(n)
StockWhat is the cheapest earlier buy?Minimum so far, best profitO(n)O(1)
Contains DuplicateHave I seen this value?HashSetO(n) expectedO(n)
Product Except SelfWhat is the product on each side?Prefix and suffixO(n)O(1) extra
Maximum SubarrayExtend or restart?Best sum ending hereO(n)O(1)
Maximum ProductCould the minimum flip into maximum?Max and min product ending hereO(n)O(1)
Rotated MinimumWhich side contains the pivot?Binary search intervalO(log n)O(1)
Rotated SearchWhich half is sorted, and can target fit?Modified binary searchO(log n)O(1)
3SumFix one value; how should the pair move?Sort + two pointersO(n²)Depends on sort; output excluded
Container WaterWhich boundary is the bottleneck?Two pointers + elimination proofO(n)O(1)

Interview explanation template

  1. “A straightforward solution would be…” — state brute force and its complexity.
  2. “The repeated work / key observation is…” — identify what can be remembered or eliminated.
  3. “I will maintain…” — define map, pointers, or DP state.
  4. “The invariant is…” — state exactly what remains true.
  5. “This is correct because…” — explain why no valid or optimal candidate is lost.
  6. “The complexity is…” — derive time and auxiliary space.

Practice without looking at the solution

1. Why does Two Sum look up before inserting?

To ensure the complement comes from an earlier, different index. It also handles the repeated-value case cleanly.

2. Why is one minimum enough for Stock?

For a fixed sell day, the cheapest earlier buy maximizes the profit. All other earlier prices are dominated.

3. Why does Product Except Self not need division?

Every answer is the product of a left contribution and a right contribution. Two directional passes compute those contributions directly, including when zeroes exist.

4. Why does Kadane work on all-negative arrays?

Because current and best start at the first actual element, enforcing a non-empty subarray. The algorithm returns the least negative value.

5. Why track both maximum and minimum product?

A negative number reverses product ordering. The most negative previous product can become the most positive after multiplication by another negative.

6. Why does rotated-array binary search still work?

At least one half is sorted. Its value interval tells us whether the target can be there, allowing the other half to be discarded.

7. Why move the shorter wall in Container With Most Water?

The shorter wall limits the current area. Keeping it while reducing width cannot produce a better area, so it is safe to discard that boundary.

Suggested study loop: First read the problem and derive the brute-force solution. Then hide the optimized section and try to discover the key observation. Trace the visualization by hand. Finally, implement from memory and explain the invariant aloud. Revisit the problem the next day without reading the code.