HARD Lab · Strings · 10

3. Longest Substring Without Repeating Characters

Find the length of the longest substring with no repeated characters by keeping a sliding window whose left edge jumps past any duplicate the moment R would let one in — one pass, O(n).

MediumSliding WindowHash SetTypeScript

PROBLEM What we're solving

Given a string s, return the length of the longest substring that contains no duplicate characters. s="abcabcbb": the answer is 3 — the window "abc" (indices 0–2). When R hits the second 'a' at index 3, the window must shrink. For s="bbbbb" the answer is 1; for s="pwwkew" it is 3 ("wke").

KEY IDEA Keep a window of unique chars; jump on collision

Insight → maintain a window [L, R] that always contains unique characters. When s[R]would create a duplicate, don't slide L one step at a time — jump it directly past the previous occurrence of the duplicate. A Map<char, lastIndex> makes that jump O(1), so the whole pass is O(n).

RECIPE Expand right, jump left on collision

  • 0 · Init. L=0, best=0, empty lastSeen map (char → most recent index).
  • 1 · Expand right. Move R through every character. This is the driver — we always try to grow.
  • 2 · Detect collision. If lastSeen has s[R] at index prev and prev >= L(it's still inside our window), jump: L = prev + 1. The >= L guard prevents accidentally pulling L backwards if the map has a stale entry from before the current window.
  • 3 · Update map and best. Record lastSeen[s[R]] = R (always, whether or not we jumped). Then best = max(best, R−L+1).
Classic confusion → forgetting the prev >= L guard. If a character appeared before the current window starts, its stale map entry must be ignored — otherwise L could move backwards and allow duplicates back in. Always confirm the collision is inside the live window.

COST Complexity & alternatives

Brute-force all substrings
O(n²)
Check every start; O(n) uniqueness check each.
Sliding window + map
O(n)
Each character visited at most twice (enter + evict).

Space note

The map holds at most min(n, |alphabet|) entries — O(1) for a fixed alphabet (e.g. ASCII 128). A Set works too but requires sliding L one step at a time until the duplicate leaves, giving the same O(n) total but more iterations per collision.

Pattern transfer → this window-plus-last-seen-index pattern generalises to Longest Substring with At Most K Distinct Characters, Minimum Window Substring, and Permutation in String. Whenever a problem asks for the longest/shortest contiguous subarray satisfying a uniqueness or frequency constraint, reach for a sliding window with a hash map.

RUN IT Slide right, shrink on repeat

step 0 / 14
STARTBegin with empty window. L=0, scan R left-to-right.
s =a0b1c2a3b4c5b6b7
State
0
L
R
(empty)
window
0
best
inside windowcurrent R (right pointer)repeat collisionbest length
slowfast

TYPESCRIPT The solution, annotated

lengthOfLongestSubstring.ts
function lengthOfLongestSubstring(s: string): number {
  const lastSeen = new Map<string, number>();
  let L = 0;
  let best = 0;

  for (let R = 0; R < s.length; R++) {
    const prev = lastSeen.get(s[R]);
    if (prev !== undefined && prev >= L) {
      // s[R] repeats inside the current window — jump L past it
      L = prev + 1;
    }
    lastSeen.set(s[R], R);          // record / update last-seen index
    best = Math.max(best, R - L + 1);
  }

  return best;
}

Reading it block by block

Line 2 — the map. lastSeen maps each character to the last index where it appeared. This is what lets us jump L in O(1) instead of creeping forward step by step.
Lines 3–4 — two pointers. L is the left edge of the current valid window; best records the maximum window length seen so far.
Lines 6–9 — collision check and jump. When s[R] already appears inside [L, R-1], move L directly to prev + 1. The prev >= L guard is critical: without it a character last seen before the window would drag L backwards.
Lines 10–11 — update and record. Always store the newest index for s[R], even after a jump (the character is now the rightmost element of the window). Then update best.
Complexity → O(n) time — R advances exactly n times; L only moves right so across all iterations it also advances at most n times → 2n total pointer moves. O(min(n, |alphabet|)) space for the map.

INTERVIEWFollow-ups they'll ask

  • "Return the substring itself, not just its length?" Track bestL alongside best and return s.slice(bestL, bestL + best).
  • "At most k distinct characters?" Replace the collision check with a frequency map; shrink L whenever map.size > k.
  • "What changes with duplicates allowed (at most 2 of each)?" Track counts; shrink only when any count exceeds 2.
  • "Can you do it with a Set instead of a Map?" Yes — delete from the set while s[L] !== s[R] advancing L. Same O(n) amortised, but each collision can require O(window) inner loop steps; the map's O(1) jump is cleaner.
  • "Brute force first?" Try every starting index; use a Set to test uniqueness until a repeat is found — O(n²). Describe it, then explain how the map eliminates re-scanning by remembering where to restart.

MNEMONIC The one-liner

"Grow from the right; the moment a twin enters, jump the left edge past the older twin."

TRIGGERS When you see ___ → reach for ___

"longest substring / subarray with no repeats"sliding window + last-seen map
need O(1) jump of left pointer on collisionMap<char, lastIndex> + L = prev+1
"at most k distinct characters"sliding window + frequency map, shrink when size > k
stale map entry outside windowguard: only jump when prev >= L

SKELETON The reusable shape

skeleton.ts
const lastSeen = new Map<string, number>();
let L = 0, best = 0;

for (let R = 0; R < s.length; R++) {
  const prev = lastSeen.get(s[R]);
  if (prev !== undefined && prev >= L) {
    L = prev + 1;           // shrink: jump past the duplicate
  }
  lastSeen.set(s[R], R);   // update last-seen position
  best = Math.max(best, R - L + 1);
}
return best;

FLASHCARDS Tap to flip

What does the map store, and why?
Each character's most recent index. It lets L jump past a collision in O(1) instead of creeping forward one step at a time.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
What is the length of the longest substring without repeating characters for "abcabcbb"?
QUESTION 02
Why must we check "prev >= L" before jumping L past a duplicate?
QUESTION 03
What is the time complexity of the sliding-window + last-seen-map approach?
QUESTION 04
A Set-based approach slides L forward one step at a time until the duplicate leaves. Compared to the map approach, it is:
QUESTION 05
After processing s="abcabcbb" up through index R=3 (the second 'a'), where does L land?
QUESTION 06
To extend this problem to "longest substring with at most k distinct characters", which change is needed?
QUESTION 07
Space complexity for the lastSeen map when the input is a string of arbitrary Unicode?