Given a string s, find the length of the longestsubstring without duplicate characters.
Example 1:
Input: s = "abcabcbb"
Output: 3
Explanation: The answer is "abc", with the length of 3. Note that "bca" and "cab" are also correct answers.
Example 2:
Input: s = "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.
Example 3:
Input: s = "pwwkew"
Output: 3
Explanation: The answer is "wke", with the length of 3.
Notice that the answer must be a substring, "pwke" is a subsequence and not a substring.
Constraints:
0 <= s.length <= 5 * 104
s consists of English letters, digits, symbols and spaces.
The simplest idea: check every possible substring, and for each one, verify it has no repeating characters using a Set. Track the longest valid one.
Checking substrings of "abcabcbb"
a
b
c
a
b
c
b
b
len=3 ✓
a
b
c
a
b
c
b
b
'a' repeats ✗
a
b
c
a
b
c
b
b
len=3 ✓
a
b
c
a
b
c
b
b
'b' repeats ✗
We must check O(n2) substrings, and verifying each takes O(n) time in the worst case, giving O(n3) overall. With a Set we can verify in O(1) amortized per character, bringing it to O(n2).
💡
The problem: We throw away useful information. When we find "abc" is valid and "abca" is not (because 'a' repeats), we start over from 'b'. But we already know "bca" is valid from the previous check! Can we reuse that knowledge?
Step 02
The Core Insight — Sliding Window
Instead of restarting from scratch, maintain a window [left, right] that always contains unique characters. Expand right to grow the window. When a duplicate is found, jump left past the previous occurrence.
The Sliding Window Technique
Use a HashMap to remember the last seen index of each character. When character c at index right was last seen at index k and k >= left, jump left to k + 1.
Expand right
Add character to window, record its index in the map.
Duplicate found
Jump left past the previous occurrence. The window shrinks but stays valid.
Update max
After each step, record maxLen = max(maxLen, right - left + 1).
⚡
Why HashMap over Set? A Set tells us whether a character is in the window, but to shrink correctly we would need to remove characters one by one from the left. A HashMap storing the last index lets us jump left directly to the right position in O(1) — no incremental removals needed.
Step 03
Algorithm Walkthrough
Let's trace through "abcabcbb" step by step. The blue bracket shows the current window [left..right]. The amber cell is the character being examined.
right = 0, char = 'a'
a
b
c
a
b
c
b
b
'a' not in map. Window: [0,0]. maxLen = 1
'a':0
right = 1, char = 'b'
a
b
c
a
b
c
b
b
'b' not in map. Window: [0,1]. maxLen = 2
'a':0'b':1
right = 2, char = 'c'
a
b
c
a
b
c
b
b
'c' not in map. Window: [0,2]. maxLen = 3
'a':0'b':1'c':2
right = 3, char = 'a' — DUPLICATE
a
b
c
a
b
c
b
b
'a' was at index 0, and 0 >= left(0). Jump left to 1. New window: [1,3]. maxLen = 3
'a':3'b':1'c':2
right = 4, char = 'b' — DUPLICATE
a
b
c
a
b
c
b
b
'b' was at index 1, and 1 >= left(1). Jump left to 2. New window: [2,4]. maxLen = 3
'a':3'b':4'c':2
right = 5, char = 'c' — DUPLICATE
a
b
c
a
b
c
b
b
'c' was at index 2, and 2 >= left(2). Jump left to 3. New window: [3,5]. maxLen = 3
'a':3'b':4'c':5
right = 6, char = 'b' — DUPLICATE
a
b
c
a
b
c
b
b
'b' was at index 4, and 4 >= left(3). Jump left to 5. New window: [5,6]. maxLen = 3
'a':3'b':6'c':5
right = 7, char = 'b' — DUPLICATE
a
b
c
a
b
c
b
b
'b' was at index 6, and 6 >= left(5). Jump left to 7. New window: [7,7]. maxLen = 3
'a':3'b':7'c':5
Result: maxLen = 3 (substring "abc")
🎯
Notice how left only moves forward — it never backtracks. Both left and right traverse the string at most once, giving us a single pass through the data. The map.get(c) >= left check is critical: it ignores stale entries from characters that are already outside the window.
Step 04
Edge Cases
Let's verify the algorithm handles tricky inputs correctly.
Empty string
s = ""
The loop never executes. Returns 0.
All same characters
s = "bbbbb"
Window never grows past size 1. Returns 1.
All unique characters
s = "abcdef"
No duplicates found, left stays at 0. Returns 6.
Special characters
s = "a b!a b"
Spaces and symbols are treated as characters. Returns 4 ("b!a " or " b!a").
Single character
s = "x"
Window [0,0] = length 1. Returns 1.
Duplicate far apart
s = "abcda"
'a' at index 4 was last at index 0, within window. left jumps to 1. Returns 4 ("bcda").
📝
The map.get(c) >= left guard is especially important when a character appears in the map but is already outside the current window (its stored index is less than left). Without this check, we would incorrectly shrink the window. For example, in "abba" when processing the final 'a', the map says 'a' was at index 0 but left is already at 2 — so index 0 is outside the window and should be ignored.
Step 05
Full Annotated Code
classSolution {
publicintlengthOfLongestSubstring(String s) {
// Map each character to the index where it was last seenMap<Character, Integer> map = newHashMap<>();
int maxLen = 0; // tracks our best answerint left = 0; // left boundary of the windowfor (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
// If c was seen before AND its last position is// within [left..right), the window contains a duplicateif (map.containsKey(c) && map.get(c) >= left) {
// Shrink window: move left past the previous occurrence
left = map.get(c) + 1;
}
// Record / update the position of character c
map.put(c, right);
// Update the maximum window length seen so far
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
}
funclengthOfLongestSubstring(s string) int {
// Map each character to the index where it was last seen
lastSeen := make(map[byte]int)
maxLen := 0// tracks our best answer
left := 0// left boundary of the windowfor right := 0; right < len(s); right++ {
c := s[right]
// If c was seen before AND its last position is// within [left..right), the window contains a duplicateif idx, ok := lastSeen[c]; ok && idx >= left {
// Shrink window: move left past the previous occurrence
left = idx + 1
}
// Record / update the position of character c
lastSeen[c] = right
// Update the maximum window length seen so farif right-left+1 > maxLen {
maxLen = right - left + 1
}
}
return maxLen
}
deflength_of_longest_substring(s: str) -> int:
# Map each character to the index where it was last seen
last_seen: dict[str, int] = {}
max_len = 0# tracks our best answer
left = 0# left boundary of the windowfor right, c inenumerate(s):
# If c was seen before AND its last position is# within [left..right), the window contains a duplicateif c in last_seen and last_seen[c] >= left:
# Shrink window: move left past the previous occurrence
left = last_seen[c] + 1# Record / update the position of character c
last_seen[c] = right
# Update the maximum window length seen so far
max_len = max(max_len, right - left + 1)
return max_len
use std::collections::HashMap;
implSolution {
pub fnlength_of_longest_substring(s: String) -> i32 {
// Map each character to the index where it was last seenlet mut last_seen: HashMap<u8, i32> = HashMap::new();
let mut max_len = 0; // tracks our best answerlet mut left: i32 = 0; // left boundary of the windowlet bytes = s.as_bytes();
for right in0..bytes.len() asi32 {
let c = bytes[right as usize];
// If c was seen before AND its last position is// within [left..right), the window contains a duplicateif let Some(&idx) = last_seen.get(&c) {
if idx >= left {
// Shrink window: move left past the previous occurrence
left = idx + 1;
}
}
// Record / update the position of character c
last_seen.insert(c, right);
// Update the maximum window length seen so far
max_len = max_len.max(right - left + 1);
}
max_len
}
}
functionlengthOfLongestSubstring(s: string): number {
// Map each character to the index where it was last seenconst map = newMap<string, number>();
let maxLen = 0; // tracks our best answerlet left = 0; // left boundary of the windowfor (let right = 0; right < s.length; right++) {
const c = s[right];
// If c was seen before AND its last position is// within [left..right), the window contains a duplicateif (map.has(c) && map.get(c)! >= left) {
// Shrink window: move left past the previous occurrence
left = map.get(c)! + 1;
}
// Record / update the position of character c
map.set(c, right);
// Update the maximum window length seen so far
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
💡
Why right - left + 1? The window spans indices [left, right] inclusive. That is (right - left + 1) characters. For example, [2, 4] contains indices 2, 3, 4 which is 4 - 2 + 1 = 3 characters.
Step 06
Interactive Demo
Enter any string and step through the sliding window algorithm. Watch the window expand and contract in real time.
⚙ Sliding Window Visualizer
Window [left, right]
—
Current Max Length
0
Window Characters
—
HashMap
{ }
Step 07
Complexity Analysis
Time
O(n)
Space
O(min(n, m))
The right pointer visits each index exactly once, and the left pointer only moves forward — it never backtracks. Every HashMap get, put, and containsKey operation is O(1) amortized, so the total time is O(n) where n is the string length. The HashMap stores at most one entry per distinct character, bounded by both n and the character set size m (e.g., 128 for ASCII), giving O(min(n, m)) space.
Approach Breakdown
BRUTE FORCE
O(n3) time
O(n) space
Two nested loops generate O(n2) substrings, and each substring is scanned character-by-character to check for duplicates -- up to O(n) per check. A temporary set holding up to n characters accounts for the space.
SET PER SUBSTR
O(n2) time
O(min(n, m)) space
For each starting index, a Set tracks seen characters and extends the substring in O(1) per step. This eliminates the inner scan but still restarts from each index, giving O(n2) total. The set holds at most min(n, m) entries where m is the charset size.
SLIDING WINDOW
O(n) time
O(min(n, m)) space
The right pointer visits each index once, and the left pointer only advances forward -- never backtracks. A HashMap stores the last-seen index for each character, enabling O(1) jumps. Total pointer movement across the entire run is bounded by 2n.
🎯
The sliding window amortizes to O(n) because each character enters and leaves the window at most once. The left pointer only moves forward, so across the entire algorithm, it makes at most n total jumps. This is the classic "two-pointer amortization" — even though a single jump can skip many indices, the total work across all jumps is bounded by n.
Coach Notes
Common Mistakes
Review these before coding to avoid predictable interview regressions.
Shrinking the window only once
Wrong move: Using `if` instead of `while` leaves the window invalid for multiple iterations.
Usually fails on: Over-limit windows stay invalid and produce wrong lengths/counts.
Fix: Shrink in a `while` loop until the invariant is valid again.