Stage 1: Foundations·Two Pointers & Sliding Window
Medium#36 min readSliding WindowHashingLeetCode

Longest Substring Without Repeating Characters

Last-Seen Sliding Window

Your first sliding window. When do you shrink vs expand?

Published Apr 15, 2026

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.

Input
s = "abcabcbb"
Output
3
Explanation
The answer is "abc", with length 3.
Input
s = "bbbbb"
Output
1
Explanation
The answer is "b".
Input
s = "pwwkew"
Output
3
Explanation
The answer is "wke".

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

TimeO(n)
SpaceO(1)
Recommended

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
    }
}
Rust · runnable

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":

rightcharleft beforeactionbest
0a0extend to [a]1
1b0extend to [ab]2
2c0extend to [abc]3
3a0jump left to 13
4b1jump left to 23
5c2jump left to 33
6b3jump left to 53
7b5jump left to 73

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 left only jumps forward.
  • Large alphabet can use a HashMap / Map instead of a fixed array if the input is not limited to ASCII.
💡
Tip

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.

More in Two Pointers & Sliding Window