Longest substring without repeating characters (sliding window)
Sliding window with a Map<char, lastIndex>. Move right pointer through the string; when the current char's last index is within the window, jump left to one past it; track max window size as you go. O(n) time, O(min(n, alphabet)) space.
Classic sliding window problem. Two cleanest implementations: with a Set (shrink one at a time) or with a Map of last indices (jump left in O(1)).
Implementation — Map of last indices
function lengthOfLongestSubstring(s) {
const lastIndex = new Map();
let left = 0, best = 0;
for (let right = 0; right < s.length; right++) {
const ch = s[right];
if (lastIndex.has(ch) && lastIndex.get(ch) >= left) {
left = lastIndex.get(ch) + 1; // jump past the previous occurrence
}
lastIndex.set(ch, right);
best = Math.max(best, right - left + 1);
}
return best;
}- Time O(n) — each
rightadvances once;leftonly ever moves forward. - Space O(min(n, |Σ|)) — bounded by alphabet size.
Why the >= left check
lastIndex may remember a duplicate from before the window. If lastIndex.get(ch) < left, it's out of the window — ignore it. Without that check, left could go backward, breaking the invariant.
Alternative — Set, shrink one at a time
function lengthOfLongestSubstring(s) {
const inWindow = new Set();
let left = 0, best = 0;
for (let right = 0; right < s.length; right++) {
while (inWindow.has(s[right])) {
inWindow.delete(s[left++]);
}
inWindow.add(s[right]);
best = Math.max(best, right - left + 1);
}
return best;
}Same O(n) (each char enters and leaves at most once), simpler invariant, slightly slower in practice.
Walkthrough — "abcabcbb"
right=0 'a' → window [0,0] "a" → best 1
right=1 'b' → window [0,1] "ab" → best 2
right=2 'c' → window [0,2] "abc" → best 3
right=3 'a' → 'a' last at 0, in window → left = 1 → "bca" → best 3
right=4 'b' → 'b' last at 1, in window → left = 2 → "cab" → best 3
right=5 'c' → 'c' last at 2, in window → left = 3 → "abc" → best 3
right=6 'b' → 'b' last at 4, in window → left = 5 → "cb" → best 3
right=7 'b' → 'b' last at 6, in window → left = 7 → "b" → best 3Answer: 3.
Edge cases
- Empty string → 0.
- All same characters ("aaaa") → 1.
- All distinct ("abcdef") → length.
- Single character → 1.
- Unicode / multi-byte — iterate by code points if you care about emoji/surrogate pairs:
for (const ch of s).
Variants
- Longest substring with at most K distinct characters — same shape, condition on Map size.
- Longest substring with at most K repeating characters — track counts.
- Smallest window containing all chars of T — minimum window substring.
Interview framing
"Sliding window with a Map of char → last index. Move right through the string. When the current char's last seen index is within the window (>= left), advance left to one past it — that jump is what makes this O(n) instead of O(n²). Update the char's last index and the best window size. The key check is lastIndex >= left — stale entries from before the window must be ignored. O(n) time, O(min(n, alphabet)) space."
Follow-up questions
- •Why the `>= left` check?
- •How does this generalize to 'at most K distinct characters'?
- •Walk through with 'abba'.
- •How would you handle Unicode (emoji, surrogate pairs)?
Common mistakes
- •Not checking `lastIndex >= left` — left moves backward, wrong answer.
- •Updating best inside the if-branch — misses the case where no duplicate is found.
- •Off-by-one in window length (right - left vs right - left + 1).
- •Using indexOf inside the loop — O(n²).
Performance considerations
- •O(n) time, O(k) space where k = alphabet. Each index advances at most once.
Edge cases
- •Empty string.
- •Single char.
- •All identical chars.
- •Unicode: iterate by code points.
Real-world examples
- •Substring search optimizations.
- •Detecting unique-character runs in streaming data.