#3MediumSliding window
Longest Substring Without Repeating Characters
Start dumb: try every substring
A substring is contiguous, so the answer is some window [i..j] of the
input. The honest first idea is to try them all: for every start i and
every end j, check whether s[i..j] has a repeat, and keep the longest
clean one.
That check walks the whole candidate, so the total work is O(n³) —
or O(n²) if you grow j one step at a time and carry a seen-set with
you. On LeetCode’s limit of 5·10⁴ characters, n² is 2.5 billion character
touches. Correct, and unusable.
Find the bottleneck
Watch what the brute force actually wastes time on: when the window
[i..j] is clean and you extend it to j+1, everything you knew is still
true. The only thing that can go wrong is the one new character —
s[j+1] colliding with a copy of itself already inside the window. Yet
brute force re-verifies the entire window from scratch, re-proving facts
it already proved.
The optimal shape
Keep a map from character to the last index where it appeared, and a
window [start..end] that is always duplicate-free:
- Extend
endrightward one character at a time. - If the new character was last seen inside the window, move
startto just past that old copy — one O(1) jump. - Record the character’s new position and measure the window.
The map lookup replaces the re-scan; the jump replaces the crawl. Each
pointer only ever moves right, so the whole thing is a single pass —
O(n) time, O(min(n, alphabet)) space. Don’t take the O(n) on
faith: in the player below, watch the start pointer. It never once
moves left. That one-directional march is the proof.
Watch it think
Every step below is one forced move of the algorithm. Step through the
default input, then break it with your own: empty string, all-same
characters (bbbbb), no repeats at all (abcdef), and the abba trap.
Re-traces as you type — any string — try the empty string, bbbbb, abcdef, or the abba trap. Capped at 32 characters so the tiles stay readable.
Step 1 of 23: Nothing is scanned yet, so the only window we can honestly claim is the empty one. Variables: start = 0, best = 0, lastIndex = {}.
Data
Code
1fun lengthOfLongestSubstring(s: String): Int {2 val lastIndex = HashMap<Char, Int>()3 var start = 04 var best = 05 for (end in s.indices) {6 val c = s[end]7 val seen = lastIndex[c]8 if (seen != null && seen >= start) {9 start = seen + 110 }11 lastIndex[c] = end12 best = maxOf(best, end - start + 1)13 }14 return best15}
State
- start
- 0
- best
- 0
- lastIndex
- {}
Why this step
Nothing is scanned yet, so the only window we can honestly claim is the empty one.
1 / 23
The same pattern also solves
Recognize the window, and these fall to the same skeleton.
- Minimum Window Substring (LC 76) ↗
The harder pivot — minimize a window that must cover a target multiset, with a need/have counter.
- Longest Repeating Character Replacement (LC 424) ↗
Same skeleton; the shrink condition becomes "replacements needed exceed k".
- Find All Anagrams in a String (LC 438) ↗
Fixed-size window plus a frequency map instead of last-seen indices.
- Longest Substring with At Most K Distinct Characters (LC 340) ↗
Same window, but the invariant is "map size ≤ k" and start advances by evicting counts.