Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Find the longest window with all distinct characters. The interview-grade answer is a variable-size sliding window: expand the right edge, and when you hit a repeat, jump the left edge to just past the duplicate's last seen index. A hash map of char → last index makes the jump O(1). One pass, O(n) time, O(min(n, alphabet)) space.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you use a variable sliding window with a last-seen-index map to achieve one pass, instead of re-checking every substring, and whether you correctly avoid moving the left edge backward.
Where candidates slip
Setting left = last[c] + 1 even when the duplicate is outside the window (moving left backward), or forgetting to update last[c] when no jump occurs.
How to stand out
State the O(n³)/O(n²) brute force, then 'a window with a last-index map jumps left in O(1)'. Call out the `last[c] >= left` guard that prevents the left pointer from regressing.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewLongest substring, no repeatsCan the left edge only move forward?No (recheck all)YesExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Brute Force — Check Every Substring
1Hint 1Explore hintHide hint
The answer is some substring. How would you test a single substring for 'all distinct'?
2Hint 2Explore hintHide hint
For each start index i, extend an end index j and keep a set of characters in [i..j]. Stop when you'd add a duplicate.
3Hint 3Explore hintHide hint
Track the longest clean window found over all starts. It re-scans overlapping ranges — that's the inefficiency to remove.
A · Core idea
Fix a start index i. Extend an end index j rightward, adding each character to a set; the moment a character is already in the set, the substring [i..j-1] is the longest clean run starting at i. Record its length, then restart from i+1.
Why this is the natural first attempt. It directly encodes 'longest substring with all distinct characters' — try each start, grow until you can't. The set makes the distinctness test O(1) per step, so it's already better than re-checking each substring from scratch.
Why we don't ship this. Restarting the scan at every start index re-examines overlapping ranges over and over. With n up to 5 × 10⁴, the quadratic blowup is too slow. The key waste: when we hit a duplicate starting at i, we throw away all the distinctness work and restart at i+1, even though most of that window is still valid. The sliding window keeps that work.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s = "abca"i=0
a,b,c then a repeats
window "abc" len 33C · Code with teaching comments
Reference implementation previewpublic int lengthOfLongestSubstring(String s) { int n = s.length(); int best = 0; for (int i = 0; i < n; i++) { Set<Character> seen = new HashSet<>();Explore the full codeHide the full code
D · Why it works and complexityO(n²) timeO(min(n, Σ)) spaceOpen the invariant and proofExplore the proofHide the proof
When the inner loop hits a repeat, everything before the duplicate's first occurrence is still a valid window — restarting from i+1 discards that knowledge. A sliding window instead slides the left edge just past the conflict, reusing all the work in between.
ComplexityO(n²) time, O(min(n, Σ)) space — For each of n start indices, the inner extension does up to n work with O(1) set operations → O(n²) time. The set holds at most min(n, alphabet size) characters → O(min(n, Σ)) space.
E · Edge cases and pitfalls3Re-creating the set per start index — the source of the quadratic cost.Explore edge casesHide edge cases
Pitfalls
- Re-creating the set per start index — the source of the quadratic cost.
- Off-by-one: the clean length is j - i, since j stopped on the duplicate.
- Assuming a 26-letter alphabet when symbols and spaces are allowed.
Approach 2 of 2 · Optimal
Sliding Window + Last-Index Map (Optimal)
1Hint 1Explore hintHide hint
Instead of restarting at every start index, keep one window [left, right] that always holds distinct characters.
2Hint 2Explore hintHide hint
Store each character's last seen index. When the new character was seen at index p and p is inside the window (p >= left), the window must skip past p.
3Hint 3Explore hintHide hint
Jump left to p + 1 directly — no need to step it one at a time. Update the character's last index to right and track the max window length.
A · Core idea
Maintain a window [left, right] that is always free of repeats, plus a hash map last from character to the most recent index where it appeared. Slide right forward one character at a time.
Handling a repeat with a jump, not a crawl. When the character c = s[right] was last seen at index p = last[c], and p is inside the current window (p >= left), then c would repeat. The fix is to move left to p + 1 — instantly excluding the earlier c and everything before it. This is the whole speedup: instead of shrinking the window one step at a time, we leap the left edge directly past the conflict.
Why the `p >= left` guard is essential. The map remembers a character's last index anywhere in the string, even from before the current window. If that old index is to the left of left, the character is not in the window and there's no conflict — jumping to p + 1 would move left backward, wrongly re-including characters and breaking the invariant. The guard ensures we only jump when the duplicate is genuinely inside the window, so left never regresses.
Why update `last[c]` every iteration. Whether or not we jumped, the current character's newest position is right, so we always set last[c] = right. Forgetting this when there's no jump would leave a stale index and corrupt future guards.
Why one pass suffices. right advances n times; left only ever moves forward and never passes right. Their combined movement is bounded by 2n, so the algorithm is linear — every character of work the brute force repeated is now done once.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
s = "abcabcbb"Array state
right=2
"abc" all distinct, len 3
3C · Code with teaching comments
D · Why it works and complexityO(n) timeO(min(n, Σ)) spaceOpen the invariant and proofExplore the proofHide the proof
The map of last indices converts 'shrink the window until the duplicate is gone' into a single O(1) jump. The reusable lesson: when a window violation is caused by one known element, remembering where that element was lets you skip the left pointer straight past it instead of inching forward.
ComplexityO(n) time, O(min(n, Σ)) space — Each index is visited once by `right`, and `left` only moves forward, so total pointer movement is O(n). The map holds at most one entry per distinct character → O(min(n, Σ)) space.
E · Edge cases and pitfalls6s = "" — Empty string; loop never runs, best stays 0.Explore edge casesHide edge cases
Edge cases handled
s = ""Empty string; loop never runs, best stays 0.s = "abba"At right=3 'a', a's last index is 0 but left has moved to 2, so 0 < left → no backward jump; window stays valid.s = " "Single space; counts as a character, best = 1.
Pitfalls
- Omitting the `last[c] >= left` guard — left moves backward on duplicates outside the window (the classic 'abba' bug).
- Not updating last[c] when there's no jump, leaving stale indices.
- Computing the length as right − left (missing the +1).
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic int lengthOfLongestSubstring(String s) { Map<Character, Integer> last = new HashMap<>(); int left = 0, best = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right);Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Missing the window guard (the 'abba' bug)
The map remembers a char's last index even outside the window. Without the >= left check, 'abba' jumps left backward to a stale position and overcounts.
Explore the code comparisonHide the code comparison
Missing the window guard (the 'abba' bug)
The map remembers a char's last index even outside the window. Without the >= left check, 'abba' jumps left backward to a stale position and overcounts.
Don't write this
if c in last:
left = last[c] + 1
last[c] = rightWrite this instead
if c in last and last[c] >= left:
left = last[c] + 1
last[c] = right2Forgetting to update last[c] without a jump
If you only update last[c] when a jump happens, later guards read stale indices and the window logic breaks.
Explore the code comparisonHide the code comparison
Forgetting to update last[c] without a jump
If you only update last[c] when a jump happens, later guards read stale indices and the window logic breaks.
Don't write this
if (last.containsKey(c) && last.get(c) >= left) {
left = last.get(c) + 1;
last.put(c, right);
}Write this instead
if (last.containsKey(c) && last.get(c) >= left) {
left = last.get(c) + 1;
}
last.put(c, right);Revise
Carry the pattern into the interview
Pattern to remember
Variable sliding window + last-seen-index map
Code pattern to memorize
last = {}
left = 0
best = 0
for right, c in enumerate(s):
if c in last and last[c] >= left:
left = last[c] + 1
last[c] = right
best = max(best, right - left + 1)
return bestRules to remember
- 1On a repeat, jump
lefttolast[c] + 1— don't crawl it one step at a time. - 2Guard with
last[c] >= left: a duplicate outside the current window must be ignored. - 3Update
last[c] = rightevery iteration, even when no jump happens. - 4Window length is
right − left + 1; take the max after each expansion. - 5Set-based variant works too but shrinks left in a loop; the index map jumps in O(1).
Use this pattern when
- Longest/shortest contiguous window satisfying a 'no repeats' or distinctness constraint
- You can extend the window on the right and only ever need to move left forward
- A per-character last position lets you skip the left pointer ahead
- Brute force enumerates all substrings and you want a single pass
Choose another pattern when
- Order doesn't matter / subsequences allowed → not a contiguous-window problem
- Need at most k distinct (not zero repeats) → window with a count map (LC340)
- Fixed window size → simpler fixed sliding window (Permutation in String)
Interview takeaway
A window that only ever grows on the right and jumps left forward on a violation is the variable sliding-window template. The 'remember each element's last index to jump the left edge' trick generalizes to many substring problems.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternVariable sliding window with a last-seen-index map. Reach for it on longest/shortest contiguous-window problems where a violation is caused by a single known element you can jump past. Same family as At Most K Distinct and Minimum Window Substring.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Longest Substring with At Most Two Distinct CharactersComing soonWindow with a count map; shrink left while distinct-count exceeds 2.
- Longest Substring with At Most K Distinct CharactersComing soonGeneralize to k distinct — same window, shrink when the map has more than k keys.
- Longest Repeating Character ReplacementWindow valid when windowLen − maxFreq ≤ k; another variable-window classic.
- Minimum Window SubstringShrinking-window cousin — grow to satisfy a need, then shrink to minimize.