Algorithm

Longest Substring Without Repeating Characters

Sliding Window Pattern

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.

CONSTRAINTS
  • 0 <= s.length <= 5 × 10⁴
  • s consists of English letters, digits, symbols, and spaces
  • Comparison is case-sensitive ('A' and 'a' are different)
EXAMPLE 1
Input: s = "abcabcbb"
Output: 3
"abc" starting at index 0 is the longest substring without repeats (length 3). When the second "a" appears at index 3, the window shrinks to "bca".
EXAMPLE 2
Input: s = "bbbbb"
Output: 1
Every character is a repeat of the previous. The longest valid window is a single "b" (length 1).
EXAMPLE 3
Input: s = "pwwkew"
Output: 3
"wke" starting at index 3 has length 3. Note that "pwke" is not a substring (not contiguous) — it is a subsequence.
What if the string is empty?
Return 0. There are no characters and therefore no substrings to consider.
What types of characters can appear?
Any printable ASCII characters: uppercase and lowercase letters, digits, symbols, and spaces. Use a Map rather than a 26-entry array.
Does 'substring' mean the characters must be contiguous?
Yes. A substring is a contiguous block of characters. 'pwke' from 'pwwkew' is a subsequence, not a substring — and therefore does not count.
Should I return the length or the actual substring?
Return only the length of the longest valid substring.

You 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 all-substrings way, and why it drowns

The direct idea: try every start i and every end j, and check whether the slice between them is all-distinct.

python
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.

Two edges that only move forward

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).

Grow, and shrink only when forced

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.

python
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 here
Crucial Notewe record the width after the while-loop — only once the window is valid again. Measuring a broken window would count a substring that still has a repeat in it. Each character is added once and removed at most once, so despite the inner loop this is a single linear pass, not quadratic.
One refinement: jump instead of crawl

Instead 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.

python
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.

Worked Example:"abcabcbb"
- right=0 'a' → "a". best 1.
- right=1 'b' → "ab". best 2.
- right=2 'c' → "abc". best 3.
- right=3 'a' — last seen at 0, jump left to 1 → "bca". best 3.
- right=4 'b' — last seen at 1, jump left to 2 → "cab". best 3.
- right=5 'c' — last seen at 2, jump left to 3 → "abc". best 3.
- right=6 'b' — last seen at 4, jump left to 5 → "cb". best 3.
- right=7 'b' — last seen at 6, jump left to 7 → "b". best 3.
- Answer: 3.
The pattern, and how to reuse 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 & when

Four 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.

Interactive Strategy Visualization

Longest Substring

Unique Chars
Variable Window
a
0
b
1
c
2
a
3
b
4
c
5
b
6
b
7
Max Length
1
Current Size
1
O(N³) Brute Force
O(2N) Set Window
O(N) Last-Seen Jump