Longest substring without repeating characters

A sliding window with a dictionary of last-seen positions. Extend the right edge one character at a time; when a character repeats inside the current window, jump the left edge past its previous occurrence. Every character is visited once, so it is O(n).

Overview

The problem

Find the length of the longest substring containing no repeated character.

InputAnswerSubstring
"abcabcbb"3"abc"
"bbbbb"1"b"
"pwwkew"3"wke"
""0—
"dvdf"3"vdf"

Substring means contiguous. The "pwwkew" case is the one that catches people: the answer is "wke", not "pwke", because the latter is not contiguous.

The "dvdf" case catches a different mistake, covered below.

StringsCoding problemMedium

Step through it

What to watch

  • The right edge never goes backwards — that is what keeps it linear.
  • On a repeat, the left edge jumps rather than creeping.
  • A repeat that already fell off the left is ignored — watch abba.

Say this out loud

"Sliding window with a last-seen map. Right edge always advances; on a repeat inside the window the left edge jumps past the old occurrence. O(n) time, O(k) space in the alphabet."

Longest substring without repeating characters

Find the length of the longest substring with no repeated character.

Run it

The window printed at every step, then the same input run against brute force so the operation counts sit side by side, and finally the abba case with the guard removed.

1Python
Output
2Python
Output
3Python
Output

The sliding window

Expand the window rightwards; when a repeat appears, shrink from the left until it is gone.

4Python
Output

O(n) time — each pointer moves forward at most n times — and O(k) space where k is the alphabet size.

Two details carry the correctness:

last[ch] >= left is essential. Without it, a character seen before the current window would drag left backwards. That is the "dvdf" case: at the second d, the first d is at index 0 but the window has already moved past it, so left must not jump back.

Jumping rather than stepping. left = last[ch] + 1 moves in one operation. Stepping one at a time while removing from a set is also O(n) overall and is easier to get subtly wrong.

The set-based variant

Some prefer the explicit shrink loop, which reads more like the general sliding-window template:

5Python
Output

Same complexity. The while loop looks quadratic and is not: left only ever advances, so across the whole run it moves at most n times.

 Index mapSet with shrink loop
Left pointerJumpsSteps
Subtle bug riskThe >= left checkNone
Reads as the standard templateLessMore

Either is acceptable. Offering the set version first and then the jump optimisation is a reasonable way to present it.

Why brute force is quadratic

Checking every substring is O(n²) substrings, each needing a uniqueness test — O(n³) naively, O(n²) with a set per start. The waste is that each restart throws away everything the previous one learned.

The window keeps it. When the right edge advances, the answer for the new window is derived from the old one instead of recomputed.

The jump, and the guard

Store each character's last index. When the character at the right edge has been seen, the left edge moves to seen[ch] + 1 — one past the previous occurrence, in a single step.

The guard is the subtle part: only jump if seen[ch] >= start. Without it, a character last seen before the current window drags the left edge backwards, shrinking the window for no reason. "abba" is the shortest input that exposes it: at the final a, the earlier a is already outside the window.

Cost, and what to say about space

Time is O(n): the right edge advances n times and the left edge only ever moves forwards, so both pointers together make at most 2n moves. Space is O(k) in the size of the alphabet, not O(n) — the map holds one entry per distinct character.

Interviewers like the space answer because most candidates say O(n).

Tracing the awkward cases

"pwwkew" with the index-map version:

rightchlastleftWindowbest
0p{p:0}0p1
1w{p:0, w:1}0pw2
2w{p:0, w:2}2w2
3k…2wk2
4e…2wke3
5ww last at 2, ≥ left3kew3

"dvdf" — the case the >= left guard protects:

rightchleft beforeActionleft after
0d0New0
1v0New0
2d0last[d]=0 >= 0, jump1
3f1New1

Final window is "vdf", length 3. Without the guard, a later repeat of d would try to set left back to 1 when it had already advanced further — producing a window containing a duplicate and an answer that is too large.

Returning the substring

A common follow-up. Track where the best window started:

6Python
Output

One extra variable, captured only when a new best is found. Note that best_start must be recorded at the moment best improves, not afterwards — left will keep moving.

ProblemCondition maintained
No repeating charactersAll distinct
At most k distinct characterslen(counts) <= k
Exactly k distinctAt most k, minus at most k−1
Longest with at most one replacementwindow_size - max_count <= 1
Minimum window containing all of a patternAll required counts satisfied
Anagram substring of a patternCounts match exactly

The "at most k distinct" variant is the natural generalisation — this problem is the k = window-size case — and it uses a count dictionary rather than a set, shrinking while the number of distinct characters exceeds k.

The exactly k trick is worth knowing: "exactly k" equals "at most k" minus "at most k−1". Computing two at-most windows is easier than maintaining an exactly-k invariant directly.

The minimum window variant inverts the shrink condition: shrink while the window is still valid, recording before each shrink, because the goal is the smallest satisfying window rather than the largest. Getting that inversion wrong is the most common sliding-window error, and the symptom is an answer consistently too long or too short.

What it is testing

Do you recognise a sliding window? "Longest contiguous run satisfying a condition" is the signature.

Do you get O(n)? The nested while looks quadratic; explaining that each pointer advances at most n times is part of the answer.

Do you handle the stale-index case? "dvdf" is in every test suite for this problem.

Do you know it is contiguous? Answering 4 for "pwwkew" means you solved the subsequence problem instead.

Can you state the space complexity? O(min(n, k)) — bounded by the alphabet, not the string.

Recap in one screen

  • Expand the window rightwards; on a repeat, move the left edge past the previous occurrence.
  • Check last[ch] >= left or the left pointer moves backwards on characters outside the window.
  • O(n) time despite the inner loop, because both pointers only advance; O(k) space for the alphabet.
  • Substring means contiguous — "pwwkew" answers 3, not 4.
  • The family generalises to at-most-k distinct, minimum window, and anagram substrings, with the shrink condition inverted for minimum problems.

How the code works

The window printed at every step, then the same input run against brute force so the operation counts sit side by side, and finally the abba case with the guard removed.

How the code works

  1. if ch in seen and seen[ch] >= start:The guard. A repeat only matters while it is inside the window; an older occurrence has already fallen off the left edge and must be ignored.
  2. start = seen[ch] + 1One jump instead of walking the left edge forward one character at a time. Both are correct; this one keeps the whole scan clearly linear.
  3. seen[ch] = iStoring the index rather than a count is what makes the jump possible. A count-based window works too and needs the left edge to walk.
  4. i - start + 1The window length. Off-by-one here is the most common way to get an answer that is right on most inputs and wrong on the edges.

Change one thing

  • Run "abba" through both. The guard is the entire difference between 2 and 3.
  • Return the substring rather than the length by tracking start at the moment best improves — the usual follow-up.

Where this runs

Real CPython, compiled to WebAssembly and running on your own machine — nothing is uploaded. The first run takes a few seconds while the interpreter downloads; after that it is immediate. Need more room, or want to paste your own attempt? Use the Python compiler.

Check yourself

0 of 3

Answer without scrolling back up.

  1. Why is `seen[ch] >= start` needed as well as `ch in seen`?

  2. The time complexity is O(n) because:

  3. The space complexity is:

Cheat sheet

Longest substring without repeating characters

A sliding window with a dictionary of last-seen positions. Extend the right edge one character at a time; when a character repeats inside the current window, jump the left edge past its previous occurrence. Every character is visited once, so it is O(n).

INTERVIEW · vizlearn.in/interview/longest-substring-without-repeating-characters.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.