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

Longest Substring Without Repeating Characters is one of the few LeetCode problems that genuinely earns its reputation as a foundational sliding‑window 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 sliding‑window pattern.
The Window Invariant That Makes the Problem Calm Down
The useful rule is simple:
- keep a window from
lefttoright - 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 sliding‑window 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 sliding‑window 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 duplicate‑free window
If the goal is the strongest final implementation, I would choose the Map version. It keeps the same sliding‑window 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: last‑seen
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:
rightonly moves forward across the string onceleftonly 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 >= leftForgetting 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 sliding‑window questions. Minimum Window Substring is harder, but it grows from the same basic discipline.
If you want the same problem from a slightly more JavaScript‑first 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 duplicate‑free substring.
- The
Setversion is the clearest way to learn the pattern. - The last‑seen
Mapversion is usually the stronger final answer once the pattern feels natural.
Longest Substring Without Repeating Characters is a very good first sliding‑window problem because it forces the right shift in thinking. We stop restarting. We keep one moving answer alive and adjust it carefully instead.
Related Articles

Using Middleware in Next.js for Route Protection. 
Longest Substring Without Repeating Characters in JavaScript. Longest Substring Without Repeating Characters in JavaScript

Creating and Dispatching Custom Events in JavaScript. Creating and Dispatching Custom Events in JavaScript

GEO vs. SEO: Where They Overlap, and Where They Don't. GEO vs. SEO: Where They Overlap, and Where They Don't

Angular Standalone Components: Do We Still Need Modules? Angular Standalone Components: Do We Still Need Modules?

LeetCode: The 'Kth Smallest Element in a BST' Problem. LeetCode: The 'Kth Smallest Element in a BST' Problem

Preview Mode in Next.js with a Headless CMS. Preview Mode in Next.js with a Headless CMS

React Error Boundaries Explained. React Error Boundaries Explained

Multi‑Source BFS: Solving the 'Rotting Oranges' Problem. Multi‑Source BFS: Solving the 'Rotting Oranges' Problem

Browser vs. Node.js in JavaScript: Why Code Works in One and Fails in the Other. Browser vs. Node.js in JavaScript: Why Code Works in One and Fails in the Other

The Fetch API for Beginners: Get, Post, JSON, and Errors. The Fetch API for Beginners: Get, Post, JSON, and Errors

Extends and super in JavaScript Classes. extendsandsuperin JavaScript Classes