Neighboring windows share almost everything. So don't recount — track only what leaves and what enters, and keep a running tally.
Given an array and a window size B, slide a window of that width across the array and report how
many distinct values sit inside each position. For A = [1,2,1,3,4,3] with B = 3:
| Window | Distinct set | Count |
|---|---|---|
[1, 2, 1] |
{1, 2} | 2 |
[2, 1, 3] |
{1, 2, 3} | 3 |
[1, 3, 4] |
{1, 3, 4} | 3 |
[3, 4, 3] |
{3, 4} | 2 |
Answer: [2, 3, 3, 2].
Phrases like fixed-size subarray, window of size K, or every consecutive K elements
are the tell. The reason is overlap: consecutive windows share B − 1 elements. Going from
[1,2,1] to [2,1,3], only two things happen — 1 leaves the left,
3 enters the right. Everything in between is untouched.
A HashSet is tempting, but it breaks the moment duplicates leave. Take window [1,2,1]
— the set is {1, 2}. When the leftmost 1 slides out, the window becomes
[2,1], which still contains a 1. If you'd done set.remove(1) you'd have
wrongly dropped it.
The window is a train of fixed length. Each step, one car uncouples from the back and one couples to the front. You never rebuild the whole train — you just track the swap.
A guest list only says who's here. A tally sheet counts how many of each. When one copy leaves, you cross off a mark — the name stays until the count hits zero.
You don't re-measure the whole road each mile. The odometer carries state forward and ticks up or down. The distinct-count is that running dial.
Pick an example and slide the window one position at a time. The incoming element lifts in from the right, the outgoing one drops out from the left, and the frequency map on the right updates live. Bars show each value's count; the distinct total is just the number of bars.
Build the frequency map for the first window (indices 0…B−1). After that, for each new position
i from B to N−1: the entering element is A[i] and the
leaving element is A[i − B].
i = B, the previous window was 0…B−1, so index 0 leaves — and
i − B = 0. It's not i − 1. That off-by-window slip is the classic bug here.
For [1,2,1,3,4,3], B = 3. Watch the map stay exactly in sync with the current window:
| Window | Remove | Add | Frequency map | Distinct |
|---|---|---|---|---|
[1,2,1] |
— (build) | — | {1:2, 2:1} |
2 |
[2,1,3] |
1 | 3 | {1:1, 2:1, 3:1} |
3 |
[1,3,4] |
2 → key dropped | 4 | {1:1, 3:1, 4:1} |
3 |
[3,4,3] |
1 → key dropped | 3 | {3:2, 4:1} |
2 |
map.size() is precisely the distinct count — no more, no less.
HashMapimport java.util.*; static int[] countDistinct(int n, int b, int[] arr) { if (b > n) return new int[0]; // window bigger than array int[] result = new int[n - b + 1]; Map<Integer,Integer> freq = new HashMap<>(); // build the first window for (int i = 0; i < b; i++) freq.put(arr[i], freq.getOrDefault(arr[i], 0) + 1); result[0] = freq.size(); int r = 1; for (int i = b; i < n; i++) { int out = arr[i - b]; // leaving from the left freq.put(out, freq.get(out) - 1); if (freq.get(out) == 0) freq.remove(out); // keep size == distinct int in = arr[i]; // entering from the right freq.put(in, freq.getOrDefault(in, 0) + 1); result[r++] = freq.size(); } return result; }
Building the first window is O(B); every slide after is one decrement and one increment, each O(1) on average. Total: O(N) time, O(B) space — you can't beat O(N) since you must read every element at least once.
Removing a value on exit deletes it even when another copy remains. Duplicates demand counts, not membership.
Scanning all B elements per window throws away the overlap and lands you back at O(N·B).
It's arr[i − b], not arr[i − 1] or arr[i − b + 1]. Incoming is
i, outgoing trails by the window size.
If you never remove keys at frequency 0, map.size() overcounts. Drop them (or
track a separate counter).
If the constraint says values fit a small fixed range (e.g. 1 ≤ Aᵢ ≤ 10000), swap the map for a
plain integer array and keep a manual distinctCount. Same logic, no hashing overhead.
| Approach | Time | Space | When to use |
|---|---|---|---|
| HashMap | O(N) avg | O(B) | Values unbounded or huge range — the general, transferable choice |
| Frequency array | O(N) | O(max value) | Small, known value range — fastest and simplest here |
The decision rule: small bounded domain → array; large or unbounded → HashMap. Learn the map version first — it carries over to problems where you can't predict the value range.
The strongest mental model isn't "move two pointers." It's: keep a piece of state describing the current window, then transition it by subtracting the outgoing element and adding the incoming one. The answer is derived from that state. Only the state changes from problem to problem: