How do you find the longest substring without repeating characters?
Use a sliding window. Expand the right end one character at a time and remember the last index where each character was seen. If the new character already appears inside the window, move the left end just past its previous position. Track the largest window size. This is O(n) time.
Explain it simply
Picture a train with coaches labelled by letters, and you want the longest run of consecutive coaches where no letter repeats. You hold a frame that you slide along the train. You keep stretching the right edge of the frame to include the next coach. The moment a letter appears twice inside the frame, you pull the left edge forward just past the earlier copy of that letter, so the frame is valid again. Along the way, you note the widest the frame ever got.
You never go back and restart from scratch. Each coach enters the frame once and leaves at most once. That is why it is fast.
Line by line
function lengthOfLongestSubstring(s) {
const lastSeen = new Map(); // character -> last index where it appeared
let left = 0; // start of the current window
let best = 0;
for (let right = 0; right < s.length; right++) {
const ch = s[right];
// If ch was seen inside the current window, jump left past it
if (lastSeen.has(ch) && lastSeen.get(ch) >= left) {
left = lastSeen.get(ch) + 1;
}
lastSeen.set(ch, right); // update the latest position of ch
best = Math.max(best, right - left + 1); // window is s[left..right]
}
return best;
}
console.log(lengthOfLongestSubstring("abcabcbb")); // Output: 3
console.log(lengthOfLongestSubstring("bbbbb")); // Output: 1
console.log(lengthOfLongestSubstring("pwwkew")); // Output: 3
console.log(lengthOfLongestSubstring("abba")); // Output: 2
console.log(lengthOfLongestSubstring("")); // Output: 0
console.log(lengthOfLongestSubstring("dvdf")); // Output: 3Trace for "pwwkew":
| right | char | left after check | window | best |
|---|---|---|---|---|
| 0 | p | 0 | p | 1 |
| 1 | w | 0 | pw | 2 |
| 2 | w | 2 (old w at 1) | w | 2 |
| 3 | k | 2 | wk | 2 |
| 4 | e | 2 | wke | 3 |
| 5 | w | 3 (old w at 2) | kew | 3 |
The answer is 3 ("wke" or "kew"). Note "pwke" is a subsequence, not a substring, so it does not count.
How it works
- The window
s[left..right]always contains distinct characters. That is the invariant. - When a repeat arrives, all positions up to and including its previous copy can never be part of a valid window ending at
right, soleftjumps straight past it. - The check
lastSeen.get(ch) >= leftmatters. Look at"abba": when the secondaarrives at index 3,leftis already 2 (it moved past the firstb). The oldaat index 0 is outside the window, soleftmust not move back to 1. Without this check the answer would wrongly be 3.
Approaches, from slow to fast:
- Brute force: check every substring and test if its characters are unique. O(n³), or O(n²) with a set.
- Sliding window with a set: move
leftforward one step at a time, removing characters, until the repeat is gone. O(n), each character added and removed at most once. - Sliding window with last-seen indexes (above):
leftjumps directly. Still O(n), with fewer steps.
Complexity
| Approach | Time | Extra space |
|---|---|---|
| Brute force | O(n³) | O(k) |
| Sliding window with set | O(n) | O(k) |
| Sliding window with last index map | O(n) | O(k) |
k is the number of distinct characters possible, at most the size of the alphabet (for example 128 for ASCII), so the space is often described as O(min(n, k)).
Common mistakes
- Letting
leftmove backwards, as in the"abba"case. Either check that the old index is inside the window, or writeleft = Math.max(left, lastSeen.get(ch) + 1). - Off-by-one on the length. The window from
lefttorightinclusive hasright - left + 1characters. - Confusing substring (contiguous) with subsequence (any order-preserving pick).
- Restarting the scan from
lefteach time a repeat is found, which turns the solution back into O(n²).
What to say in the interview
"I use a sliding window with two pointers and a map from each character to the last index where I saw it. I move the right pointer through the string. If the current character was last seen at or after left, I move left to one past that index, so the window stays free of repeats. Then I record the character's new index and update the best length as right minus left plus 1. Each character is processed once, so it is O(n) time and O(k) space, where k is the alphabet size."
Follow-up questions
- How would you return the substring itself, not just its length?
- What is the longest substring with at most k distinct characters?
- How do you find the smallest window that contains all characters of another string?
- Why is the sliding window O(n) when there is a loop inside a loop in the set version?
Go deeper
Read these once the basics above feel easy.
To return the substring itself, store the left value whenever best improves, then return s.slice(bestLeft, bestLeft + best).
The same sliding window pattern solves a family of problems. Expand the right edge, and shrink from the left whenever the window breaks a rule. For "at most k distinct characters", keep a count of each character in the window and shrink while the map has more than k keys.
The set-based version has a while loop inside the for loop, yet it is O(n), not O(n²). The reason is amortised analysis: left only ever moves forward and never passes right, so across the whole run the inner loop executes at most n times in total. Counting total pointer movements, rather than multiplying loop bounds, is the standard way to prove sliding window and two-pointer solutions are linear.
If the input is known to be ASCII, a fixed array of 128 integers initialised to -1 is faster than a Map, and makes the O(1) space bound explicit.