Difficulty: Medium Pattern: Sliding Window LeetCode: https://leetcode.com/problems/longest-substring-without-repeating-characters/
[left..right] that always holds a valid substring; expand right, jump left when a repeat appears. glossaryGiven a string s, find the length of the longest substring (contiguous) that
contains no repeating characters.
Signature:
int lengthOfLongestSubstring(String s)
Example 1:
Input: s = "abcabcbb"
Output: 3
(The answer is "abc", length 3.)
Example 2:
Input: s = "bbbbb"
Output: 1
Example 3:
Input: s = "pwwkew"
Output: 3
(The answer is "wke", length 3. Note "pwke" is a subsequence, not a substring.)
Picture a magnifying glass sliding left-to-right along a row of letters. Inside the glass you want every letter to be different. When the glass reaches a letter that is already inside it, you slide the glass’s left edge past the earlier copy so the duplicate disappears, then keep going.
Two terms to nail down. A substring is a contiguous run of neighbours (like “abc” inside
“abcabcbb”), not a subset that skips letters. A window [left..right] means “the letters
at positions left, left+1, …, right”.
Smallest example, s = "abcabcbb", walking right from 0:
left past it, to 1. Window “bca”. Length 3.left to 2. Window “cab”. Length 3.left to 3. Window “abc”. Length 3.left to 5. Window “cb”. Length 2.left to 7. Window “b”. Length 1.Best length reached: 3.
General rule: keep a hash map from each character to its most recent index. For each new
character at right: if we have seen it before AND its last position is still inside the
window (>= left), jump left to just past that position. Then record the character’s new
position. The window is now guaranteed repetition-free, so update the best length.
The invariant we maintain is: at the start of each step, the window [left..right-1]
has no repeated letters. Adding the character at right either keeps the invariant (new
letter) or breaks it (a repeat inside the window) – and we immediately restore it by
jumping left past the old copy, which removes the duplicate. We jump instead of shrinking
one step at a time because the map tells us exactly where the duplicate sits. The check
prev >= left matters: the map remembers every character ever seen, but only an occurrence
still inside the window forces a jump – one that left has already passed must be ignored
(see the "abba" dry-run below).
This is the classic variable-size sliding window. 0219 Contains Duplicate II reuses the “hash structure tracks the recent window” reflex with a fixed-width window, and 0424 Longest Repeating Character Replacement uses a window with a richer validity check.
Pause and pick before expanding. A wrong first guess teaches more than a fast right one.
Q1 (recall). What does the hash map store for each character?
Q2 (comprehend). On s = "abba", when right reaches the final 'a', why does left stay put instead of jumping back?
left, so the prev >= left check is falsefunction lengthOfLongestSubstring(s):
last-seen = empty map from character to index
left = 0
best = 0
for right from 0 to len(s)-1:
ch = s[right]
if ch in last-seen and last-seen[ch] >= left:
left = last-seen[ch] + 1 # jump past the previous occurrence
last-seen[ch] = right
best = max(best, right - left + 1)
return best
The check last-seen[ch] >= left is essential: the map remembers every character ever
seen, but only an occurrence inside the current window forces a jump. An occurrence
to the left of left is no longer in the window and must be ignored.
import java.util.HashMap;
import java.util.Map;
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 ch = s.charAt(right);
Integer prev = lastSeen.get(ch);
if (prev != null && prev >= left) {
left = prev + 1;
}
lastSeen.put(ch, right);
best = Math.max(best, right - left + 1);
}
return best;
}
}
A HashMap<Character, Integer> stores the most recent index of each character; this lets
left jump straight past a repeat rather than creeping one step at a time (which would
still be O(n) overall, but the jump is cleaner). prev is typed Integer so that
null (“never seen”) is distinguishable from index 0. The guard prev >= left ignores
occurrences that are already outside the window – without it, inputs like "abba"
mis-jump left backwards and produce wrong answers. best is updated every step because
every window after the (possible) jump is guaranteed to be repetition-free.
Time: O(n) -- `right` advances n times; each map op is O(1) amortized.
Space: O(k) -- the map holds at most k entries, where k is the alphabet size
(min(n, charset size); 65536 for full Java char).
Step-by-step on s = "abcabcbb":
| right | ch | prev (last-seen[ch]) | prev >= left? | left (after) | last-seen[ch]=right | best |
|---|---|---|---|---|---|---|
| init | - | - | - | 0 | - | 0 |
| 0 | a | null | - | 0 | a:0 | 1 |
| 1 | b | null | - | 0 | b:1 | 2 |
| 2 | c | null | - | 0 | c:2 | 3 |
| 3 | a | 0 | 0 >= 0 yes | 1 | a:3 | 3 |
| 4 | b | 1 | 1 >= 1 yes | 2 | b:4 | 3 |
| 5 | c | 2 | 2 >= 2 yes | 3 | c:5 | 3 |
| 6 | b | 4 | 4 >= 3 yes | 5 | b:6 | 3 |
| 7 | b | 6 | 6 >= 5 yes | 7 | b:7 | 3 |
Return 3.
A second trace on the tricky input s = "abba", where ignoring prev >= left would
break:
| right | ch | prev | prev >= left? | left | best | window |
|---|---|---|---|---|---|---|
| 0 | a | null | - | 0 | 1 | “a” |
| 1 | b | null | - | 0 | 2 | “ab” |
| 2 | b | 1 | 1 >= 0 yes | 2 | 2 | “b” |
| 3 | a | 0 | 0 >= 2 no | 2 | 2 | “ba” |
Without the prev >= left guard, step 3 would jump left to 1, shrinking the window
to “a” instead of correctly staying at “ba”.
Q1 (apply). Trace s = "abcb". What length is returned?
Q2 (analyze). If you deleted the prev >= left guard, on which input would the answer first go wrong?
Q3 (transfer). Suppose the question asked for the substring itself, not just its length. What one piece of bookkeeping would you add?
prev >= left check and jumping left backwards on inputs like
"abba" or "tmmzuxt".right - left instead of right - left + 1.HashSet and shrinking one index at a time – correct but slower to reason
about than the index-jump version.== to compare characters stored as boxed Character values; the map returns
an index, so this does not bite here, but it bites the analogous HashSet approach.