Longest Substring Without Repeating Characters
Last-Seen Sliding Window
Your first sliding window. When do you shrink vs expand?
Problem
Given a string s, return the length of the longest substring without repeating characters.
A substring is a contiguous block of characters inside the string.
Intuition
The naive solution checks every substring and tests whether it contains duplicates. That is quadratic at best, and the duplicate check inside each window can make it worse.
The key observation is that a duplicate only matters if it lies inside the current window. If the same character appeared earlier but is now outside the window, it is irrelevant.
That means we do not need to "search the whole window" every time. We only need to remember the most recent position of each character. When we see a repeated character, we jump the left edge of the window just past the previous occurrence.
That jump is the entire trick. The left pointer never moves backward, so the window expands and contracts in linear time.
Approach
Last-Seen Sliding Window
O(n)O(1)Maintain a window [left, right] with no duplicate characters and a table of
the last index where each character was seen.
When the character at right was seen inside the current window, move left
to one position after that previous index. Then update the last-seen index and
record the current window length.
Because the alphabet is fixed for this problem, a small array is enough. There is no need for a hash map unless you want the code to generalize beyond ASCII.
impl Solution {
pub fn length_of_longest_substring(s: String) -> i32 {
let bytes = s.as_bytes();
let mut last = [-1i32; 128];
let mut left = 0usize;
let mut best = 0usize;
for (right, &b) in bytes.iter().enumerate() {
let idx = b as usize;
if last[idx] >= left as i32 {
left = (last[idx] + 1) as usize;
}
last[idx] = right as i32;
best = best.max(right - left + 1);
}
best as i32
}
}function lengthOfLongestSubstring(s: string): number {
const last = new Array<number>(128).fill(-1);
let left = 0;
let best = 0;
for (let right = 0; right < s.length; right++) {
const idx= s.charCodeAt(right);
left= Math.max(left, last[idx] + 1);
last[idx]= right;
best= Math.max(best, right - left + 1);
}
return best;
}The invariant is simple: everything in [left, right] is unique. If the next
character breaks that invariant, move left just far enough to restore it.
Trace
For s = "abcabcbb":
| right | char | left before | action | best |
|---|---|---|---|---|
| 0 | a | 0 | extend to [a] | 1 |
| 1 | b | 0 | extend to [ab] | 2 |
| 2 | c | 0 | extend to [abc] | 3 |
| 3 | a | 0 | jump left to 1 | 3 |
| 4 | b | 1 | jump left to 2 | 3 |
| 5 | c | 2 | jump left to 3 | 3 |
| 6 | b | 3 | jump left to 5 | 3 |
| 7 | b | 5 | jump left to 7 | 3 |
The window never rescans old characters. Each index is processed once.
Edge cases
- Empty string returns
0. - All characters identical returns
1. - Duplicate outside the current window does not force a shrink, because
leftonly jumps forward. - Large alphabet can use a
HashMap/Mapinstead of a fixed array if the input is not limited to ASCII.
This is the standard sliding-window template: expand right, repair the invariant, record the best answer. Once you internalize that loop, #76 Minimum Window Substring becomes the same pattern with a different invariant.
Takeaway
Sliding window is not "two pointers with a vibe." It is a loop that maintains an explicit invariant.
For this problem, the invariant is "no duplicate characters inside the window." Once that is true, the longest valid substring seen so far is easy to track, and every character only enters and leaves the window once.
Converging pointers from both ends. Simple but builds the muscle memory.
The ‘hard’ sliding window — but if you understood #3, this is the same pattern with a counter.
Why does moving the left pointer increase the sum? Understand the proof.
More in Two Pointers & Sliding Window
Converging pointers from both ends. Simple but builds the muscle memory.
Why does moving the left pointer increase the sum? Understand the proof.
Fix one element, two-pointer the rest. Duplicate handling is the real challenge.
The ‘hard’ sliding window — but if you understood #3, this is the same pattern with a counter.