Week 2: Arrays, Two Pointers & Sliding Window

Week 1 gave you the vocabulary — references, Big-O, the STL basics — to read and reason about code. This week puts that vocabulary to work on the data structure you'll touch in nearly every problem: the array. You'll build prefix sums for O(1) range queries, learn the two-pointer technique in both its opposite-ends and same-direction forms, and turn brute-force nested loops into single-pass sliding-window solutions. The fast/slow pointer pattern you practice here on arrays is the same one you'll re-apply to detect cycles in linked lists starting Week 6, and the deque-based sliding-window-maximum pattern in Week 7 builds directly on the fixed-window technique below.

Module 2 of 17 Week 2 of 20 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Build and query prefix sum arrays in O(1) per query after O(n) preprocessing
  • Apply the two-pointer technique in both opposite-ends and fast/slow forms
  • Recognize when a problem calls for a fixed-size vs. variable-size sliding window and implement both

1. Prefix Sums & In-Place Array Manipulation

A prefix sum array stores the running total of an array up to each index. Once built, it turns any range-sum query into a single subtraction — O(1) per query instead of re-summing the range every time. This is the single highest-leverage trick in array problems: whenever you see "answer many queries about a range," prefix sums are the first thing to consider.

prefix_sum.cpp
#include <vector>
using namespace std;

// prefix[i] holds the sum of nums[0..i-1], so prefix[0] = 0
vector<int> buildPrefixSum(const vector<int>& nums) {
    vector<int> prefix(nums.size() + 1, 0);
    for (int i = 0; i < (int)nums.size(); i++) {
        prefix[i + 1] = prefix[i] + nums[i];
    }
    return prefix;
}

// Sum of nums[l..r] inclusive, O(1) after the O(n) build above
int rangeSum(const vector<int>& prefix, int l, int r) {
    return prefix[r + 1] - prefix[l];
}

Prefix sums are one flavor of a broader habit: manipulating an array in place with index bookkeeping instead of allocating new containers. A classic example is separating non-zero elements from zeroes using a single "write pointer" that trails a "read pointer":

move_zeroes.cpp
#include <vector>
#include <algorithm>
using namespace std;

// Moves all zeroes to the end while preserving the relative order of non-zero elements.
// O(n) time, O(1) extra space.
void moveZeroesToEnd(vector<int>& nums) {
    int writePos = 0;
    for (int readPos = 0; readPos < (int)nums.size(); readPos++) {
        if (nums[readPos] != 0) {
            swap(nums[writePos], nums[readPos]);
            writePos++;
        }
    }
}
Prefix sums scale to 2D too

The same idea extends to a 2D prefix-sum grid for rectangle-sum queries, and generalizes further into the Fenwick tree (Binary Indexed Tree) you'll build in Week 14 — which supports the same O(1)-ish range queries but also allows O(log n) point updates, something a plain prefix array can't do without a full O(n) rebuild.

2. Two-Pointer Technique

The two-pointer technique replaces a nested loop with two indices that each move at most n times total, turning an O(n²) brute force into O(n). It comes in two distinct shapes, and recognizing which one a problem needs is half the battle.

Opposite-ends pointers

When the array is sorted, a pointer starting at each end can decide which side to move based on a comparison — no need to check every pair:

two_sum_sorted.cpp
#include <vector>
using namespace std;

// nums is sorted ascending. Returns indices (i, j) with nums[i] + nums[j] == target,
// or (-1, -1) if no such pair exists. O(n) time, O(1) space.
pair<int, int> twoSumSorted(const vector<int>& nums, int target) {
    int left = 0, right = (int)nums.size() - 1;
    while (left < right) {
        int sum = nums[left] + nums[right];
        if (sum == target) return {left, right};
        else if (sum < target) left++;   // sum too small -- grow it
        else right--;                     // sum too big -- shrink it
    }
    return {-1, -1};
}

Fast/slow pointers (same direction)

Both pointers start together and move in the same direction, but at different rates or under different conditions. A common use is compacting an array in place — here, deduplicating a sorted array by letting a "slow" write pointer only advance when a genuinely new value is found:

remove_duplicates.cpp
#include <vector>
using namespace std;

// nums is sorted. Overwrites nums in place so the first k slots hold the unique
// values in order, and returns k. O(n) time, O(1) extra space.
int removeDuplicates(vector<int>& nums) {
    if (nums.empty()) return 0;
    int slow = 0;
    for (int fast = 1; fast < (int)nums.size(); fast++) {
        if (nums[fast] != nums[slow]) {
            slow++;
            nums[slow] = nums[fast];
        }
    }
    return slow + 1;
}
This pattern reappears on linked lists

The exact fast/slow idea above — one pointer that advances under a condition, one that always advances — is what Floyd's tortoise-and-hare cycle detection uses on a linked list in Week 6. If you're comfortable with fast/slow on an array now, that week is mostly a change of data structure, not a new idea.

3. Fixed-Size Sliding Window

When a problem asks about every contiguous subarray of a fixed length k, recomputing the sum (or max, or count) of each window from scratch is O(n·k). A sliding window instead updates the running value incrementally as the window slides one position at a time — drop the element leaving the window, add the element entering it:

max_sum_fixed_window.cpp
#include <vector>
#include <algorithm>
using namespace std;

// Maximum sum of any contiguous subarray of length k. O(n) time, O(1) space.
int maxSumFixedWindow(const vector<int>& nums, int k) {
    int windowSum = 0;
    for (int i = 0; i < k; i++) windowSum += nums[i];

    int best = windowSum;
    for (int i = k; i < (int)nums.size(); i++) {
        windowSum += nums[i] - nums[i - k];  // slide: add new, drop old
        best = max(best, windowSum);
    }
    return best;
}

Every element enters the window exactly once and leaves it exactly once, so the total work across the whole array is O(n) regardless of how large k is — a significant improvement over the O(n·k) brute force of resumming each window.

Fixed window works for more than sums

The same "add new, drop old" shape works for a window's max, min, frequency counts, or a hash of its contents. Only the "add" and "drop" operations change; the sliding skeleton stays identical. When tracking a window's max efficiently, Week 7's monotonic deque is the tool that keeps that update O(1) amortized instead of O(k).

4. Variable-Size Sliding Window

Some problems don't fix the window length in advance — instead they ask for the smallest or largest window satisfying some condition. The pattern still uses two pointers, left and right, but now right always advances while left only advances when the window needs to shrink to restore a property:

smallest_subarray_at_least_target.cpp
#include <vector>
#include <climits>
using namespace std;

// Length of the smallest contiguous subarray whose sum is >= target, or 0 if none exists.
// All values in nums are assumed positive, so shrinking from the left is always safe.
int smallestSubarrayWithSumAtLeast(const vector<int>& nums, int target) {
    int left = 0;
    long long windowSum = 0;
    int best = INT_MAX;

    for (int right = 0; right < (int)nums.size(); right++) {
        windowSum += nums[right];
        while (windowSum >= target) {
            best = min(best, right - left + 1);
            windowSum -= nums[left];
            left++;
        }
    }
    return best == INT_MAX ? 0 : best;
}

The nested while loop looks like it might make this O(n²), but it isn't: left only ever moves forward, and across the entire run of the outer loop it can advance at most n times total. That gives an amortized O(n) time complexity — every index is visited by right once and by left at most once.

Fixed vs. variable: how to tell them apart

If the problem gives you an explicit window length k, it's fixed-size. If it asks you to find the smallest/largest/shortest/longest window satisfying some condition, it's variable-size — you'll grow right to search for the condition and shrink left to optimize once it's met. This exact variable-window shape returns in Week 4 for longest-substring string problems.

5. Hands-on Exercise

Hands-on

Build a mini sales-analytics toolkit

Apply prefix sums, both flavors of two pointers, and both window sizes to a single running dataset of daily sales figures.

Requirements:

  1. Implement buildPrefixSum and rangeSum, and verify a few range-sum queries by hand against a sample vector<int>.
  2. Implement twoSumSorted using opposite-ends two pointers on a sorted copy of the data to find two days whose sales add up to a target figure.
  3. Implement removeDuplicates using the fast/slow pointer pattern to collapse consecutive equal-sales days.
  4. Implement maxSumFixedWindow to find the best k-day sales streak for a given k.
  5. Implement smallestSubarrayWithSumAtLeast to find the shortest run of days that hits a revenue target.
  6. For each function, add a comment stating its time and space complexity, and confirm your fixed-window and variable-window results against a brute-force nested loop on a small test case.
Hint

Write the brute-force nested-loop version of each function first, on a small array where you can check the answer by eye. Then write the optimized version and assert the two agree on random inputs before trusting the optimized one on anything larger.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why does building a prefix sum array make repeated range-sum queries faster, and what does it cost you?

Without preprocessing, each range-sum query costs O(range length) because you re-add every element in it. A prefix sum array pays an O(n) cost once, up front, so that every subsequent query becomes a single subtraction — O(1). The cost is O(n) extra space for the prefix array, and it only works cleanly for static data; if the underlying array changes, you have to rebuild the prefix array (or reach for a Fenwick tree instead, as in Week 14).

Q2

Why must the array be sorted to apply the opposite-ends two-pointer technique for two-sum?

The technique decides whether to move left or right based on whether the current sum is too small or too large — that decision is only correct if moving left forward is guaranteed to increase the sum and moving right backward is guaranteed to decrease it. That guarantee only holds when the array is sorted; on an unsorted array, moving either pointer could move the sum in either direction, so the pruning logic breaks.

Q3

How do you decide whether a problem calls for a fixed-size or variable-size sliding window?

If the problem states an exact window length k up front (e.g. "the best k-day streak"), it's fixed-size, and you slide by adding one new element and dropping one old one each step. If instead the problem asks you to find the shortest or longest contiguous run satisfying some condition, without telling you the length, it's variable-size, and you grow the right edge to search for the condition and shrink the left edge to optimize once you have it.

Q4

The variable sliding window has a while loop nested inside a for loop — why is the overall complexity still O(n) and not O(n²)?

The key is that left only ever moves forward — it never resets or moves backward. Even though the while loop runs a variable number of times on each iteration of the outer for loop, the total number of times left can advance across the entire function call is bounded by n. Summing that up, the whole function does at most 2n pointer movements total, which is O(n) amortized, not O(n²).