LeetCode #3 Medium

Longest Substring Without Repeating Characters

Return the length of the longest substring of s with no repeating characters.

Constraints
  • 0 <= s.length <= 10⁵
  • s consists of English letters, digits, symbols and spaces.
stringsliding-windowhash-table
Open on LeetCode ↗
02

Intuition

Longest substring without repeating characters finds the longest substring in which no character appears twice. The brute force checks every substring for duplicates at O(n³), and a sliding window reduces that to a single pass. The window holds a stretch of characters that are all distinct. Extending the right edge is always allowed unless the incoming character is already inside the window — and that is the whole condition: - When a repeat appears, the window must shrink from the left until the earlier copy of that character is excluded. The naive shrink advances the left pointer one step at a time. That is correct, and still linear overall since the left pointer never moves backwards. The optimised version jumps instead of stepping. Storing each character's last index in a map lets the left pointer move directly past the previous occurrence in one operation. That jump has a trap. The left pointer must move to max(left, lastIndex + 1), never backwards: In a string like "abba", when the second a is reached, its stored index is 0 — well behind the current left pointer. Jumping there unconditionally reopens a window containing a duplicate b, and the answer comes out too large. Taking the maximum prevents it. The length must be measured after the window is made valid, not before. Space is O(min(n, charset)), since the map holds at most one entry per distinct character rather than one per position.

How to spot this pattern

Two clues put you on a sliding window: the answer is a contiguous stretch, and the constraint is monotone — once a window is invalid, growing it right can't fix it. Then ask whether the left edge should crawl or jump. If you can store where the offender was, jump the left edge past it in O(1) instead of shrinking one character at a time.

03

Approach

Try it first

Before reading on: price up what enumerating every case costs here, then ask what makes a window invalid, and which pointer should move when it is. Aim for O(n) time and O(min(n, charset)) space.

1

Maintain a window of distinct characters

The window holds a stretch with no repeats. Extending right is always safe unless the incoming character is already inside it — that single condition drives the algorithm.

2

Shrink past the earlier copy

When a repeat appears, advance the left pointer until the previous occurrence is excluded. The window is then valid again.

3

Store last-seen indices

Map each character to its most recent index. This lets the left pointer jump directly past a repeat rather than stepping one position at a time.

4

Never move the left pointer backwards

Use left = max(left, lastIndex + 1). In "abba", the second a points back to index 0, and jumping there unconditionally reopens a window containing a duplicate.

5

Measure after validating

Record the window length once the duplicate is excluded. Measuring first counts a window that still contains a repeat.

6

Cost of the scan

Each character is visited once and the left pointer only advances, giving O(n) time with O(min(n, charset)) space — one map entry per distinct character.

04

Solution & live demo

▶1class Solution:
▶2 def lengthOfLongestSubstring(self, s):
▶3 last = {}
▶4 left = 0
▶5 best = 0
▶6 for right, ch in enumerate(s):
▶7 if ch in last and last[ch] >= left:
▶8 left = last[ch] + 1
▶9 last[ch] = right
▶10 best = max(best, right - left + 1)
▶11 return best
05

Common pitfalls

Moving left backwards on a stale index

✗ Wrong
if ch in last:
    left = last[ch] + 1
✓ Right
if ch in last and last[ch] >= left:
    left = last[ch] + 1

last keeps every character ever seen, including ones already outside the window. On "abba", when the second a arrives left is already 2, but last['a'] is 0 — dragging left back to 1 re-admits the b that was deliberately excluded and reports 3. The >= left test ignores repeats that have already fallen out of the window.

Recording the character after measuring the window

✗ Wrong
best = max(best, right - left + 1)
last[ch] = right
✓ Right
last[ch] = right
best = max(best, right - left + 1)

Here it happens to be harmless, but it's fragile ordering: the window is only genuinely valid once the current character has been registered. Update state, then measure — the same discipline that keeps the crawling variant correct.

06

Edge cases

All identical characters, e.g. 'bbbb'

Each new character forces left to follow right, so the window length stays 1.

Empty string

The loop never runs; best stays 0.

Repeat that lies left of the window

The last[ch] >= left guard ignores stale occurrences, so left never moves backward.

07

Complexity

Time
O(n)
Space
O(min(n, charset))
Each index enters and leaves the window once; map holds at most one entry per distinct character.