Longest Substring Without Repeating Characters
Find the length of the longest run of consecutive characters in a string that contains no character twice.
Grow a window that is always free of repeats. Only the newest character can break that.
1def length_of_longest_substring(s):2 last = {}3 best = 04 left = 05 for right, ch in enumerate(s):6 if ch in last and last[ch] >= left:7 left = last[ch] + 18 last[ch] = right9 best = max(best, right - left + 1)10 return bestRead the 21 steps as text
- 1Grow a window that is always free of repeats. Only the newest character can break that.
- 2Add 'a' at index 0.
- 3Window is "a" โ 1 characters, a new best.
- 4Add 'b' at index 1.
- 5Window is "ab" โ 2 characters, a new best.
- 6Add 'c' at index 2.
- 7Window is "abc" โ 3 characters, a new best.
- 8Add 'a' at index 3.
- 9'a' is already in the window at index 0. Any window starting at or before 0 contains it twice โ jump the left edge to 1.
- 10Window is 3 long. Best stays 3.
- 11Add 'b' at index 4.
- 12'b' is already in the window at index 1. Any window starting at or before 1 contains it twice โ jump the left edge to 2.
- 13Window is 3 long. Best stays 3.
- 14Add 'c' at index 5.
- 15'c' is already in the window at index 2. Any window starting at or before 2 contains it twice โ jump the left edge to 3.
- 16Window is 3 long. Best stays 3.
- 17Add 'd' at index 6.
- 18Window is "abcd" โ 4 characters, a new best.
- 19Add 'b' at index 7.
- 20'b' is already in the window at index 4. Any window starting at or before 4 contains it twice โ jump the left edge to 5.
- 21Window is 3 long. Best stays 4.
The idea
Checking every substring is O(nยฒ) candidates, each needing a duplicate check. But those candidates overlap enormously, and the overlap is the opening.
Keep a window that is always valid โ always duplicate-free. Extend it to the right one character at a time. The only thing that can break validity is the character you just added, so that's the only repair you ever need to make.
When the new character was already inside the window, the window has to start after that character's previous position. Not one step right โ all the way past it, because any start before that still encloses the duplicate.
The left edge therefore never moves backwards, and the right edge never moves backwards. Two forward-only pointers over n characters is O(n) total, no matter how the string is shaped.
The approach
- 1Remember the most recent index of every character seen.
- 2Walk the right edge forward one character at a time.
- 3If that character was seen before, and that sighting is inside the current window, jump the left edge to just past it.
- 4Record the character's new position, then update the best length with the current window size.
Complexity
Each edge crosses the string once. The map holds at most one entry per distinct character, so k is the alphabet size โ often a constant.
Code
def length_of_longest_substring(s):
last = {}
best = 0
left = 0
for right, ch in enumerate(s):
if ch in last and last[ch] >= left:
left = last[ch] + 1
last[ch] = right
best = max(best, right - left + 1)
return bestWhat goes wrong
- โขForgetting the `last[ch] >= left` check. A duplicate that has already been left behind is not a duplicate any more, and jumping to it drags the left edge backwards, producing lengths that are too small.
- โขMoving the left edge one step at a time when a repeat is found. It's correct but reintroduces the quadratic worst case on strings like "aaaaโฆ".
- โขReporting the substring instead of its length, or the count of distinct characters โ both are different questions.