Topics › Sliding Window and Two Pointers › Medium Problems
Longest Substring Without Repeating Characters
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Given a string s, return the length of the longest substring in which no character appears more than once. A substring is a contiguous block of characters; picking characters from different places does not count.
Constraints: 0 ≤ |s| ≤ 5 × 10^4. The string may contain English letters, digits, symbols and spaces, and a space is a character like any other.
Example 1
s = "abcabcbb"
answer = 3 // "abc" (several other windows of length 3 also work)
Example 2
s = "pwwkew"
answer = 3 // "wke"; "pwke" has distinct letters but is not contiguous
Example 3
s = ""
answer = 0 // no characters, so the longest valid substring is empty
Approach 1: Brute force
Intuition. Try every starting index i and extend the substring to the right one character at a time, keeping the characters seen so far in a set. The first time we meet a character already in the set, we can stop extending from this start: every longer substring beginning at i contains that same repeated pair, so none of them can be valid. The longest clean extension over all starts is the answer.
This already improves on the most naive idea, which would list all O(n^2) substrings and check each one in O(n), for O(n^3) in total.
Algorithm.
- For each start
i: create an empty set. - For each
jfromi: ifs[j]is already in the set, stop; otherwise add it and updatebestwithj - i + 1. - Return
best.
static int bruteForce(String s) {
int n = s.length(), best = 0;
for (int i = 0; i < n; i++) {
Set<Character> seen = new HashSet<>();
for (int j = i; j < n; j++) {
if (!seen.add(s.charAt(j))) break; // s[j] already in s[i..j-1]: every longer substring repeats too
best = Math.max(best, j - i + 1);
}
}
return best;
}
Complexity. Time O(n^2) in general. There is a finer bound: the inner loop can never run for more than Σ + 1 steps, where Σ is the alphabet size, because after Σ distinct characters the next one must repeat. So the time is O(n × min(n, Σ)). Space O(min(n, Σ)) for the set.
The weakness is that after a repeat we throw away everything we learned and restart from i + 1, re-adding characters we had just checked.
Approach 2: Better, sliding window with a set
Intuition. Keep a window s[left..right] that is always free of repeats, together with a set of its characters. Move right forward one step at a time. If the new character c is not in the window, simply add it. If it is, the window has to lose its older copy of c; the only way to remove a character from a contiguous window is from the left, so we drop characters from the left until c is gone.
Why is this correct? For every right, the loop leaves left at the smallest index for which s[left..right] has no repeats, so the window is the longest valid substring ending at right. The overall answer is the longest of those. And left never needs to move back: if s[l..right] contains a repeat, so does s[l..right + 1], which contains it.
Algorithm.
- Start with an empty set,
left = 0,best = 0. - For each
right: whiles[right]is in the set, removes[left]and doleft++. - Add
s[right]to the set and updatebestwithright - left + 1. - Return
best.
static int better(String s) {
Set<Character> window = new HashSet<>();
int left = 0, best = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
while (window.contains(c)) { // shrink until the old copy of c has left the window
window.remove(s.charAt(left));
left++;
}
window.add(c);
best = Math.max(best, right - left + 1);
}
return best;
}
Complexity. Time O(n): each index is added to the set once and removed at most once, so there are at most 2n set operations in total, even though there is a while loop inside the for loop. Space O(min(n, Σ)).
Approach 3: Optimal, jump the left edge with last-seen indices
Intuition. In Approach 2, when s[right] repeats, left walks forward one character at a time until it passes the previous copy. If we remember where that previous copy is, we can jump there in one step. So store, for each character, the index of its most recent occurrence. When s[right] = c was last seen at prev, the new window must start at prev + 1.
There is one trap. The map is never cleaned, so it still holds indices of characters that have already left the window. Such a stale index is smaller than left, and jumping to it would move left backwards and bring repeats back into the window. So we only jump when prev >= left, that is, when the old copy is really inside the window.
Algorithm.
- Start with an empty map
last,left = 0,best = 0. - For each
right, withc = s[right]:- If
lasthascat indexprevandprev >= left, setleft = prev + 1. - Store
last[c] = right. - Update
bestwithright - left + 1.
- If
- Return
best.
static int optimal(String s) {
Map<Character, Integer> last = new HashMap<>(); // character -> index of its latest occurrence
int left = 0, best = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
Integer prev = last.get(c);
if (prev != null && prev >= left) left = prev + 1; // jump past the old copy; never move left backwards
last.put(c, right);
best = Math.max(best, right - left + 1);
}
return best;
}
Dry run on s = "abba", the classic case for the stale-index trap:
| right | c | prev | prev ≥ left? | left after | window | best |
|---|---|---|---|---|---|---|
| 0 | a | - | - | 0 | “a” | 1 |
| 1 | b | - | - | 0 | “ab” | 2 |
| 2 | b | 1 | yes | 2 | “b” | 2 |
| 3 | a | 0 | no, stale | 2 | “ba” | 2 |
At right = 3, the stored index of a is 0, but the window already starts at 2. Setting left = prev + 1 = 1 would produce the window "bba" and the wrong answer 3. The guard keeps the answer at 2. An equivalent way to write the update is left = Math.max(left, prev + 1).
Complexity. Time O(n): exactly n iterations, each with a constant number of map operations. Space O(min(n, Σ)) for the map.
Approaches 2 and 3 have the same big-O; the difference is that 3 does one step per character instead of up to two. For a known small alphabet, replacing the HashMap with an int[128] (or int[256]) array filled with -1 makes both faster in practice by avoiding boxing; the map version shown here works for any char.
Edge cases and pitfalls
- Empty string. The answer is
0; none of the loops run. - Spaces and symbols.
" "has answer1, and"a b!a"has answer4(" b!a"). Do not filter out non-letters. - All characters equal.
"bbbbb"gives1: the window keeps collapsing to the newest character. - No repeats at all.
"abcdef"gives6; the window simply grows to cover the whole string. - Stale indices.
"abba"and"tmmzuxt"(answer5) both fail if the left edge is allowed to move backwards. - Substring, not subsequence. For
"pwwkew"the answer is3, not4; skipping over the secondwis not allowed. - Off-by-one in the length. The window
s[left..right]includes both ends, so its length isright - left + 1. - Characters outside ASCII. If you use a fixed-size array instead of a map, size it for the actual alphabet. An
int[128]throwsArrayIndexOutOfBoundsExceptionon'é'.
Related problems
- Longest Substring Without Repeating Characters is the same problem in the A2Z sliding-window track.
- Longest Repeating Character Replacement uses a window whose validity depends on character counts.
- Max Consecutive Ones III is the same “longest valid window” template on a binary array.
- Longest Substring With At Most K Distinct Characters relaxes “all distinct” to “at most
kdistinct”. - Minimum Window Substring flips the goal to the shortest window that satisfies a condition.
- Longest Consecutive Sequence in an Array is the neighbouring hashing problem in this part of the SDE sheet.