Longest Substring Without Repeating Characters
Given a string s, find the length of the longest substring that contains no repeated character — every character inside it is distinct. A substring is a contiguous block of s (not a scattered subsequence). Comparison is case-sensitive, and s may hold letters, digits, symbols, or spaces. Return the length as an integer; for the empty string return 0.
- 0 <= s.length <= 5 × 10⁴
- s consists of English letters, digits, symbols, and spaces
- Comparison is case-sensitive ('A' and 'a' are different)
s = "abcabcbb"3s = "bbbbb"1s = "pwwkew"3You are looking for the longest run of characters in s where nobody repeats — every character inside the run differs from all the others — and you return that run's length. The run must be contiguous: a substring, not a scattered subsequence.
The direct idea: try every start i and every end j, and check whether the slice between them is all-distinct.
best = 0
for i in range(len(s)):
for j in range(i, len(s)):
if all_unique(s[i:j+1]):
best = max(best, j - i + 1)There are about N² slices, and checking each for uniqueness scans it again — roughly N³ work. The waste is glaring: after confirming "abc" is all-distinct, we throw that away and re-verify it from scratch inside "abcd". We keep re-reading characters we already trust.
Keep a window — a left edge and a right edge — over the part of the string you are currently considering, with one rule: everything inside the window is distinct. Push right forward to grow the window; as long as the newcomer is not already inside, the window stays valid and you have a longer candidate. The moment the newcomer duplicates a character already in the window, the rule breaks — and the only cure is to pull left forward, dropping characters off the back until that duplicate is gone.
Here is the fact that makes this fast, worth believing rather than memorizing: the left edge never needs to move backward. Removing a character from the left can only remove a clash, never cause one; adding a character on the right can only cause a clash, never fix one. So the boundary between "valid" and "broken" slides one way only. Each edge sweeps the string once, and the whole thing is O(N).
Track the characters currently in the window with a set. Extend right; if that character is already present, walk left forward — removing from the set — until the duplicate leaves. Then measure the width.
seen = set()
left = 0
best = 0
for right in range(len(s)):
while s[right] in seen: # broken - shrink until the clash is gone
seen.remove(s[left])
left += 1
seen.add(s[right])
best = max(best, right - left + 1) # window is valid hereInstead of a set, remember the last index each character was seen at. On a duplicate you can send left straight past the old copy in one move rather than crawling.
last = {} # char -> last index seen
left = 0
best = 0
for right, ch in enumerate(s):
if ch in last and last[ch] >= left:
left = last[ch] + 1 # jump past the previous copy
last[ch] = right
best = max(best, right - left + 1)The last[ch] >= left guard matters: a character last seen long ago but already outside the window is not a real clash, so we must not yank left backward to it.
Two edges, both moving only forward, the window growing until it breaks a rule and shrinking just enough to restore it — that is the Variable Sliding Window, the workhorse behind every "longest / shortest contiguous stretch such that ..." question.
It is legal only when the rule behaves predictably at the edges: adding on the right can only push the window toward breaking, and removing on the left can only push it back toward valid. That one-directional behaviour (a kind of monotonicity) is exactly what lets left advance and never rewind. On any new problem, finding that property is the justification for using the pattern.
The skeleton, with the slots you fill per problem:
left = 0; state = empty
for right in range(n):
add s[right] to state # (b) expand
while window is INVALID: # (a) the rule
remove s[left] from state; left++ # (c) shrink
record answer from [left .. right] # (d) what & whenFour decisions change per problem: (a) what makes the window invalid, (b) how the newcomer updates your bookkeeping, (c) what releasing the left element does, (d) what you record and when. Here: invalid = a repeat exists, state = a set (or last-seen map), record = the max width.
There are two shapes of this template, and knowing which you are in saves you. For a longest valid window you keep the window valid and record its width — this problem, and Max Consecutive Ones III, Longest Repeating Character Replacement, Fruit Into Baskets. For a shortest valid window you shrink while still valid and record on the way — the very next problem, Minimum Size Subarray Sum, and later Minimum Window Substring.
Signals to reach for it: "longest" or "shortest" contiguous subarray/substring "such that <condition on the window's contents>" — distinct characters, sum ≥ target, at most k of something — with N large enough (10⁴–10⁵) that checking every window is out.
Where it breaks: if the rule is not one-directional. The classic trap is "exactly k" (exactly k distinct values): adding an element can flip a window from valid to invalid, and shrinking can flip it back, so left no longer has a single resting place and the plain window stalls. The fix is to rewrite "exactly k" as "at most k minus at most k−1" — you will meet it in Subarrays with K Different Integers.
Longest Substring
Duplicate Detection
We use a set or hash map to track characters in the current window. When a character is added that already exists, the window has become invalid.
Step-by-Step Logic
Step 1: First char 'a'. New character, window expands. Length 1.