This is one of the most important sliding-window problems. The goal is not to generate every substring, but to maintain one valid window whose characters are all unique.
Problem 1: Longest Substring Without Repeating Characters
Problem description:
Given a string s, return the length of the longest substring that contains no repeated characters.
What we are solving actually: Brute force checks every possible substring and then tests whether it has duplicates, which is too expensive. The real idea is to keep one moving window that is always valid and repair it only when the newest character creates a repeat.
What we are doing actually:
- Expand the window with pointer
right. - Track the last seen index of each character.
- If the current character was already seen inside the current window, move
leftjust past that old position. - Update the best window length after each step.
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> lastSeen = new HashMap<>();
int left = 0;
int best = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (lastSeen.containsKey(c)) {
// Never move left backward; only shrink the window if the repeated
// character is still inside the current valid window.
left = Math.max(left, lastSeen.get(c) + 1);
}
lastSeen.put(c, right); // Record the newest position of this character.
best = Math.max(best, right - left + 1); // Current window [left..right] is valid.
}
return best;
}
}
Debug steps:
- print
left,right, current character, and current window length after each iteration - test
"abcabcbb","bbbbb", and"abba"because"abba"catches the left-moves-backward bug - verify the invariant that substring
s[left..right]never contains duplicate characters
Sliding Window Idea
The window is:
left= start of the current valid substringright= end of the current valid substring
As right moves, the window grows.
If a repeat appears, we do not restart from scratch.
We move left only as far as needed to restore the no-duplicate condition.
That is why this problem is a classic example of “expand, then repair” sliding-window logic.
Dry Run
Input: "abcabcbb"
-
right=0, char=awindow ="a", best =1 -
right=1, char=bwindow ="ab", best =2 -
right=2, char=cwindow ="abc", best =3 -
right=3, char=apreviousawas at index0moveleftto1window ="bca", best stays3 -
continue the same way
Final answer: 3
The algorithm does not rebuild substrings. It only updates boundaries and the last seen positions.
Why Math.max(left, lastSeen + 1) Is Required
This line prevents a subtle bug.
Consider "abba":
- after reading the second
b,leftmoves to index2 - when reading the final
a, the oldawas at index0
If we wrote:
left = lastSeen.get(c) + 1
then left would move backward to 1, which would make the window invalid again.
Math.max ensures left only moves forward.
ASCII Optimization Variant
If the input is guaranteed to be ASCII, an array can replace the map:
int[] last = new int[128];
Arrays.fill(last, -1);
That keeps the same algorithm and complexity, but improves constants.
The HashMap version is usually easier to explain because it works for general character sets too.
Common Mistakes
- moving
leftbackward by skippingMath.max - forgetting to update the latest index of the current character
- reconstructing substrings each step and accidentally adding extra
O(n)work - confusing substring with subsequence
Boundary Cases
- empty string -> answer
0 - all unique characters -> answer is full length
- all same characters -> answer
1 - repeated blocks like
"abcabcbb"-> answer comes from maintaining the best repaired window
Complexity
- Time:
O(n) - Space:
O(min(n, charset))
Key Takeaways
- this is a sliding-window problem with a “last seen index” map
- the key invariant is that the current window is always duplicate-free
leftmust move only forward, never backward
Comments