Data Structures · Sliding Window · A Field Guide

Distinct elements in
every window

Neighboring windows share almost everything. So don't recount — track only what leaves and what enters, and keep a running tally.

121343
01 · The problem

Count the distinct values in each sliding window

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].


02 · The signal

"Every window of size B" means sliding window

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.

The optimization question Stop asking "how many distinct values are in this window?" Ask "what changed since the last window?" Then update only that.

03 · Why a set isn't enough

You need counts, not just membership

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 real requirement You must know not just whether a value is present, but how many times. That means a frequency map, and a value stays distinct as long as its frequency stays above zero.

A train, one car at a time

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.

×3 ×1

A tally sheet, not a guest list

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.

An odometer, not a re-survey

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.


04 · See it slide

Step the window across the array

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.

Sliding-window frequency map

frequency map (current window)
distinct in window: 0
answer so far

05 · The mechanics

Build once, then swap one in and one out

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].

Incoming
A[i]
Increment its frequency. If it was 0 before, distinct count goes up.
Outgoing
A[i − B]
Decrement its frequency. If it hits 0, drop the key and distinct count goes down.
Why the outgoing index is i − B When 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.

06 · Dry run

The sample, window by window

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
The invariant that proves it Right before recording each answer, the map holds exactly the frequencies of the current window's elements. So map.size() is precisely the distinct count — no more, no less.

07 · The code

Clean Java with a HashMap

import 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.


08 · Traps to avoid

Four ways this breaks

1 · Using a plain set

Removing a value on exit deletes it even when another copy remains. Duplicates demand counts, not membership.

2 · Rebuilding each window

Scanning all B elements per window throws away the overlap and lands you back at O(N·B).

3 · Wrong outgoing index

It's arr[i − b], not arr[i − 1] or arr[i − b + 1]. Incoming is i, outgoing trails by the window size.

4 · Leaving zero-count keys

If you never remove keys at frequency 0, map.size() overcounts. Drop them (or track a separate counter).


09 · A faster variant

When values are bounded, skip the hashing

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.


10 · The deeper model

Sliding window is really incremental state

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:

Max sum of size K
state = running sum
sum −= out; sum += in
Distinct in window
state = frequency map
answer = non-zero entries
First negative in window
state = queue of indices
front is the answer
Max in window
state = monotonic deque
front holds the maximum

11 · One month from now

Reconstruct it, don't memorize it

  1. The question is about every fixed-size contiguous window.
  2. Adjacent windows overlap heavily — only one leaves, one enters.
  3. So update incrementally instead of recomputing.
  4. A set can't handle duplicates leaving, so track frequencies.
  5. On each slide: decrement outgoing (drop at zero), increment incoming.
  6. Distinct count = number of live keys in the map.
The transferable lesson When consecutive answers lean on heavily overlapping data, pin down exactly what changes and update only that. Fixed-size window + incremental state + frequency tracking = an O(N) solution. That recipe reaches far past this one problem.