Sliding Window Fundamentals: Solving 'Longest Substring Without Repeating Characters'

Hero image for Sliding Window Fundamentals: Solving 'Longest Substring Without Repeating Characters'. Image by Andrii Khrystian.
Hero image for 'Sliding Window Fundamentals: Solving 'Longest Substring Without Repeating Characters'.' Image by Andrii Khrystian.

Longest Substring Without Repeating Characters is one of the few LeetCode problems that genuinely earns its reputation as a foundational slidingwindow question. It is not hard because the final code is especially exotic. It is hard because it is the first time a lot of people have to trust a moving window instead of restarting the work from scratch every time something goes wrong.

That is the real shift. We stop thinking in terms of "try every substring" and start thinking in terms of "keep one valid window, then adjust it carefully".


What the Problem is Really Asking

Given a string, we need the length of the longest contiguous substring that contains no repeated characters.

So for:

'abcabcbb'

the answer is 3, because the longest valid substring is 'abc'.

For:

'bbbbb'

the answer is 1, because every valid substring is just 'b'.

And for:

'pwwkew'

the answer is 3, because 'wke' works and 'pwke' does not. Contiguous really matters here.


The Brute‑Force Instinct and Why It Gets Old Fast

The first pass most people write is something like:

  • start at each index
  • extend the substring character by character
  • stop when a duplicate appears
  • track the longest length seen

That is fine as a way to prove the problem to ourselves. It is also wasteful, because neighbouring substrings overlap heavily. If we already know that 'abc' is valid, we should not need to rebuild that knowledge from scratch just because the left edge moves forward by one character.

That reuse is the whole point of the slidingwindow pattern.


The Window Invariant That Makes the Problem Calm Down

The useful rule is simple:

  • keep a window from left to right
  • make sure the window never contains duplicates

As right moves forward, the window may stop being valid. When that happens, we do not throw the whole window away. We move left forward just enough to restore the rule.

That is the invariant:

  • the current window always contains unique characters

Once that sentence feels trustworthy, the rest of the solution gets much easier to follow.


A clean teaching version with a Set

This is the version I would use to explain the slidingwindow pattern for the first time, because the invariant is visible in the data structure itself.

export const lengthOfLongestSubstring = (value: string): number => {  const seen = new Set<string>();  let left = 0;  let longest = 0;  for (let right = 0; right < value.length; right += 1) {    while (seen.has(value[right])) {      seen.delete(value[left]);      left += 1;    }    seen.add(value[right]);    longest = Math.max(longest, right - left + 1);  }  return longest;};

Why the Set version works

When we look at value[right], there are only two possibilities:

  • it is not already in the window, so we can safely expand
  • it is already in the window, so the window is no longer valid

If the second case happens, we shrink from the left until the duplicate disappears. Then we add the new character and carry on.

Take:

'abba'

Walk it through slowly:

  • window 'a' is valid
  • window 'ab' is valid
  • next character is 'b', which duplicates something already in the window
  • shrink from the left until that old 'b' is gone
  • continue from the restored valid window

That shrinking step is the part people often distrust at first. It feels as if we might be throwing away something useful. In reality, we are removing only what is necessary to restore validity. The window then keeps growing from the best surviving position.


Another Strong Answer: Jump with a Last‑Seen Map

Once the basic slidingwindow idea has landed, there is a slightly tighter implementation that many people prefer in production or interview code. Instead of removing characters one by one with a Set, we remember the most recent index where each character appeared.

That means we can sometimes move left in one jump.

export const lengthOfLongestSubstring = (value: string): number => {  const lastSeen = new Map<string, number>();  let left = 0;  let longest = 0;  for (let right = 0; right < value.length; right += 1) {    const character = value[right];    const previousIndex = lastSeen.get(character);    if (previousIndex !== undefined && previousIndex >= left) {      left = previousIndex + 1;    }    lastSeen.set(character, right);    longest = Math.max(longest, right - left + 1);  }  return longest;};

Why the Map Version Feels Cleverer

Suppose the window currently covers:

'abcdefg'

and we now see another 'e'.

The Set version removes characters from the left one by one until the old 'e' leaves the window. The Map version already knows where the old 'e' was, so it can say:

  • the left edge cannot stay before previousIndex + 1

That saves some bookkeeping and can feel more direct once we are comfortable with the invariant.


Which Solution is Best?

There are really two honest answers here.

If the goal is learning sliding windows properly, the Set version is the best explanation. It makes the invariant obvious:

  • the set is exactly the current duplicatefree window

If the goal is the strongest final implementation, I would choose the Map version. It keeps the same slidingwindow idea, but it jumps the left pointer more directly and usually reads as the more polished answer once you already trust the pattern.

So my view is:

  • best teaching version: Set
  • best final version: lastseen Map

That split is worth saying plainly because people often pretend there is only one respectable answer. There is not. Some versions teach better. Some versions optimise the bookkeeping better.


Why This is Still Linear

The Set version has a while loop inside a for loop, which can make it look quadratic at first glance. It is not.

The reason is that:

  • right only moves forward across the string once
  • left only moves forward across the string once

No character is added and removed infinitely often. The total pointer movement stays linear.

The Map version is also O(n) for the same reason. Each character is processed once, and the left boundary never moves backwards.


Common Mistakes

Resetting the Whole Window When a Duplicate Appears

That throws away useful information. We only need to move the left edge far enough to restore validity.

Moving left backwards in the map version

If a character last appeared before the current window started, that old index no longer matters. That is why the check is:

previousIndex >= left

Forgetting the Problem Asks for a Contiguous Substring

This is not "longest subsequence with unique characters". We are not allowed to skip around.


The Habit Worth Keeping

This problem matters because it teaches a reusable mental model:

  • define what makes the window valid
  • expand until that rule breaks
  • shrink until the rule is true again

That is the core move behind a lot of slidingwindow questions. Minimum Window Substring is harder, but it grows from the same basic discipline.

If you want the same problem from a slightly more JavaScriptfirst angle, Longest Substring Without Repeating Characters in JavaScript covers it from that direction.

The Part to Remember

  • The window is not arbitrary. It always represents a valid duplicatefree substring.
  • The Set version is the clearest way to learn the pattern.
  • The lastseen Map version is usually the stronger final answer once the pattern feels natural.

Longest Substring Without Repeating Characters is a very good first slidingwindow problem because it forces the right shift in thinking. We stop restarting. We keep one moving answer alive and adjust it carefully instead.


Categories:

  1. Algorithms
  2. Development
  3. Guides
  4. JavaScript
  5. LeetCode
  6. TypeScript