LC 3·Tier Two·4 min read·Run it here·Solve it on LeetCode

Longest Substring Without Repeating Characters

Sliding window: grow the right edge one character at a time, and when a repeat appears, jump the left edge just past its previous position.

Pattern
Sliding window — grow the right edge, jump the left edge past the last repeat
Difficulty
Medium
Time
O(n)
Space
O(k)

Given a string, return the length of the longest substring (contiguous) with no repeated character. (LC 3)

This is the sliding window in its cleanest form, and nothing else in the circuit teaches it. Checking every substring is O(n³) done naively, and O(n²) with a set per start. The window gets it to one pass.

The recipe

Say before you type: "I keep a window with no repeats, defined by a left and a right edge. I move the right edge one character at a time. If that character is already inside the window, I jump the left edge to just past where I last saw it. Either way I record the window's length."

  1. Keep a map from character to the last index you saw it at.
  2. Move right across the string.
  3. If the character was last seen at an index that is inside the window (>= left), set left = lastSeen + 1.
  4. Record lastSeen[char] = right.
  5. Update the best with right - left + 1.

The code

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

  for (let right = 0; right < s.length; right++) {
    const ch = s[right];
    const prev = lastSeen.get(ch);
    if (prev !== undefined && prev >= left) left = prev + 1; // only if it is inside the window
    lastSeen.set(ch, right);
    best = Math.max(best, right - left + 1);
  }

  return best;
}

Why this shape

The window [left, right] always holds distinct characters. When right meets a repeat, you do not need to slide the left edge one step at a time, because the map already says where the repeat is: everything up to and including its old position has to go. Jumping there directly is why the whole thing is one pass.

Both edges only move forward, so each index is visited at most twice, which is O(n).

Complexity

CostBecause
TimeO(n)right visits each character once, and left only moves forward
SpaceO(k)The map holds at most one entry per distinct character, k of them

Traps

  • The left edge must never move backwards. "abba" is the classic. When you reach the final a, the map says it was last seen at index 0, which is behind the window (left is already 2). Without the prev >= left check you would drag left back to 1 and report a window of 3 instead of 2. "tmmzuxt" breaks it the same way.
  • Restarting the window at a repeat. Clearing everything when you see a duplicate is tempting. "dvdf" shows why it fails: the best window is "vdf", which starts inside the old one.
  • Subsequence versus substring. "pwwkew" is 3 ("wke"), not 4. Counting distinct characters, new Set(s).size, answers a different question.
  • The off-by-one. The window length is right - left + 1, not right - left.
  • Unicode. s[right] indexes UTF-16 code units, so an emoji counts as two. If the prompt allows them, iterate with Array.from(s) instead. Mentioning it is the kind of thing that gets noticed.

The pattern this trains

A contiguous range with a property that breaks when you extend it is a sliding window. Grow the right edge to include more, shrink the left edge to restore the property. It is the same two-pointer move in Minimum Window Substring (76), Longest Repeating Character Replacement (424), Permutation in String (567) and Max Consecutive Ones III (1004). The trigger words are longest, shortest or at most k attached to a substring or subarray.